diff --git a/README.md b/README.md index 30b5fdc..fe3df68 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Sin dependencias de ejecución. Funciona en navegadores y en Node 20+: solo usa `crypto.subtle` solo existe en contextos seguros: `https`, o `http` en `localhost`. La página del paso 5 servida por `http` desde una IP de la red local (por ejemplo `vite --host` para probar en un móvil) no lo tiene, y `inspect` rechaza entonces con un `Error` que lo dice (`SHA-256 needs Web Crypto (crypto.subtle), …`) en vez de dar un veredicto. El registro por defecto no memoriza ese fallo: la siguiente llamada lo vuelve a intentar. -| Fichero | Contenido | Equivale en Go (`692cf87`) | +| Fichero | Contenido | Equivale en Go (`f6f2e9f`) | |---|---|---| | `errors.ts` | `DateKeysError` con el código normativo de §69 (`ERR_*`); mensajes con la forma `contexto: CÓDIGO` de Go | `errors.go` | | `bytes.ts` | Hex, UTF-8 estricto, `goQuote` (el `%q` de Go, con la tabla de `strconv.IsPrint` de Go 1.26 fijada en el código) y `sha256` (Web Crypto) | `strconv`, `unicode/utf8` | @@ -39,7 +39,9 @@ Los tests (`*.test.ts`) están junto a cada fichero. ### Equivalencia con la referencia Go -El comportamiento se contrastó con la librería Go en `3820066` (`692cf87` solo cambia la regla de UTF-8 de `dk1_` descrita abajo) mediante un oráculo diferencial fuera del repositorio: 483 527 entradas de tres semillas. Son mutaciones de los cinco `.dkc` oficiales de todas las clases (bits, bytes, truncados, inserciones, borrados, longitudes y campos del prelude, cabeceras reescritas con cambios de CBOR, DateKeys, arrays de extensiones con y sin registro de extensiones, cabeceras `age` de SEALED_CONTROL y de PAYLOAD_AGE, argumentos del stanza tlock, rondas, secciones cambiadas de sitio y combinaciones de varios defectos); CBOR de cada esquema (PUBLIC_HEADER, CONTROL_CBOR, cuerpo y fichero `.dkk`, Provider Profile, arrays de extensiones); `walk`, `peek` y `checkSchema`; cabeceras `age`, preludes, cadenas `dk1_`, rondas y fechas. En todas coinciden el veredicto, el código y el paso, y también el texto del error, el valor decodificado y la salida entera de `datekeys inspect -json`, byte a byte. +El comportamiento se contrastó con la librería Go en `3820066` (`692cf87` solo cambia la regla de UTF-8 de `dk1_` descrita abajo) mediante un oráculo diferencial fuera del repositorio: 483 527 entradas de tres semillas. Son mutaciones de los cinco `.dkc` oficiales de todas las clases (bits, bytes, truncados, inserciones, borrados, longitudes y campos del prelude, cabeceras reescritas con cambios de CBOR, DateKeys, arrays de extensiones con y sin registro de extensiones, cabeceras `age` de SEALED_CONTROL y de PAYLOAD_AGE, argumentos del stanza tlock, rondas, secciones cambiadas de sitio y combinaciones de varios defectos); CBOR de cada esquema (PUBLIC_HEADER, CONTROL_CBOR, cuerpo y fichero `.dkk`, Provider Profile, arrays de extensiones); `walk`, `peek` y `checkSchema`; cabeceras `age`, preludes, cadenas `dk1_`, rondas y fechas. En todas coinciden el veredicto, el código y el paso, y también el texto del error, el valor decodificado y la salida entera de `datekeys inspect -json`, byte a byte. Un segundo diferencial con otro generador, de 407 196 entradas, se repitió contra `f6f2e9f` (la enmienda de canonicidad de puntos) con el mismo resultado, textos incluidos. + +Como la referencia desde `f6f2e9f`, los errores no copian el texto de una librería: una cabecera `age` que no se puede leer da siempre `agewrap: not an age v1 header: malformed, truncated or beyond the parser limits` (`ERR_INTEGRITY`). El motivo del parser, con las palabras de `age`, queda en `cause` del error, fuera del mensaje, y los tests lo comparan con los textos de `age` para comprobar que se rechaza por la misma razón. Precedencia de errores (§69.1): primero la trama; después el tipo y la versión de esquema (`checkSchema`); después el perfil CBOR y el CDDL, con los límites de implementación de §74, en una sola decodificación (`unmarshal` con el decodificador del esquema, que ya comprueba tipos, tamaños, rangos, `access_policy`, `extension_version`, el máximo de 64 extensiones y su orden); y solo entonces los campos con código propio, en orden de clave: DateKey, perfil fijado y extensiones críticas en PUBLIC_HEADER; `access_type` y `access_material` en la `.dkk`; los campos y la autocomprobación de `chain_hash` en el Provider Profile. Entre pasos decide el orden de §63. Un PUBLIC_HEADER que rompe a la vez el CDDL y la DateKey da `ERR_NON_CANONICAL_CBOR`, como en la referencia de `3820066`; la de `afb44a3` leía `access_policy` y `extension_version` como `uint64` y los acotaba después de la DateKey, y la vía `wideUint` que lo imitaba ya no existe. @@ -136,7 +138,7 @@ Umbrales de cobertura (`vitest.config.ts`): `cbor.ts` al 100 % en líneas, ramas - `testdata/fixtures/*.inspect.json`: la vista de `inspect` de cada `.dkc`, escrita con `inspectJSON` como la imprime la CLI (`file` incluido), es idéntica byte a byte al fichero, también el texto de cada paso. - `testdata/vectors/dk1.json` (con los tres vectores de los refinamientos de §19: LF dentro del Base64, CR y LF después, y la versión `1.0000000000000001`), `quicknet_rounds.json` y `profile_quicknet.json`: se ejecutan todos. - `testdata/vectors/cbor.json`: cada vector genérico (`accept` y `reject`) pasa por `walk` con los `max_depth` y `max_len` del fichero; los enteros aceptados comparan su `value` (número o, por encima de 2⁵³ − 1, `bigint`), y los rechazados «above max_len» o «above max_depth» se aceptan sin ese límite. Cada vector de `schemas` pasa por el decodificador de su esquema (`decodeProfile`, `decodeHeader`, `decodeControl`, `decodeAccessKeyBody`) con el código exacto, y un objeto aceptado se reescribe a los mismos bytes. -- `testdata/vectors/mutations.json`: se leen los 55 casos enteros (ediciones sobre un fixture o hex congelado, release, reloj, registro, extensiones, `.dkk` e identidades). Los 31 de los pasos 1 a 8 pasan por `inspect` con su registro y sus extensiones, y dan el mismo código y el mismo paso; los 24 de los pasos 9 a 18 necesitan `open` (fase 2) y se saltan uno a uno con ese motivo, y un test fija los dos recuentos. Las `.dkk` ofrecidas se decodifican. +- `testdata/vectors/mutations.json`: se leen los 65 casos enteros (ediciones sobre un fixture o hex congelado, release, reloj, registro, extensiones, `.dkk` e identidades). Los 31 de los pasos 1 a 8 pasan por `inspect` con su registro y sus extensiones, y dan el mismo código y el mismo paso; los 34 de los pasos 9 a 18 (entre ellos las 10 mutaciones de la enmienda de canonicidad de puntos, en los pasos 10 y 11) necesitan `open` (fase 2) y se saltan uno a uno con ese motivo, y un test fija los dos recuentos. Las `.dkk` ofrecidas se decodifican. - `testdata/vectors/inspect_differential.json`: las 1 825 mutaciones dan el mismo veredicto, código y paso que Go; los `bases` se comprueban por su SHA-256. - Todo se lee con los formatos de `testdata/README.md` (`testing/vectors.ts`): una clave desconocida o que falta, un valor de otro tipo, un código que no es de §69 o una edición fuera de su base hacen fallar el fichero con su motivo; nada se salta en silencio. - Todo fichero de `testdata/` tiene que ejecutarlo algún test: un nombre nuevo exportado por Go (otro `vectors/*.json`, un fichero de fixture que ningún JSON nombra) hace fallar `testdata/ holds no file that no test runs` hasta que se le añade su bloque. @@ -175,7 +177,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. -Copia actual: la de `testdata/SOURCE.json` (commit `692cf87`, rama `v0.8.2`). +Copia actual: la de `testdata/SOURCE.json` (commit `f6f2e9f`, rama `v0.8.2`). ## Licencia diff --git a/src/lib/dkc/age.test.ts b/src/lib/dkc/age.test.ts index 5d0894f..eaeb35c 100644 --- a/src/lib/dkc/age.test.ts +++ b/src/lib/dkc/age.test.ts @@ -4,6 +4,16 @@ import { quicknet } from './profile.ts'; import { expectCode, hx, readBytes, readJSON } from './testing/testdata.ts'; const te = new TextEncoder(); +// The reason behind the fixed ERR_INTEGRITY text of parseAgeHeader, in the +// words of age (the error's cause). +const causeOf = (f: () => unknown): string => { + try { + f(); + } catch (err) { + return ((err as Error).cause as Error | undefined)?.message ?? ''; + } + return ''; +}; const INTRO = 'age-encryption.org/v1'; const MAC = '--- ' + 'A'.repeat(43); const age = (...lines: string[]): Uint8Array => te.encode(lines.join('\n') + '\n'); @@ -126,7 +136,8 @@ describe('parseAgeHeader', () => { expect(parseAgeHeader(at).length).toBe(limit); const over = header(limit + 1); expect(over.length).toBe(limit + 1); - expect(() => parseAgeHeader(over)).toThrow(/header exceeds 2 MiB/); + expect(() => parseAgeHeader(over)).toThrow(/^agewrap: not an age v1 header: .*: ERR_INTEGRITY$/); + expect(causeOf(() => parseAgeHeader(over))).toMatch(/header exceeds 2 MiB$/); }); it('rejects everything age rejects', () => { @@ -179,12 +190,17 @@ describe('parseAgeHeader', () => { expect(cases).toHaveLength(GO_MESSAGES.length); for (const [i, c] of cases.entries()) { let msg = ''; + let cause = ''; try { parseAgeHeader(c); } catch (err) { msg = (err as Error).message; + cause = ((err as Error).cause as Error).message; } - expect(msg, `case ${i}`).toBe(`agewrap: not a valid age file: failed to read header: ${GO_MESSAGES[i]!}: ERR_INTEGRITY`); + // The message is the reference's fixed text; the cause keeps the reason + // in the words of age, so the parser still rejects for the same reason. + expect(msg, `case ${i}`).toBe(`agewrap: not an age v1 header: malformed, truncated or beyond the parser limits: ERR_INTEGRITY`); + expect(cause, `case ${i}`).toBe(`failed to read header: ${GO_MESSAGES[i]!}`); } }); @@ -199,13 +215,13 @@ describe('parseAgeHeader', () => { ]; for (const [arg, quoted] of lines) { const file = new Uint8Array([...te.encode(`${INTRO}\n-> X25519 `), ...arg, 0x0a]); - let msg = ''; + let cause = ''; try { ageStanzas(file); } catch (err) { - msg = (err as Error).message; + cause = ((err as Error).cause as Error).message; } - expect(msg).toBe(`agewrap: not a valid age file: failed to read header: failed to parse header: malformed stanza: "-> X25519 ${quoted}${B}n": ERR_INTEGRITY`); + expect(cause).toBe(`failed to read header: failed to parse header: malformed stanza: "-> X25519 ${quoted}${B}n"`); } }); @@ -218,15 +234,19 @@ describe('parseAgeHeader', () => { expect(build(fit).length).toBeLessThanOrEqual(2 << 20); expect(parseAgeHeader(build(fit)).stanzas[0]!.body).toHaveLength(fit * 48); expect(build(fit + 1).length).toBeGreaterThan(2 << 20); - expectCode(() => parseAgeHeader(build(fit + 1)), 'ERR_INTEGRITY', /: failed to read header: failed to read header: parsing age header: header exceeds 2 MiB: /); + expectCode(() => parseAgeHeader(build(fit + 1)), 'ERR_INTEGRITY'); + expect(causeOf(() => parseAgeHeader(build(fit + 1)))).toBe('failed to read header: failed to read header: parsing age header: header exceeds 2 MiB'); // A line cut by the limit, in a larger file, and the three bytes of "---". const cut = new Uint8Array((2 << 20) + 10).fill(0x41); cut.set(te.encode(prefix)); - expectCode(() => parseAgeHeader(cut), 'ERR_INTEGRITY', /: failed to parse header: failed to read line: parsing age header: header exceeds 2 MiB: /); + expectCode(() => parseAgeHeader(cut), 'ERR_INTEGRITY'); + expect(causeOf(() => parseAgeHeader(cut))).toMatch(/: failed to parse header: failed to read line: parsing age header: header exceeds 2 MiB$/); const edge = build(fit); const tail = new Uint8Array(edge.length - MAC.length - 1); tail.set(edge.subarray(0, tail.length)); - expectCode(() => parseAgeHeader(new Uint8Array([...tail, 0x2d, 0x2d])), 'ERR_INTEGRITY', /: parsing age header: failed to read header: EOF: /); + const eof = new Uint8Array([...tail, 0x2d, 0x2d]); + expectCode(() => parseAgeHeader(eof), 'ERR_INTEGRITY'); + expect(causeOf(() => parseAgeHeader(eof))).toMatch(/: parsing age header: failed to read header: EOF$/); }); }); diff --git a/src/lib/dkc/age.ts b/src/lib/dkc/age.ts index 5bb7957..b07921b 100644 --- a/src/lib/dkc/age.ts +++ b/src/lib/dkc/age.ts @@ -237,14 +237,19 @@ function parse(file: Uint8Array): AgeHeader { /** * Parses the age header at the start of `file`, as age's format.Parse, and * returns its stanzas and MAC. Bytes after the header are not read. Throws - * ERR_INTEGRITY, with the error text of age, for anything age would not - * parse. + * ERR_INTEGRITY for anything age would not parse, with the fixed text of the + * reference (agewrap.Stanzas) as its message and the parser's reason, in the + * words of age, as its cause. */ export function parseAgeHeader(file: Uint8Array): AgeHeader { try { return parse(file); } catch (err) { - throw new DateKeysError('ERR_INTEGRITY', `agewrap: not a valid age file: failed to read header: ${(err as Error).message}`); + throw new DateKeysError( + 'ERR_INTEGRITY', + 'agewrap: not an age v1 header: malformed, truncated or beyond the parser limits', + new AgeError(`failed to read header: ${(err as Error).message}`), + ); } } diff --git a/src/lib/dkc/errors.ts b/src/lib/dkc/errors.ts index 2545ad2..2309131 100644 --- a/src/lib/dkc/errors.ts +++ b/src/lib/dkc/errors.ts @@ -37,16 +37,19 @@ export class DateKeysError extends Error { * @param code the normative code. * @param context the text before the code; the message is * `${context}: ${code}`, or the bare code without context. + * @param cause an internal reason that stays out of the message, such as + * the parser detail behind a fixed text (the reference never copies a + * library's error text into its errors). */ - constructor(code: ErrorCode, context?: string) { - super(context === undefined || context === '' ? code : `${context}: ${code}`); + constructor(code: ErrorCode, context?: string, cause?: unknown) { + super(context === undefined || context === '' ? code : `${context}: ${code}`, cause === undefined ? undefined : { cause }); this.name = 'DateKeysError'; this.code = code; } /** Returns a copy of this error with `prefix: ` prepended to the message. */ wrap(prefix: string): DateKeysError { - const e = new DateKeysError(this.code); + const e = new DateKeysError(this.code, undefined, this.cause); e.message = `${prefix}: ${this.message}`; return e; } diff --git a/src/lib/dkc/inspect.test.ts b/src/lib/dkc/inspect.test.ts index 083ae76..938534f 100644 --- a/src/lib/dkc/inspect.test.ts +++ b/src/lib/dkc/inspect.test.ts @@ -167,7 +167,7 @@ describe('inspect', () => { const late = run(frame({ ...parts, header: header({ dk: t(dkRound(83903165812)) }), payload: noStanza(parts.payload) })); expect(last(late)).toEqual([6, 'payload structure', false, 'ERR_INTEGRITY']); expect(late.checks.at(-1)!.detail).toBe( - 'capsule: PAYLOAD_AGE: agewrap: not a valid age file: failed to read header: parsing age header: no recipient stanzas: ERR_INTEGRITY', + 'capsule: PAYLOAD_AGE: agewrap: not an age v1 header: malformed, truncated or beyond the parser limits: ERR_INTEGRITY', ); }); @@ -184,7 +184,7 @@ describe('inspect', () => { const cr = run(frame({ ...parts, sealed: replaceText(parts.sealed, `${ch}\n`, `${ch}\r\n`) })); expect(last(cr)).toEqual([5, 'sealed control structure', false, 'ERR_INTEGRITY']); expect(cr.checks.at(-1)!.detail).toBe( - `capsule: SEALED_CONTROL: agewrap: not a valid age file: failed to read header: failed to parse header: malformed stanza: "-> tlock 2000 ${ch}\\r\\n": ERR_INTEGRITY`, + 'capsule: SEALED_CONTROL: agewrap: not an age v1 header: malformed, truncated or beyond the parser limits: ERR_INTEGRITY', ); }); @@ -224,7 +224,7 @@ describe('inspect', () => { const outer = (sealed: Uint8Array): Inspection => run(frame({ ...parts, sealed })); const payload = (p: Uint8Array): Inspection => run(frame({ ...parts, payload: p })); expect(last(outer(new Uint8Array(10)))).toEqual([5, 'sealed control structure', false, 'ERR_INTEGRITY']); - expect(outer(new Uint8Array(10)).checks.at(-1)!.detail).toMatch(/^capsule: SEALED_CONTROL: agewrap: not a valid age file: /); + expect(outer(new Uint8Array(10)).checks.at(-1)!.detail).toMatch(/^capsule: SEALED_CONTROL: agewrap: not an age v1 header: /); const twoStanzas = replaceText(parts.sealed, '\n---', '\n-> X25519 AAAA\n\n---'); const two = outer(twoStanzas); expect(last(two)).toEqual([5, 'sealed control structure', false, 'ERR_POLICY_STRUCTURE_MISMATCH']); diff --git a/src/lib/dkc/profile.ts b/src/lib/dkc/profile.ts index 6d79323..006bbca 100644 --- a/src/lib/dkc/profile.ts +++ b/src/lib/dkc/profile.ts @@ -352,11 +352,11 @@ async function validateDrand(p: Profile): Promise { if (OTHER_DRAND_SCHEMES.has(p.scheme)) { throw new DateKeysError('ERR_UNKNOWN_PROFILE', `profile ${p.id}: scheme ${goQuote(p.scheme)} is not supported by tlock`); } - throw new DateKeysError('ERR_UNKNOWN_PROFILE', `profile ${p.id}: invalid scheme name ${goQuote(p.scheme)}`); + throw new DateKeysError('ERR_UNKNOWN_PROFILE', `profile ${p.id}: ${goQuote(p.scheme)} is not a drand scheme`); } const point = checkCompressedPoint(group, p.publicKey); if (point === 'invalid') { - throw new DateKeysError('ERR_UNKNOWN_PROFILE', `profile ${p.id}: public key is not a ${p.scheme} group element`); + throw new DateKeysError('ERR_UNKNOWN_PROFILE', `profile ${p.id}: public key is not the canonical encoding of a point of the key group of ${p.scheme}`); } if (point === 'identity') throw new DateKeysError('ERR_UNKNOWN_PROFILE', `profile ${p.id}: public key is the identity element`); const hash = await chainInfoHash(p); diff --git a/src/lib/dkc/vectors.test.ts b/src/lib/dkc/vectors.test.ts index 9cb0b0c..e92be13 100644 --- a/src/lib/dkc/vectors.test.ts +++ b/src/lib/dkc/vectors.test.ts @@ -502,14 +502,16 @@ describe('vectors/mutations.json', () => { const inspected = f.filter((c) => c.step <= LAST_INSPECT_STEP); const opened = f.filter((c) => c.step > LAST_INSPECT_STEP); - it('has the cases README counts: 55, the 23 of §64 first, 31 in steps 1 to 8 (13 of them from §64), 24 after step 8', () => { - expect(f).toHaveLength(55); - expect(f.slice(0, 23).every((c) => c.spec)).toBe(true); - expect(f.slice(23).some((c) => c.spec)).toBe(false); + it('has the cases README counts: 65, the 33 of §64 first, 31 in steps 1 to 8 (13 of them from §64), 34 after step 8', () => { + // 3820066 had 55 cases (23 from §64); the v0.8.2 amendment on canonical + // point encoding (f6f2e9f) added ten §64 cases at steps 10 and 11. + expect(f).toHaveLength(65); + expect(f.slice(0, 33).every((c) => c.spec)).toBe(true); + expect(f.slice(33).some((c) => c.spec)).toBe(false); expect(inspected.length).toBe(31); expect(inspected.filter((c) => c.spec).length).toBe(13); // The cases that are skipped below, and only those. - expect(opened.length).toBe(24); + expect(opened.length).toBe(34); expect(inspected.length + opened.length).toBe(f.length); expect(new Set(f.map((c) => c.name)).size, 'unique names').toBe(f.length); }); diff --git a/src/lib/inspector/load.test.ts b/src/lib/inspector/load.test.ts index 4d2b3e6..749b202 100644 --- a/src/lib/inspector/load.test.ts +++ b/src/lib/inspector/load.test.ts @@ -133,6 +133,9 @@ describe('readCapsule', () => { const endless = concat(intro, new Uint8Array(3 << 20).fill(0x41)); const dkc = frame({ ...parts, payload: endless }); await samePrefixVerdict(dkc); - expect(inspectWith(dkc, registry).checks.at(-1)!.detail).toMatch(/header exceeds 2 MiB/); + // The reference reports every malformed age header with one fixed text. + expect(inspectWith(dkc, registry).checks.at(-1)!.detail).toBe( + 'capsule: PAYLOAD_AGE: agewrap: not an age v1 header: malformed, truncated or beyond the parser limits: ERR_INTEGRITY', + ); }); }); diff --git a/testdata/README.md b/testdata/README.md index 294a496..99e4efc 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -32,7 +32,7 @@ Conventions for every file: | `vectors/quicknet_rounds.json` | date → round resolution | §15, §16, §65 | | `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 | §58, CDDL | -| `vectors/mutations.json` | the mutation corpus: 23 mutations of §64 and further cases | §63, §64 | +| `vectors/mutations.json` | the mutation corpus: 33 mutations of §64 and further cases | §63, §64 | | `vectors/inspect_differential.json` | 1825 mutations of the fixtures with the verdict of steps 1 to 8 | §63 | | `fixtures/.dkc`, `.json` | official capsules and every intermediate value | §67 | | `fixtures/.dkk`, `.dkk.json` | official access keys | §68 | @@ -173,7 +173,7 @@ reading flow (`capsule.Open`, §63) must fail. ``` - `name`: unique, stable. -- `spec`: true for the 23 mutations listed in spec §64 (the first 23 cases), +- `spec`: true for the 33 mutations listed in spec §64 (the first 33 cases), false for the further cases of the reference. - `dkc`: the capsule, as edits of a fixture (see above). The reader gets it as a seekable file, so that the `capsule_digest` of an offered `.dkk` is checked @@ -184,8 +184,9 @@ reading flow (`capsule.Open`, §63) must fail. credentials; absent when none. - `release`: what the release source answers to every request, whatever round is asked for. The reader must verify it (§51): a case may serve a release of - another round, or a round with the signature of another. `null` means that - no release is available (`ERR_RELEASE_UNAVAILABLE`). + another round, a round with the signature of another, or a signature that is + not the canonical encoding of a point (§12.2). `null` means that no release + is available (`ERR_RELEASE_UNAVAILABLE`). - `now`: the reader's clock, RFC 3339. No release is requested before the round time of the DateKey. - `registry`: `default` pins exactly the Quicknet profile of @@ -224,6 +225,27 @@ file (steps 11, 13 and 17), are those of spec §63 as well. In this corpus: - every `.dkk` offered decodes: the corpus checks step 9.a, not the decoding of a `.dkk`, whose errors spec §63 also places at step 9.a. +### Point encodings: steps 10 and 11 + +Ten cases of §64 test the canonical point encoding of spec §12.2 and the tlock +stanza body `U || V || W` of spec §63 step 11: + +- five edit the tlock stanza body of `time_only.dkc`: U with c0 + p, U the + point at infinity (the byte 0xc0, then zeros), U with the infinity flag + over its own coordinate bits, and bodies of 127 and 129 bytes. Each recomputes the header + MAC of OUTER_TIME_AGE with FK_TIME, so the age header stays authentic and + only the rules of step 11 reject the capsule (`ERR_INTEGRITY`); a reader + whose decoder reduces coordinates modulo p opens the first one; +- three serve the release of `time_only.dkc` with its signature edited: the + point at infinity, the infinity flag over its own coordinate bits, and the + negated signature (the sort flag flipped: a canonical point that does not + verify); a fourth serves the published signature of round 1004 re-encoded + as x + p, over a frozen capsule for round 1004, because no fixture round + has a signature whose x + p fits in 381 bits. All four fail at step 10 + (`ERR_RELEASE_INVALID`); +- the last one serves the negated signature with U with c0 + p: step 10 + comes first. + ## The checks of steps 1 to 8 `mutations.json` and `inspect_differential.json` follow the rules of the diff --git a/testdata/SOURCE.json b/testdata/SOURCE.json index 0f54efc..7e98b19 100644 --- a/testdata/SOURCE.json +++ b/testdata/SOURCE.json @@ -1,8 +1,8 @@ { "module": "g.activething.com/go/DateKeys", - "commit": "692cf87db1ea88a88624b59e2a95400fbc236e08", + "commit": "f6f2e9f55f01d8773b2fe7872b15541012704004", "files": { - "README.md": "f8f41632ee3e7cd3f7a3d3fa4459bc7bdbd201d905109ba8a9f9c6a5defcda78", + "README.md": "720070e7d4f42b426f66f58f41a0c9e640b11701cf175a0e51f0e555ee4d889f", "fixtures/empty_payload.dkc": "871e9bf05b52bbae17f3adfbbf97b46e7f0e53aa8f57bcaa506e43f36f53a9d4", "fixtures/empty_payload.inspect.json": "dd2b21faefdfe3aa59b5eeef9130a6896070ae30a2b16933f3aefd146828207e", "fixtures/empty_payload.json": "490a6e7f7acc882bcb956b297ad8c817d7b21da29e8d4c4a2cd31c852707eee0", @@ -32,7 +32,7 @@ "vectors/cbor.json": "444fe6104476fbcda91e1873c12752186dbf7e55ad0769e1eb0678ed9673c9d6", "vectors/dk1.json": "3a345330fed244662b0a335f0a6d5581c200ceeb09bfb74c4d1c342daea72864", "vectors/inspect_differential.json": "fe207b67d307e3ae7a78295b7a882d98669adbf498b725443ab4757eb9da03f7", - "vectors/mutations.json": "0ab1f3dd30f4aef86c39a69b314a2843d0819193153a97fd3b9959199d0dbd65", + "vectors/mutations.json": "c4599d1ecc4ea5dcc64e3d963db0714b28661db8daf8277346dc8238218e7c33", "vectors/profile_quicknet.json": "4f9de60475d0807aa580f59cf3e6df38cba2a55a62f590aefa2a55753ddb0055", "vectors/quicknet_rounds.json": "22c764aac454ce320daf7e65b9494ff2d207cac974c68d07e84f4ec18c3ed920" } diff --git a/testdata/vectors/mutations.json b/testdata/vectors/mutations.json index ff046e2..0b7fd84 100644 --- a/testdata/vectors/mutations.json +++ b/testdata/vectors/mutations.json @@ -458,6 +458,199 @@ "error": "ERR_NON_CANONICAL_CBOR", "step": 4 }, + { + "name": "tlock stanza U re-encoded with c0 + p", + "spec": true, + "dkc": { + "base": "time_only.dkc", + "edits": [ + [303, 156, "4b3974334342577331793673785069396157564b384579504b612b796b6762676b4a46782f5a763371667465747a654b4f7a3852534856786467582f6e37506e0a62435948626e705136755561775854674a5342647748507232587a4e56733234454758492b3676375543770a2d2d2d20675a464c773845307137726b6e384b675355414e76345a316679413258534e446c584653584b617456716f"] + ] + }, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "now": "2023-08-23T15:59:24Z", + "registry": "default", + "network": true, + "frozen": false, + "error": "ERR_INTEGRITY", + "step": 11 + }, + { + "name": "tlock stanza U is the point at infinity", + "spec": true, + "dkc": { + "base": "time_only.dkc", + "edits": [ + [238, 221, "774141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141410a414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141410a62435948626e705136755561775854674a5342647748507232587a4e56733234454758492b3676375543770a2d2d2d20472f6d426f3779325364374b6f4a567364384734547a357557346f6a77384e6467453772384b717a514534"] + ] + }, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "now": "2023-08-23T15:59:24Z", + "registry": "default", + "network": true, + "frozen": false, + "error": "ERR_INTEGRITY", + "step": 11 + }, + { + "name": "tlock stanza U with the infinity flag and a payload", + "spec": true, + "dkc": { + "base": "time_only.dkc", + "edits": [ + [238, 220, "77492b4f36746b50507576754c6d3456314a4369713563794644554e463532634b662f49424f59532b50335557385053725068564c437337746d5a7634776c490a4564706c48647773384a5268715645484a686d65474f67583369712f445051684b574366584b564773396441437a654c69657352534c74796467582f6f416b380a62435948626e705136755561775854674a5342647748507232587a4e56733234454758492b3676375543770a2d2d2d20336a65787a7a2f586e7359324459467751754c706d6b6f2b6d465373366b786c6c775273487a6e51346e"] + ] + }, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "now": "2023-08-23T15:59:24Z", + "registry": "default", + "network": true, + "frozen": false, + "error": "ERR_INTEGRITY", + "step": 11 + }, + { + "name": "tlock stanza body of 127 bytes", + "spec": true, + "dkc": { + "base": "time_only.dkc", + "edits": [ + [15, 444, "bda5006a646174656b657963617001010250ad4d676812b134ff8a3de263f77018b4037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d5441774d483004006167652d656e6372797074696f6e2e6f72672f76310a2d3e20746c6f636b203130303020353264623962613730653063633066366561663738303364643037343437613166353437373733356664336636363137393262613934363030633834653937310a67492b4f36746b50507576754c6d3456314a4369713563794644554e463532634b662f49424f59532b50335557385053725068564c437337746d5a7634776c490a4564706c48647773384a5268715645484a686d65474f67583369712f445051684b574366584b564773396441437a654c69657352534c74796467582f6f416b380a62435948626e705136755561775854674a5342647748507232587a4e56733234454758492b36763755410a2d2d2d206d6c44362b7a79424a6b524852363657776857654f5362584a5030394c6373734f57796c4962412f6c5649"] + ] + }, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "now": "2023-08-23T15:59:24Z", + "registry": "default", + "network": true, + "frozen": false, + "error": "ERR_INTEGRITY", + "step": 11 + }, + { + "name": "tlock stanza body of 129 bytes", + "spec": true, + "dkc": { + "base": "time_only.dkc", + "edits": [ + [15, 444, "bfa5006a646174656b657963617001010250ad4d676812b134ff8a3de263f77018b4037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d5441774d483004006167652d656e6372797074696f6e2e6f72672f76310a2d3e20746c6f636b203130303020353264623962613730653063633066366561663738303364643037343437613166353437373733356664336636363137393262613934363030633834653937310a67492b4f36746b50507576754c6d3456314a4369713563794644554e463532634b662f49424f59532b50335557385053725068564c437337746d5a7634776c490a4564706c48647773384a5268715645484a686d65474f67583369712f445051684b574366584b564773396441437a654c69657352534c74796467582f6f416b380a62435948626e705136755561775854674a5342647748507232587a4e56733234454758492b367637554377410a2d2d2d20325a524d54626a4b62363170795949635756664f79336d75474f584f777056366b4d4b5a4b553434484a30"] + ] + }, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "now": "2023-08-23T15:59:24Z", + "registry": "default", + "network": true, + "frozen": false, + "error": "ERR_INTEGRITY", + "step": 11 + }, + { + "name": "release signature re-encoded with x + p", + "spec": true, + "dkc": { + "edits": [ + [0, 0, "444b43310100000000000079000001bea5006a646174656b657963617001010250a7336e7ec2e3ae0b86694ddd7be97da9037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d5441774e483004006167652d656e6372797074696f6e2e6f72672f76310a2d3e20746c6f636b203130303420353264623962613730653063633066366561663738303364643037343437613166353437373733356664336636363137393262613934363030633834653937310a7439545361546770564a6e392b4861653971615944366a5a746e2f493243716d4b542b77756732614a51394c612f4855474d515a337475412f713846475571510a41712b48753433514e6a6f5062703647727678526461354448532f6a50714667504736485a6a3259387a4771523274714c2f692b744a5a46464e44684e375a620a6d5a796e7a55717a6d7659346753324c48507342686c504c792b57766f6d694f5643784d6b35334c612f670a2d2d2d205173416550494e6e34734247574f525258596a6579614d52364a4e644f734259646d6351416e65516e6b410a15bc97bcfc7d847e72586753b8c90e8d39a7d42905a881f8e6152c51d7dc51e41c1d5c81df60fa04c03ba568ef6ec0962867c157c2422f6e04327433ba2c741b8a9217a58d27376a1e7280c1ec7fe6eebadb72ee0882b55d32e167fd72491ac77407b4aaff6c5b972bc61a07401988371fbb0b2b6ebf1307cb8b576167652d656e6372797074696f6e2e6f72672f76310a2d3e2058323535313920573365486f4233743353593277524f46506c6d326e47594e33627363714c714a7a476d35754575787748300a665333744877494255434d36497241336b4a4179353663483836756d7a5436477a63497268526b466a51670a2d2d2d2074694d326b676a652f384c2b494b69736930397076525555304450593371564758493437356831413969380ad2636c0ca74ce080748ad38ba4abe73f7527099ed68830e9fa83512290bcb69401acb6a51ef8017216a42c0fe6da88a310"] + ] + }, + "release": { + "round": 1004, + "signature": "be076aa25a40df5b4d22f9f7bcedaff30dcac20d94556107b8e652c7b54b2a440ce796f9fa53ad28023c84b40a580f55" + }, + "now": "2024-08-23T15:09:27Z", + "registry": "default", + "network": true, + "frozen": true, + "error": "ERR_RELEASE_INVALID", + "step": 10 + }, + { + "name": "release signature is the point at infinity", + "spec": true, + "dkc": { + "base": "time_only.dkc", + "edits": [] + }, + "release": { + "round": 1000, + "signature": "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + "now": "2023-08-23T15:59:24Z", + "registry": "default", + "network": true, + "frozen": false, + "error": "ERR_RELEASE_INVALID", + "step": 10 + }, + { + "name": "release signature with the infinity flag and a payload", + "spec": true, + "dkc": { + "base": "time_only.dkc", + "edits": [] + }, + "release": { + "round": 1000, + "signature": "d44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "now": "2023-08-23T15:59:24Z", + "registry": "default", + "network": true, + "frozen": false, + "error": "ERR_RELEASE_INVALID", + "step": 10 + }, + { + "name": "release signature negated", + "spec": true, + "dkc": { + "base": "time_only.dkc", + "edits": [] + }, + "release": { + "round": 1000, + "signature": "944679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "now": "2023-08-23T15:59:24Z", + "registry": "default", + "network": true, + "frozen": false, + "error": "ERR_RELEASE_INVALID", + "step": 10 + }, + { + "name": "negated release signature and U re-encoded with c0 + p", + "spec": true, + "dkc": { + "base": "time_only.dkc", + "edits": [ + [303, 156, "4b3974334342577331793673785069396157564b384579504b612b796b6762676b4a46782f5a763371667465747a654b4f7a3852534856786467582f6e37506e0a62435948626e705136755561775854674a5342647748507232587a4e56733234454758492b3676375543770a2d2d2d20675a464c773845307137726b6e384b675355414e76345a316679413258534e446c584653584b617456716f"] + ] + }, + "release": { + "round": 1000, + "signature": "944679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "now": "2023-08-23T15:59:24Z", + "registry": "default", + "network": true, + "frozen": false, + "error": "ERR_RELEASE_INVALID", + "step": 10 + }, { "name": "magic", "spec": false,