testdata at spec-v0.15: the release objects are releases/<round>.cbor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
dev 21 hours ago
parent d2fad410fa
commit 7446db6084

@ -8,7 +8,7 @@ La especificación 0.15, de la rama `v0.15` de `datekeys-go`: el objeto release,
### La especificación 0.15: el objeto release y el release en la mano (07-10-2026) ### La especificación 0.15: el objeto release y el release en la mano (07-10-2026)
- `SPEC_VERSION` pasa a `0.15` y la versión a `0.4.0-dev`. `testdata` se sincroniza con `datekeys-go` en `3c3e737`, que añade `vectors/release.json`, los ficheros de `releases/` y el campo `source` de `mutations.json`. - `SPEC_VERSION` pasa a `0.15` y la versión a `0.4.0-dev`. `testdata` se sincroniza con el tag `spec-v0.15` de `datekeys-go` (`fe50885`), que añade `vectors/release.json`, los ficheros de `releases/` y el campo `source` de `mutations.json`.
- **El objeto release** (§47.1). `releaseobject.ts`, sin noble, hace lo de `provider/release.go` y `provider/archive.go` de Go, con sus textos byte a byte: `encodeRelease` y `decodeRelease`, con las capas del paso 10 (el tamaño de 1 a 1024 bytes antes de decodificar, el tipo y la versión, el schema); `parseRelease`, que lee también el JSON de drand como lo lee `encoding/json` de Go, con `ERR_RELEASE_INVALID` para cualquier fallo; `ReleaseSupplier` y `encodedRelease`, el release en la mano; y `ReleaseArchive`, el archivo de releases local, formato informativo (§50), cuyos fallos son `ERR_RELEASE_UNAVAILABLE`, también una ronda a ceros. - **El objeto release** (§47.1). `releaseobject.ts`, sin noble, hace lo de `provider/release.go` y `provider/archive.go` de Go, con sus textos byte a byte: `encodeRelease` y `decodeRelease`, con las capas del paso 10 (el tamaño de 1 a 1024 bytes antes de decodificar, el tipo y la versión, el schema); `parseRelease`, que lee también el JSON de drand como lo lee `encoding/json` de Go, con `ERR_RELEASE_INVALID` para cualquier fallo; `ReleaseSupplier` y `encodedRelease`, el release en la mano; y `ReleaseArchive`, el archivo de releases local, formato informativo (§50), cuyos fallos son `ERR_RELEASE_UNAVAILABLE`, también una ronda a ceros.
- **La cadena en el paso 10.** `verifyRelease` compara primero la cadena que nombra el release con la del perfil fijado (`ERR_PROFILE_MISMATCH`), y después la ronda y la firma. - **La cadena en el paso 10.** `verifyRelease` compara primero la cadena que nombra el release con la del perfil fijado (`ERR_PROFILE_MISMATCH`), y después la ronda y la firma.
- **El paso 9.c, opción B.** `open` acepta `release`, un release en la mano, en lugar de `source`: no se compara con el reloj, `Opened.clockBehind` dice si el reloj iba por detrás de la fecha, y el paso 10 lo decodifica con los códigos de ese paso. A una fuente de red no se le pide nada antes de `round_time`, como antes. El release verificado lleva la cadena del perfil fijado. - **El paso 9.c, opción B.** `open` acepta `release`, un release en la mano, en lugar de `source`: no se compara con el reloj, `Opened.clockBehind` dice si el reloj iba por detrás de la fecha, y el paso 10 lo decodifica con los códigos de ese paso. A una fuente de red no se le pide nada antes de `round_time`, como antes. El release verificado lleva la cadena del perfil fijado.

@ -21,7 +21,7 @@ Hay tres números de versión, cada uno con su significado, como en la referenci
| Versión | Dónde | Cambia cuando | | Versión | Dónde | Cambia cuando |
|---|---|---| |---|---|---|
| Formato | Dentro de los objetos: el formato de la cápsula, el `VERSION` del prelude de DKC1, 1, 2 o 3 al leer, que fija también la versión de schema de CONTROL_CBOR; y 1 en la trama DKK1 y en el schema de los demás objetos | Cambia el formato. Un lector rechaza una versión que no conoce (§22, §70) | | Formato | Dentro de los objetos: el formato de la cápsula, el `VERSION` del prelude de DKC1, 1, 2 o 3 al leer, que fija también la versión de schema de CONTROL_CBOR; y 1 en la trama DKK1 y en el schema de los demás objetos | Cambia el formato. Un lector rechaza una versión que no conoce (§22, §70) |
| Especificación | `SPEC_VERSION` de `src/lib/dkc/version.ts`, hoy `0.15`: la v0.15 de `datekeys-go`, en su rama `v0.15`. `testdata` está en `3c3e737`, y todos sus ficheros dicen `0.15` | Cambia el texto normativo | | Especificación | `SPEC_VERSION` de `src/lib/dkc/version.ts`, hoy `0.15`: la del tag `spec-v0.15` de `datekeys-go`, que aprobó el autor el 7 de octubre de 2026. `testdata` está en ese tag (`fe50885`), y todos sus ficheros dicen `0.15` | Cambia el texto normativo |
| Librería | `VERSION` de `src/lib/dkc/version.ts`, igual al campo `version` de `package.json` | Cambia la API o el comportamiento. Versionado semántico, sin promesa de estabilidad antes de 1.0.0 | | Librería | `VERSION` de `src/lib/dkc/version.ts`, igual al campo `version` de `package.json` | Cambia la API o el comportamiento. Versionado semántico, sin promesa de estabilidad antes de 1.0.0 |
`version.test.ts` comprueba que `VERSION` coincide con `package.json` y con su lockfile, y que `SPEC_VERSION` es la versión que nombran los vectores y fixtures compartidos; `vectors.test.ts` exige esa versión a cada fichero. El pie de la página muestra las dos. `version.test.ts` comprueba que `VERSION` coincide con `package.json` y con su lockfile, y que `SPEC_VERSION` es la versión que nombran los vectores y fixtures compartidos; `vectors.test.ts` exige esa versión a cada fichero. El pie de la página muestra las dos.
@ -466,7 +466,7 @@ Comprueba que los ficheros coinciden con `SOURCE.json`, sin faltantes ni sobrant
`.gitattributes` marca `testdata/**` como binario para que git no altere ningún byte. `.gitattributes` marca `testdata/**` como binario para que git no altere ningún byte.
Copia actual: la de `testdata/SOURCE.json` (la v0.15 de `datekeys-go`, `3c3e737` de su rama `v0.15`), que se sincroniza con `node scripts/sync-testdata.mjs sync --repo ../datekeys-go --commit 3c3e737`. Añade `vectors/release.json`, los ficheros de `releases/` y el campo `source` de `mutations.json`. Copia actual: la de `testdata/SOURCE.json` (el tag `spec-v0.15` de `datekeys-go`, `fe50885`), que se sincroniza con `node scripts/sync-testdata.mjs sync --commit spec-v0.15`. Añade `vectors/release.json`, los ficheros de `releases/` y el campo `source` de `mutations.json`.
## Licencia ## Licencia

@ -1,6 +1,6 @@
// The release object of spec v0.15, §47.1, as provider/release.go and // The release object of spec v0.15, §47.1, as provider/release.go and
// provider/archive.go of the Go reference: the release of a round as data // provider/archive.go of the Go reference: the release of a round as data
// that is kept, the content of a .dkr file and the answer of a release cache // that is kept: an entry of a release cache or archive and the answer of a release cache
// or of the Release API; drand's JSON, which a reader accepts too as the // or of the Release API; drand's JSON, which a reader accepts too as the
// input of the caller; the sources of a release in the caller's hand // input of the caller; the sources of a release in the caller's hand
// (provider.Supplier); and a local release archive, the informative format // (provider.Supplier); and a local release archive, the informative format
@ -10,13 +10,20 @@
// only decodes, so that the page can read what the person gives before // only decodes, so that the page can read what the person gives before
// loading the code that opens a capsule. // loading the code that opens a capsule.
import { equalBytes, goQuote, sha256, toHex, utf8Length } from './bytes.ts'; import { equalBytes, goQuote, sha256, toHex, utf8Length } from "./bytes.ts";
import { checkSchema, Decoder, Encoder, MAX_SAFE_UINT, peek, unmarshal } from './cbor.ts'; import {
import { DateKeysError, withContext } from './errors.ts'; checkSchema,
import { QUICKNET_SCHEME, type Profile } from './profile.ts'; Decoder,
Encoder,
MAX_SAFE_UINT,
peek,
unmarshal,
} from "./cbor.ts";
import { DateKeysError, withContext } from "./errors.ts";
import { QUICKNET_SCHEME, type Profile } from "./profile.ts";
/** The type tag of the release object (key 0). */ /** The type tag of the release object (key 0). */
export const RELEASE_TYPE_TAG = 'datekeys-release'; export const RELEASE_TYPE_TAG = "datekeys-release";
/** The schema version of the release object (key 1). */ /** The schema version of the release object (key 1). */
export const RELEASE_SCHEMA_VERSION = 1; export const RELEASE_SCHEMA_VERSION = 1;
/** /**
@ -32,7 +39,7 @@ export const MAX_SIGNATURE_LEN = 96;
export const MAX_RELEASE_JSON_SIZE = 8 << 10; export const MAX_RELEASE_JSON_SIZE = 8 << 10;
/** The type tag of the header of a release archive (spec v0.15, §50). */ /** The type tag of the header of a release archive (spec v0.15, §50). */
export const ARCHIVE_TYPE_TAG = 'datekeys-release-archive'; export const ARCHIVE_TYPE_TAG = "datekeys-release-archive";
/** The schema version of the header of a release archive. */ /** The schema version of the header of a release archive. */
export const ARCHIVE_SCHEMA_VERSION = 1; export const ARCHIVE_SCHEMA_VERSION = 1;
@ -94,12 +101,24 @@ function encodeWire(e: Encoder, w: ReleaseWire): void {
// ERR_NON_CANONICAL_CBOR: what each field means against the pinned profile // ERR_NON_CANONICAL_CBOR: what each field means against the pinned profile
// and the DateKey is checked by verifyRelease, at step 10. // and the DateKey is checked by verifyRelease, at step 10.
function decodeWire(d: Decoder): ReleaseWire { function decodeWire(d: Decoder): ReleaseWire {
const w: ReleaseWire = { chainHash: new Uint8Array(0), round: 0, signature: new Uint8Array(0) }; const w: ReleaseWire = {
chainHash: new Uint8Array(0),
round: 0,
signature: new Uint8Array(0),
};
const pairs = d.map(RELEASE_KEYS); const pairs = d.map(RELEASE_KEYS);
if (pairs !== RELEASE_KEYS) throw new DateKeysError('ERR_NON_CANONICAL_CBOR', `${pairs} keys, want all ${RELEASE_KEYS}`); if (pairs !== RELEASE_KEYS)
throw new DateKeysError(
"ERR_NON_CANONICAL_CBOR",
`${pairs} keys, want all ${RELEASE_KEYS}`,
);
for (let want = 0; want < RELEASE_KEYS; want++) { for (let want = 0; want < RELEASE_KEYS; want++) {
const k = d.key(); const k = d.key();
if (k !== want) throw new DateKeysError('ERR_NON_CANONICAL_CBOR', `key ${k} where key ${want} was expected`); if (k !== want)
throw new DateKeysError(
"ERR_NON_CANONICAL_CBOR",
`key ${k} where key ${want} was expected`,
);
withContext(`key ${k}`, () => { withContext(`key ${k}`, () => {
switch (want) { switch (want) {
case 0: case 0:
@ -113,7 +132,8 @@ function decodeWire(d: Decoder): ReleaseWire {
break; break;
case 3: case 3:
w.round = d.uint(MAX_SAFE_UINT); w.round = d.uint(MAX_SAFE_UINT);
if (w.round === 0) throw new DateKeysError('ERR_NON_CANONICAL_CBOR', 'round 0'); if (w.round === 0)
throw new DateKeysError("ERR_NON_CANONICAL_CBOR", "round 0");
break; break;
default: default:
w.signature = d.bstr(1, MAX_SIGNATURE_LEN); w.signature = d.bstr(1, MAX_SIGNATURE_LEN);
@ -125,7 +145,7 @@ function decodeWire(d: Decoder): ReleaseWire {
} }
/** /**
* The release object of `r`, the content of a .dkr file (spec v0.15, * The release object of `r`, as a release cache or archive keeps it (spec v0.15,
* §47.1): its chain hash, its round and its signature, as * §47.1): its chain hash, its round and its signature, as
* provider.EncodeRelease. It does not verify the release: verifyRelease * provider.EncodeRelease. It does not verify the release: verifyRelease
* does, against the pinned profile. * does, against the pinned profile.
@ -133,14 +153,20 @@ function decodeWire(d: Decoder): ReleaseWire {
export function encodeRelease(r: Release): Uint8Array { export function encodeRelease(r: Release): Uint8Array {
const chainHash = r.chainHash ?? new Uint8Array(0); const chainHash = r.chainHash ?? new Uint8Array(0);
if (chainHash.length !== 32) { if (chainHash.length !== 32) {
throw new DateKeysError('ERR_NON_CANONICAL_CBOR', `provider: release object: chain hash of ${chainHash.length} bytes, want 32`); throw new DateKeysError(
"ERR_NON_CANONICAL_CBOR",
`provider: release object: chain hash of ${chainHash.length} bytes, want 32`,
);
} }
if (!Number.isSafeInteger(r.round) || r.round < 1) { if (!Number.isSafeInteger(r.round) || r.round < 1) {
throw new DateKeysError('ERR_NON_CANONICAL_CBOR', `provider: release object: round ${r.round} outside 1..${MAX_SAFE_UINT}`); throw new DateKeysError(
"ERR_NON_CANONICAL_CBOR",
`provider: release object: round ${r.round} outside 1..${MAX_SAFE_UINT}`,
);
} }
if (r.signature.length === 0 || r.signature.length > MAX_SIGNATURE_LEN) { if (r.signature.length === 0 || r.signature.length > MAX_SIGNATURE_LEN) {
throw new DateKeysError( throw new DateKeysError(
'ERR_NON_CANONICAL_CBOR', "ERR_NON_CANONICAL_CBOR",
`provider: release object: signature of ${r.signature.length} bytes outside 1..${MAX_SIGNATURE_LEN}`, `provider: release object: signature of ${r.signature.length} bytes outside 1..${MAX_SIGNATURE_LEN}`,
); );
} }
@ -159,9 +185,12 @@ export function encodeRelease(r: Release): Uint8Array {
*/ */
export function decodeRelease(b: Uint8Array): Release { export function decodeRelease(b: Uint8Array): Release {
if (b.length === 0 || b.length > MAX_RELEASE_OBJECT_SIZE) { if (b.length === 0 || b.length > MAX_RELEASE_OBJECT_SIZE) {
throw new DateKeysError('ERR_NON_CANONICAL_CBOR', `provider: release object of ${b.length} bytes, outside 1..${MAX_RELEASE_OBJECT_SIZE}`); throw new DateKeysError(
"ERR_NON_CANONICAL_CBOR",
`provider: release object of ${b.length} bytes, outside 1..${MAX_RELEASE_OBJECT_SIZE}`,
);
} }
const w = withContext('provider: release object', () => { const w = withContext("provider: release object", () => {
checkSchema(b, RELEASE_TYPE_TAG, RELEASE_SCHEMA_VERSION); checkSchema(b, RELEASE_TYPE_TAG, RELEASE_SCHEMA_VERSION);
return unmarshal(b, decodeWire, encodeWire); return unmarshal(b, decodeWire, encodeWire);
}); });
@ -201,52 +230,76 @@ export async function parseRelease(b: Uint8Array): Promise<ParsedRelease> {
// integer of 0 to 2^64-1, fails the whole input. // integer of 0 to 2^64-1, fails the whole input.
async function parseDrandJSON(b: Uint8Array): Promise<ParsedRelease> { async function parseDrandJSON(b: Uint8Array): Promise<ParsedRelease> {
if (b.length > MAX_RELEASE_JSON_SIZE) { if (b.length > MAX_RELEASE_JSON_SIZE) {
throw new DateKeysError('ERR_RELEASE_INVALID', `provider: drand JSON of ${b.length} bytes, larger than ${MAX_RELEASE_JSON_SIZE}`); throw new DateKeysError(
"ERR_RELEASE_INVALID",
`provider: drand JSON of ${b.length} bytes, larger than ${MAX_RELEASE_JSON_SIZE}`,
);
} }
const malformed = (): DateKeysError => const malformed = (): DateKeysError =>
new DateKeysError('ERR_RELEASE_INVALID', 'provider: drand JSON: malformed, or without round or signature'); new DateKeysError(
"ERR_RELEASE_INVALID",
"provider: drand JSON: malformed, or without round or signature",
);
const text = new TextDecoder().decode(b); const text = new TextDecoder().decode(b);
const members = jsonMembers(text); const members = jsonMembers(text);
if (members === undefined) throw malformed(); if (members === undefined) throw malformed();
let round: bigint | undefined; let round: bigint | undefined;
let signature: string | undefined; let signature: string | undefined;
let randomness = ''; let randomness = "";
let typeError = false; let typeError = false;
for (const [key, raw] of members) { for (const [key, raw] of members) {
const name = foldName(key); const name = foldName(key);
if (name === 'round') { if (name === "round") {
if (raw === 'null') round = undefined; if (raw === "null") round = undefined;
else if (/^(0|[1-9][0-9]*)$/.test(raw) && BigInt(raw) < 2n ** 64n) round = BigInt(raw); else if (/^(0|[1-9][0-9]*)$/.test(raw) && BigInt(raw) < 2n ** 64n)
round = BigInt(raw);
else typeError = true; else typeError = true;
} else if (name === 'signature' || name === 'randomness') { } else if (name === "signature" || name === "randomness") {
if (raw === 'null') { if (raw === "null") {
if (name === 'signature') signature = undefined; if (name === "signature") signature = undefined;
} else if (raw.startsWith('"')) { } else if (raw.startsWith('"')) {
const v = JSON.parse(raw) as string; const v = JSON.parse(raw) as string;
if (name === 'signature') signature = v; if (name === "signature") signature = v;
else randomness = v; else randomness = v;
} else { } else {
typeError = true; typeError = true;
} }
} }
} }
if (typeError || round === undefined || signature === undefined) throw malformed(); if (typeError || round === undefined || signature === undefined)
throw malformed();
if (!/^([0-9a-fA-F]{2})*$/.test(signature)) { if (!/^([0-9a-fA-F]{2})*$/.test(signature)) {
throw new DateKeysError('ERR_RELEASE_INVALID', 'provider: drand JSON: signature is not hex'); throw new DateKeysError(
"ERR_RELEASE_INVALID",
"provider: drand JSON: signature is not hex",
);
} }
const sig = Uint8Array.from(signature.match(/../g) ?? [], (h) => parseInt(h, 16)); const sig = Uint8Array.from(signature.match(/../g) ?? [], (h) =>
if (randomness !== '' && randomness.toLowerCase() !== toHex(await sha256(sig))) { parseInt(h, 16),
throw new DateKeysError('ERR_RELEASE_INVALID', 'provider: drand JSON: randomness does not match the signature'); );
if (
randomness !== "" &&
randomness.toLowerCase() !== toHex(await sha256(sig))
) {
throw new DateKeysError(
"ERR_RELEASE_INVALID",
"provider: drand JSON: randomness does not match the signature",
);
} }
return { round: round <= BigInt(MAX_SAFE_UINT) ? Number(round) : round, signature: sig }; return {
round: round <= BigInt(MAX_SAFE_UINT) ? Number(round) : round,
signature: sig,
};
} }
// Go's encoding/json matches a key to a field name without case: ASCII // Go's encoding/json matches a key to a field name without case: ASCII
// letters in lower case, and any other character as unicode.ToLower of // letters in lower case, and any other character as unicode.ToLower of
// unicode.ToUpper, so that U+017F (long s) is an s and U+212A (Kelvin) a k. // unicode.ToUpper, so that U+017F (long s) is an s and U+212A (Kelvin) a k.
function foldName(s: string): string { function foldName(s: string): string {
let out = ''; let out = "";
for (const c of s) out += c.charCodeAt(0) < 0x80 ? c.toLowerCase() : c.toUpperCase().toLowerCase(); for (const c of s)
out +=
c.charCodeAt(0) < 0x80 ? c.toLowerCase() : c.toUpperCase().toLowerCase();
return out; return out;
} }
@ -263,16 +316,16 @@ function jsonMembers(text: string): [string, string][] | undefined {
const out: [string, string][] = []; const out: [string, string][] = [];
let i = 0; let i = 0;
const ws = (): void => { const ws = (): void => {
while (i < text.length && ' \t\n\r'.includes(text[i]!)) i++; while (i < text.length && " \t\n\r".includes(text[i]!)) i++;
}; };
const skipString = (): void => { const skipString = (): void => {
i++; i++;
while (text[i] !== '"') i += text[i] === '\\' ? 2 : 1; while (text[i] !== '"') i += text[i] === "\\" ? 2 : 1;
i++; i++;
}; };
const skipValue = (): void => { const skipValue = (): void => {
if (text[i] === '"') return skipString(); if (text[i] === '"') return skipString();
if (text[i] === '{' || text[i] === '[') { if (text[i] === "{" || text[i] === "[") {
let depth = 0; let depth = 0;
do { do {
const c = text[i]!; const c = text[i]!;
@ -280,18 +333,18 @@ function jsonMembers(text: string): [string, string][] | undefined {
skipString(); skipString();
continue; continue;
} }
if (c === '{' || c === '[') depth++; if (c === "{" || c === "[") depth++;
else if (c === '}' || c === ']') depth--; else if (c === "}" || c === "]") depth--;
i++; i++;
} while (depth > 0); } while (depth > 0);
return; return;
} }
while (i < text.length && !',}] \t\n\r'.includes(text[i]!)) i++; while (i < text.length && !",}] \t\n\r".includes(text[i]!)) i++;
}; };
ws(); ws();
i++; // { i++; // {
ws(); ws();
while (text[i] !== '}') { while (text[i] !== "}") {
const k0 = i; const k0 = i;
skipString(); skipString();
const key = JSON.parse(text.slice(k0, i)) as string; const key = JSON.parse(text.slice(k0, i)) as string;
@ -302,7 +355,7 @@ function jsonMembers(text: string): [string, string][] | undefined {
skipValue(); skipValue();
out.push([key, text.slice(v0, i)]); out.push([key, text.slice(v0, i)]);
ws(); ws();
if (text[i] === ',') { if (text[i] === ",") {
i++; i++;
ws(); ws();
} }
@ -316,7 +369,11 @@ function jsonMembers(text: string): [string, string][] | undefined {
* verifying the release (spec v0.15, §47.1). * verifying the release (spec v0.15, §47.1).
*/ */
export function newReleaseObject(p: Profile, r: Release): Uint8Array { export function newReleaseObject(p: Profile, r: Release): Uint8Array {
return encodeRelease({ round: r.round, signature: r.signature, chainHash: p.chainHash }); return encodeRelease({
round: r.round,
signature: r.signature,
chainHash: p.chainHash,
});
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -351,7 +408,8 @@ export function encodedRelease(b: Uint8Array): ReleaseSupplier {
// A local release archive (informative, spec v0.15, §50) // A local release archive (informative, spec v0.15, §50)
// The text of what a read threw, for an error of the archive. // The text of what a read threw, for an error of the archive.
const errText = (err: unknown): string => (err instanceof Error ? err.message : String(err)); const errText = (err: unknown): string =>
err instanceof Error ? err.message : String(err);
interface ArchiveHeader { interface ArchiveHeader {
chainHash: Uint8Array; chainHash: Uint8Array;
@ -378,7 +436,8 @@ function encodeArchiveWire(e: Encoder, h: ArchiveHeader): void {
function decodeArchiveWire(d: Decoder): ArchiveHeader { function decodeArchiveWire(d: Decoder): ArchiveHeader {
const h: ArchiveHeader = { chainHash: new Uint8Array(0), first: 0, count: 0 }; const h: ArchiveHeader = { chainHash: new Uint8Array(0), first: 0, count: 0 };
const pairs = d.map(ARCHIVE_KEYS); const pairs = d.map(ARCHIVE_KEYS);
if (pairs !== ARCHIVE_KEYS) throw new Error(`${pairs} keys, want all ${ARCHIVE_KEYS}`); if (pairs !== ARCHIVE_KEYS)
throw new Error(`${pairs} keys, want all ${ARCHIVE_KEYS}`);
for (let want = 0; want < ARCHIVE_KEYS; want++) { for (let want = 0; want < ARCHIVE_KEYS; want++) {
const k = d.key(); const k = d.key();
if (k !== want) throw new Error(`key ${k} where key ${want} was expected`); if (k !== want) throw new Error(`key ${k} where key ${want} was expected`);
@ -414,7 +473,11 @@ function decodeArchiveWire(d: Decoder): ArchiveHeader {
* signatures follow it, one after another, each with the length of a * signatures follow it, one after another, each with the length of a
* signature of the chain, and a round the archive lacks is written as zeros. * signature of the chain, and a round the archive lacks is written as zeros.
*/ */
export function encodeArchiveHeader(chainHash: Uint8Array, first: number, count: number): Uint8Array { export function encodeArchiveHeader(
chainHash: Uint8Array,
first: number,
count: number,
): Uint8Array {
if ( if (
chainHash.length !== 32 || chainHash.length !== 32 ||
!Number.isSafeInteger(first) || !Number.isSafeInteger(first) ||
@ -423,7 +486,9 @@ export function encodeArchiveHeader(chainHash: Uint8Array, first: number, count:
count < 1 || count < 1 ||
first > MAX_SAFE_UINT - count + 1 first > MAX_SAFE_UINT - count + 1
) { ) {
throw new Error(`provider: archive header: chain hash of ${chainHash.length} bytes, rounds ${first} to ${first} + ${count} - 1`); throw new Error(
`provider: archive header: chain hash of ${chainHash.length} bytes, rounds ${first} to ${first} + ${count} - 1`,
);
} }
const e = new Encoder(); const e = new Encoder();
encodeArchiveWire(e, { chainHash, first, count }); encodeArchiveWire(e, { chainHash, first, count });
@ -469,8 +534,12 @@ export class ReleaseArchive implements ReleaseSupplier {
async supply(p: Profile, round: number): Promise<Uint8Array> { async supply(p: Profile, round: number): Promise<Uint8Array> {
const unavailable = (detail: string): DateKeysError => const unavailable = (detail: string): DateKeysError =>
new DateKeysError('ERR_RELEASE_UNAVAILABLE', `provider: release archive: ${detail}`); new DateKeysError(
const size = this.#data instanceof Uint8Array ? this.#data.length : this.#data.size; "ERR_RELEASE_UNAVAILABLE",
`provider: release archive: ${detail}`,
);
const size =
this.#data instanceof Uint8Array ? this.#data.length : this.#data.size;
let head: Uint8Array; let head: Uint8Array;
try { try {
head = await this.#read(0, Math.min(size, MAX_ARCHIVE_HEADER)); head = await this.#read(0, Math.min(size, MAX_ARCHIVE_HEADER));
@ -497,20 +566,39 @@ export class ReleaseArchive implements ReleaseSupplier {
const enc = e.out(); const enc = e.out();
/* v8 ignore next 3 -- @preserve: what the strict decoder read encodes back to the same bytes */ /* v8 ignore next 3 -- @preserve: what the strict decoder read encodes back to the same bytes */
if (!equalBytes(enc, head.subarray(0, Math.min(enc.length, head.length)))) { if (!equalBytes(enc, head.subarray(0, Math.min(enc.length, head.length)))) {
throw unavailable('its header is not the deterministic encoding of its value'); throw unavailable(
"its header is not the deterministic encoding of its value",
);
} }
if (!equalBytes(h.chainHash, p.chainHash)) { if (!equalBytes(h.chainHash, p.chainHash)) {
throw unavailable(`archive of chain ${toHex(h.chainHash)}, the pinned profile ${p.id} is chain ${toHex(p.chainHash)}`); throw unavailable(
`archive of chain ${toHex(h.chainHash)}, the pinned profile ${p.id} is chain ${toHex(p.chainHash)}`,
);
} }
if (h.first === 0 || h.count === 0 || round < h.first || round - h.first >= h.count) { if (
throw unavailable(`round ${round} is not in the archive, which holds ${h.count} rounds from ${h.first}`); h.first === 0 ||
h.count === 0 ||
round < h.first ||
round - h.first >= h.count
) {
throw unavailable(
`round ${round} is not in the archive, which holds ${h.count} rounds from ${h.first}`,
);
} }
if (p.provider !== 'drand') throw unavailable(`profile ${p.id}: provider ${goQuote(p.provider)} is not drand: ERR_UNKNOWN_PROFILE`); if (p.provider !== "drand")
if (p.scheme !== QUICKNET_SCHEME) throw unavailable(`profile ${p.id}: ${goQuote(p.scheme)} is not a drand scheme: ERR_UNKNOWN_PROFILE`); throw unavailable(
`profile ${p.id}: provider ${goQuote(p.provider)} is not drand: ERR_UNKNOWN_PROFILE`,
);
if (p.scheme !== QUICKNET_SCHEME)
throw unavailable(
`profile ${p.id}: ${goQuote(p.scheme)} is not a drand scheme: ERR_UNKNOWN_PROFILE`,
);
const n = QUICKNET_SIGNATURE_LEN; const n = QUICKNET_SIGNATURE_LEN;
const want = BigInt(enc.length) + BigInt(h.count) * BigInt(n); const want = BigInt(enc.length) + BigInt(h.count) * BigInt(n);
if (BigInt(h.count) > (1n << 62n) / BigInt(n) || BigInt(size) !== want) { if (BigInt(h.count) > (1n << 62n) / BigInt(n) || BigInt(size) !== want) {
throw unavailable(`${size} bytes, its header announces ${h.count} rounds of ${n} bytes`); throw unavailable(
`${size} bytes, its header announces ${h.count} rounds of ${n} bytes`,
);
} }
let sig: Uint8Array; let sig: Uint8Array;
try { try {
@ -518,7 +606,8 @@ export class ReleaseArchive implements ReleaseSupplier {
} catch (err) { } catch (err) {
throw unavailable(errText(err)); throw unavailable(errText(err));
} }
if (sig.every((x) => x === 0)) throw unavailable(`round ${round} is missing: its entry is zeros`); if (sig.every((x) => x === 0))
throw unavailable(`round ${round} is missing: its entry is zeros`);
return encodeRelease({ chainHash: h.chainHash, round, signature: sig }); return encodeRelease({ chainHash: h.chainHash, round, signature: sig });
} }
} }

25
testdata/README.md vendored

@ -51,8 +51,8 @@ Conventions for every file:
| `vectors/dk1.json` | canonical `dk1_` strings, and rejected encodings with their code | §18, §19, §66 | | `vectors/dk1.json` | canonical `dk1_` strings, and rejected encodings with their code | §18, §19, §66 |
| `vectors/cbor.json` | the CBOR profile, and one block of vectors per schema, CONTROL_CBOR in the three formats | §58, CDDL | | `vectors/cbor.json` | the CBOR profile, and one block of vectors per schema, CONTROL_CBOR in the three formats | §58, CDDL |
| `vectors/tlock_ibe.json` | H2 of the tlock IBE: the serialization of an element of GT | §63 step 11 | | `vectors/tlock_ibe.json` | H2 of the tlock IBE: the serialization of an element of GT | §63 step 11 |
| `vectors/release.json` | the release object, the content of a `.dkr` file, and drand's JSON, each with the result of step 10; the lookups of a local release archive | §47.1, §50, §63 step 10 (v0.15) | | `vectors/release.json` | the release object and drand's JSON, each with the result of step 10; the lookups of a local release archive | §47.1, §50, §63 step 10 (v0.15) |
| `releases/<round>.dkr` | the release object of each published round of the tests: 1000, 1001, 1004 and 2000 | §47.1 (v0.15) | | `releases/<round>.cbor` | the release object of each published round of the tests: 1000, 1001, 1004 and 2000 | §47.1 (v0.15) |
| `releases/archive_1000_1004.bin` | a local release archive, the informative format of §50, of rounds 1000 to 1004, two of them missing | §50 (v0.15) | | `releases/archive_1000_1004.bin` | a local release archive, the informative format of §50, of rounds 1000 to 1004, two of them missing | §50 (v0.15) |
| `vectors/tlock_steps.json` | steps 10 and 11 for Quicknet value by value: the message of a round, its hash to G1, and the decryption of a tlock stanza with H2, H4, H3 and the file key | §63 steps 10 and 11 (v0.14) | | `vectors/tlock_steps.json` | steps 10 and 11 for Quicknet value by value: the message of a round, its hash to G1, and the decryption of a tlock stanza with H2, H4, H3 and the file key | §63 steps 10 and 11 (v0.14) |
| `vectors/padding.json` | the padding of formats 2 and 3: P for each content length L, and the length of PAYLOAD_AGE | §29.1 | | `vectors/padding.json` | the padding of formats 2 and 3: P for each content length L, and the length of PAYLOAD_AGE | §29.1 |
@ -320,8 +320,9 @@ writes them, give `0118eea9d5971745f71e3c94926f1717` and another FK_TIME.
## `vectors/release.json` and `releases/` ## `vectors/release.json` and `releases/`
The release object of spec v0.15, §47.1: the release of a round as a file, The release object of spec v0.15, §47.1: the release of a round as data,
`.dkr`, that a person keeps next to the capsule. It is deterministic CBOR with the answer of the Release API, an entry of a Release Cache and a release the
caller gives from a file or from an archive. It is deterministic CBOR with
the profile of §58, a map of five keys, all required: the profile of §58, a map of five keys, all required:
```text ```text
@ -332,10 +333,11 @@ the profile of §58, a map of five keys, all required:
4 → signature (1 to 96 bytes; 48 in Quicknet) 4 → signature (1 to 96 bytes; 48 in Quicknet)
``` ```
The object of a round above 255 measures 111 bytes. `releases/<round>.dkr` is The object of a round above 255 measures 111 bytes. `releases/<round>.cbor`
the object of each published round the fixtures use, 1000, 1001, 1004 and is the object of each published round the fixtures use, 1000, 1001, 1004 and
2000, with the Quicknet chain hash: the release that opens each fixture, as a 2000, with the Quicknet chain hash: the release that opens each fixture, as a
file. file. The protocol gives the object no file extension; `.cbor` is the generic
one of CBOR (RFC 8949).
`release.json` has three lists: `release.json` has three lists:
@ -364,7 +366,7 @@ file.
each round, one after another; a round the archive lacks is 48 zero bytes. each round, one after another; a round the archive lacks is 48 zero bytes.
This one holds rounds 1000 to 1004, and 1002 and 1003 are zeros. Each lookup This one holds rounds 1000 to 1004, and 1002 and 1003 are zeros. Each lookup
gives a `round` and its `result`: `ok` with the `encoding` of the release gives a `round` and its `result`: `ok` with the `encoding` of the release
object the archive supplies, the `.dkr` of that round, or object the archive supplies, `releases/<round>.cbor` for that round, or
`ERR_RELEASE_UNAVAILABLE` for a round the archive lacks or does not cover. `ERR_RELEASE_UNAVAILABLE` for a round the archive lacks or does not cover.
The texts are those of the reference, for an implementation that wants to The texts are those of the reference, for an implementation that wants to
@ -848,12 +850,13 @@ reading flow (`capsule.Open`, §63) must fail.
cases only, is the chain the release object names when it is not the cases only, is the chain the release object names when it is not the
Quicknet chain. Quicknet chain.
- `source` (v0.15): what kind of source answers, spec v0.15 §63 step 9: - `source` (v0.15): what kind of source answers, spec v0.15 §63 step 9:
- `supplied`: the release is in the caller's hand, as a `.dkr` would be. - `supplied`: the release is in the caller's hand, as a release object
read from a file would be.
The reader is given the release object of `release`, with the Quicknet The reader is given the release object of `release`, with the Quicknet
chain hash unless `chain_hash` says another, and decodes and verifies it chain hash unless `chain_hash` says another, and decodes and verifies it
at step 10: one that breaks a rule of step 10 gets the code of step 10. at step 10: one that breaks a rule of step 10 gets the code of step 10.
The clock is not compared with the round time (step 9.c): with a `now` The clock is not compared with the round time (step 9.c): with a `now`
before it, the capsule opens all the same. Every case but three is before it, the capsule opens all the same. Every case but two is
`supplied`. `supplied`.
- `network`: a network source, such as a drand relay. It is never asked - `network`: a network source, such as a drand relay. It is never asked
before the round time (step 9.c), and it verifies its answer with the before the round time (step 9.c), and it verifies its answer with the
@ -888,7 +891,7 @@ reading flow (`capsule.Open`, §63) must fail.
Every case reproduces offline: the recorded release stands in for the network. Every case reproduces offline: the recorded release stands in for the network.
What v0.15 changed in this file: every case gained `source`, `supplied` but What v0.15 changed in this file: every case gained `source`, `supplied` but
for three; the further case "round not reached yet", a valid release of round for two; the further case "round not reached yet", a valid release of round
1000 and a clock one nanosecond before its round time, was 1000 and a clock one nanosecond before its round time, was
`ERR_RELEASE_UNAVAILABLE` at step 9 and now opens (`ok`, step 0, with `ERR_RELEASE_UNAVAILABLE` at step 9 and now opens (`ok`, step 0, with
`network` true), because the release is in the caller's hand; and four cases `network` true), because the release is in the caller's hand; and four cases

@ -1,8 +1,8 @@
{ {
"module": "g.activething.com/go/DateKeys", "module": "g.activething.com/go/DateKeys",
"commit": "3c3e7370c2b6319e1fc261a1b26c88f1d43e3fa6", "commit": "fe5088549186465e08d55f89680be986076bf165",
"files": { "files": {
"README.md": "4cf931dfa0a1d3a33dd24cef95b3f4fc30d190ed912ee8d30808ac9bc3aca930", "README.md": "d26b3f507edf5afe89600f063cb509b9ef62a908b93ed476bff964b5a443bf4b",
"fixtures/empty_payload.dkc": "871e9bf05b52bbae17f3adfbbf97b46e7f0e53aa8f57bcaa506e43f36f53a9d4", "fixtures/empty_payload.dkc": "871e9bf05b52bbae17f3adfbbf97b46e7f0e53aa8f57bcaa506e43f36f53a9d4",
"fixtures/empty_payload.inspect.json": "373e5d012b023ad58bbb54cbdffe0bed9e50c637438a4083ddb74d5414c59f59", "fixtures/empty_payload.inspect.json": "373e5d012b023ad58bbb54cbdffe0bed9e50c637438a4083ddb74d5414c59f59",
"fixtures/empty_payload.json": "d1fc459ab76d4ee0231a8c6b7dfc212fcf8da90e7a392b30133f646b3a9df4db", "fixtures/empty_payload.json": "d1fc459ab76d4ee0231a8c6b7dfc212fcf8da90e7a392b30133f646b3a9df4db",
@ -119,10 +119,10 @@
"fixtures/time_only_extensions.inspect.json": "6f957b028da8a4a495b5e951ced0b91e0678128dac4e962b02d024b9439a0ba1", "fixtures/time_only_extensions.inspect.json": "6f957b028da8a4a495b5e951ced0b91e0678128dac4e962b02d024b9439a0ba1",
"fixtures/time_only_extensions.json": "4bb636805cc681ba1c74aa426f5afda72580f4350929a720da49eeccee3eb2be", "fixtures/time_only_extensions.json": "4bb636805cc681ba1c74aa426f5afda72580f4350929a720da49eeccee3eb2be",
"fixtures/time_only_extensions.plaintext": "1129768e195e2f1e50b7a6f926b6eebef120212c29b5642c8a662c503b2a9131", "fixtures/time_only_extensions.plaintext": "1129768e195e2f1e50b7a6f926b6eebef120212c29b5642c8a662c503b2a9131",
"releases/1000.dkr": "5d2e86210d2e8d64ce36e55edf3ff6c8997fda4bc06fec7fd5feb0dd6cab8293", "releases/1000.cbor": "5d2e86210d2e8d64ce36e55edf3ff6c8997fda4bc06fec7fd5feb0dd6cab8293",
"releases/1001.dkr": "2b55dcc09dfe8fa97192aa6d9a85f9fc50206142d52f66329261cfe9e03de755", "releases/1001.cbor": "2b55dcc09dfe8fa97192aa6d9a85f9fc50206142d52f66329261cfe9e03de755",
"releases/1004.dkr": "aabdffe0fb944d8796528a6682fdbafb448b1e5da164ee039b6c3ee7f354e766", "releases/1004.cbor": "aabdffe0fb944d8796528a6682fdbafb448b1e5da164ee039b6c3ee7f354e766",
"releases/2000.dkr": "9e37be0004850faaa8541658a90ee8aba0832fccf2b695df42c89cb3f7de29ff", "releases/2000.cbor": "9e37be0004850faaa8541658a90ee8aba0832fccf2b695df42c89cb3f7de29ff",
"releases/archive_1000_1004.bin": "bb53d542abd704f3af9f6436c9178f65bf812a06630607b3aad3a09eaed0cce1", "releases/archive_1000_1004.bin": "bb53d542abd704f3af9f6436c9178f65bf812a06630607b3aad3a09eaed0cce1",
"vectors/cbor.json": "715c8e7ca88d17c4e68a8764350217f108e96484e59282d384a032169d36bfb8", "vectors/cbor.json": "715c8e7ca88d17c4e68a8764350217f108e96484e59282d384a032169d36bfb8",
"vectors/dk1.json": "e2b849b1f606e7961a8571c305dcd0c4374f03c8943a3a715b20a9a43002ea44", "vectors/dk1.json": "e2b849b1f606e7961a8571c305dcd0c4374f03c8943a3a715b20a9a43002ea44",
@ -137,7 +137,7 @@
"vectors/paths.json": "a33ecdbd6a191d693600e18879e15a5813d77b133d46f30c5535aae72eeaeb04", "vectors/paths.json": "a33ecdbd6a191d693600e18879e15a5813d77b133d46f30c5535aae72eeaeb04",
"vectors/profile_quicknet.json": "e291d5167cdce9b9e24993571a7f349e35fbbcfea793665883ec9ec8d479b498", "vectors/profile_quicknet.json": "e291d5167cdce9b9e24993571a7f349e35fbbcfea793665883ec9ec8d479b498",
"vectors/quicknet_rounds.json": "bf990896dddc51a91e309143fede773adeae918ef47433e0ab6132a4456ce9a8", "vectors/quicknet_rounds.json": "bf990896dddc51a91e309143fede773adeae918ef47433e0ab6132a4456ce9a8",
"vectors/release.json": "4e9d4902c041d70361bcf6e0f069b963fa91c22e494f3239ffeda2c2688da5b5", "vectors/release.json": "97bd46e055024a00f6a765f840b47ebfea5dad1e08a88c70d2cf42a77df6d2ba",
"vectors/resolved_ip.json": "7c554e7c3f272a62a89c3f3e97203dc7d5dc50290bbe356f4a8efd44a19eea70", "vectors/resolved_ip.json": "7c554e7c3f272a62a89c3f3e97203dc7d5dc50290bbe356f4a8efd44a19eea70",
"vectors/security.json": "6045492767cbeb02fce5b6faf6cd0e179ffe96c898c8c023793e4a138b0277f0", "vectors/security.json": "6045492767cbeb02fce5b6faf6cd0e179ffe96c898c8c023793e4a138b0277f0",
"vectors/security_cms.json": "4915fa3cfe93b1ad92e91a68bba7517c3b2dc3e33fd9def7340b3a045eff99cf", "vectors/security_cms.json": "4915fa3cfe93b1ad92e91a68bba7517c3b2dc3e33fd9def7340b3a045eff99cf",

@ -1,6 +1,6 @@
{ {
"spec": "0.15", "spec": "0.15",
"description": "The release object, the content of a .dkr file (spec v0.15, §47.1), and drand's JSON as the input of the caller, each checked against the pinned Quicknet profile and the round of a DateKey as step 10 of spec §63 checks a release that the caller supplies; and the lookups of a local release archive (spec v0.15, §50). See testdata/README.md.", "description": "The release object (spec v0.15, §47.1), and drand's JSON as the input of the caller, each checked against the pinned Quicknet profile and the round of a DateKey as step 10 of spec §63 checks a release that the caller supplies; and the lookups of a local release archive (spec v0.15, §50). See testdata/README.md.",
"profile": "datekeys:quicknet:v1", "profile": "datekeys:quicknet:v1",
"objects": [ "objects": [
{ {

Loading…
Cancel
Save

Powered by TurnKey Linux.