TypeScript aligned with the Go reference at 692cf87: layered error
precedence, the refined spec rules and the §19 UTF-8 rule; every shared
Go vector file runs in the suite (2,375 tests, 24 phase-2 cases skipped
and counted). Two differentials against Go, 483,527 and 407,208 inputs
from independent generators, found no disagreement on verdict, code or
step; an independent review approved the merge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ -18,38 +18,38 @@ 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 (`afb44a3`) |
| Fichero | Contenido | Equivale en Go (`692cf87`) |
|---|---|---|
| `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` |
| `cbor.ts` | Perfil de §58: `Encoder` con error persistente, `Decoder` estricto, `unmarshal` (decodifica, reencodifica y compara; `onReject` para borrar secretos), `peek`/`checkSchema` (tipo y versión antes del resto), `wideUint` (los dos campos que Go lee como `uint64`) y `walk` (lector genérico acotado) | `codec` |
| `extension.ts` | Mapas de extensión, reglas del array (1 a 64, orden por bytes UTF-8 de `extension_id`, sin repetidos ni solapes), registros, críticas y no críticas | `extension` |
| `profile.ts` | Provider Profile: CBOR exacto, `profile_hash`, validación completa, registro pinneado; Quicknet fijado por su CBOR y su hash | `profile` |
| `cbor.ts` | El codec propio de la referencia, con las mismas lecturas, las mismas comprobaciones en el mismo orden y los mismos textos de error: `Encoder` con error persistente; `Decoder`, cursor estricto (`map`/`key`/`endMap`, `array`, `uint`/`uint64`, `bstr`, `text`, `done`); `unmarshal` (decodifica, reencodifica y compara; `onReject` para borrar secretos); `peek`/`checkSchema` (capa 2 de §69.1: tipo y versión antes que nada) y `walk` (lector genérico acotado en profundidad y longitud) | `codec` |
| `schema.ts` | Lo que comparten los decodificadores de PUBLIC_HEADER, CONTROL_CBOR y el cuerpo de la `.dkk`: `key N: ` en los errores, claves obligatorias y arrays de extensiones | `capsule/framing.go`, `accesskey` |
| `extension.ts` | Arrays de extensiones, leídos con su objeto (capa 3 de §69.1): de 1 a 64, en orden estrictamente ascendente de los bytes UTF-8 de `extension_id` (nunca por unidades UTF-16), `extension_version` hasta 2³² − 1, `data` ausente o `bstr` no vacío, ningún id en los dos arrays; registros, críticas y no críticas (capa 4) | `extension` |
| `profile.ts` | Provider Profile: CBOR exacto, `profile_hash`, reglas 1 a 4 de §12.1 en su orden (el límite de `period` de §74 en la capa del esquema; alfabetos, clave pública del grupo del scheme y fórmula de `chain_hash`), registro pinneado; Quicknet fijado por su CBOR y su hash | `profile` |
| `bls12381.ts` | Pertenencia de claves públicas BLS12-381 comprimidas (G1 y G2) al subgrupo, como `FromCompressed` de kilic | `kyber-bls12381` |
| `datekey.ts` | `dk1_` canónico, ronda desde una fecha con precisión de nanosegundos, parser RFC 3339 equivalente a `time.Parse(time.RFC3339Nano, …)` | `datekey` |
| `header.ts`, `control.ts`, `accesskey.ts` | PUBLIC_HEADER, CONTROL_CBOR y `.dkk` (cuerpo y trama), decodificar y codificar | `capsule`, `accesskey` |
| `framing.ts` | Prelude DKC1 (16 bytes) y DKK1 (12 bytes), longitudes, límites de §57 y troceo de secciones | `capsule/framing.go` |
| `age.ts` | Parser estricto de la cabecera `age` v1 sobre los ficheros binarios, con los textos de error de `age`; reglas de stanzas; `MAX_AGE_HEADER_LEN` (2 MiB), el límite que usa la página para leer solo el prefijo de un `.dkc` grande | `agewrap`, `filippo.io/age/internal/format` |
| `inspect.ts` | Pasos 1 a 8 de §63 y la vista JSON de `datekeys inspect -json`| `capsule/inspect.go`, `cmd/datekeys` |
| `datekey.ts` | `dk1_` canónico con las reglas de lectura de §19 (CR, LF y todo carácter fuera del alfabeto fallan el paso 1; números JSON por su valor decimal exacto), ronda desde una fecha con precisión de nanosegundos y cota de 9999-12-31T23:59:59Z (§15), parser RFC 3339 equivalente a `time.Parse(time.RFC3339Nano, …)` | `datekey` |
| `header.ts`, `control.ts`, `accesskey.ts` | PUBLIC_HEADER, CONTROL_CBOR y `.dkk` (cuerpo y trama), decodificar y codificar, con las capas de §69.1 | `capsule`, `accesskey` |
| `framing.ts` | Prelude DKC1 (16 bytes) y DKK1 (12 bytes) en el orden de §23 y §40, longitudes de 1 byte hasta los límites de §57, y troceo de secciones | `capsule/framing.go` |
| `age.ts` | Parser estricto de la cabecera `age` v1 (§28.1) sobre los ficheros binarios, con los textos de error de `age`; reglas de stanzas; `MAX_AGE_HEADER_LEN` (2 MiB), el límite que usa la página para leer solo el prefijo de un `.dkc` grande | `agewrap`, `filippo.io/age/internal/format` |
| `inspect.ts` | Pasos 1 a 8 de §63 y la vista JSON de `datekeys inspect -json`(`inspectView`, `inspectJSON`) | `capsule/inspect.go`, `internal/inspectview` |
| `index.ts` | Reexporta todo | |
| `testing/` | Solo para tests: lectura de `testdata/`, constructores de CBOR en hex, cirugía de cápsulas | |
| `testing/` | Solo para tests: lectura de `testdata/` y de sus formatos (`vectors.ts`: ediciones, vectores), constructores de CBOR en hex, cirugía de cápsulas | |
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 `afb44a3` mediante un oráculo diferencial fuera del repositorio: unos 310 000 casos (cápsulas mutadas byte a byte y estructuralmente, cabeceras, controles, `.dkk`, perfiles, cabeceras `age`, `dk1_`, fechas y cabeceras de schema). Coinciden el veredicto, el código, el paso y los campos, los textos de todos los pasos superados y los textos de fallo de framing, `age`, DateKey, perfiles y extensiones. La vista de `inspect` de cada fixture es idéntica byte a byte a la salida de `datekeys inspect -json`.
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.
Enteros: el decodificador rechaza al leerlo todo entero por encima de 2⁵³ − 1 (plan §5), salvo en `access_policy` de PUBLIC_HEADER y en `extension_version`. Go lee esos dos campos como `uint64` y comprueba su rango después de la DateKey y de los demás campos, con otro código; `Decoder.wideUint` los lee igual (un valor por encima de 2⁵³ − 1 queda como `Infinity` con sus 8 bytes, para reencodificarlo y escribirlo en decimal), y el orden, el código y el texto coinciden con Go.
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.
Diferencias conocidas:
Enteros: el decodificador lee cada entero hasta 2⁶⁴ − 1 (un `bigint` por encima de 2⁵³ − 1) y lo compara con el máximo de su campo, con el texto de la referencia (`unsigned integer 9007199254740992 above 9007199254740991`). Ningún campo de un objeto del protocolo admite más de 2⁵³ − 1; `walk` admite cualquier `uint64`, como `codec.Walk`.
- **Enteros por encima de 2⁵³ − 1 en los demás sitios.** En `walk` y en los campos de perfil, control y `.dkk`, TypeScript los rechaza al decodificar y Go los acepta como `uint64` y los rechaza después. El código es siempre `ERR_NON_CANONICAL_CBOR` en los dos y el paso el mismo; cambia el texto. Un caso `accept` de `cbor.json` con un entero así (el genérico de Go) se espera rechazado aquí con `ERR_NON_CANONICAL_CBOR`.
- **Textos de errores de decodificación CBOR.** Los de Go vienen de `fxamacker/cbor` (`codec: decode: …`); los de TypeScript describen el mismo fallo con otras palabras. El código y el paso coinciden.
`peek` lee la capa 2 de §69.1 como `codec.Peek`: una cabecera de mapa de longitud definida que anuncia al menos dos entradas y no más de la mitad de los bytes que la siguen, la clave 0 con un texto de hasta 64 bytes y la clave 1 con un entero de hasta 2⁵³ − 1, todo en su forma más corta. No lee nada más: una versión 2 es `ERR_UNSUPPORTED_VERSION` sea lo que sea lo que venga detrás, y cualquier otra cosa en esas posiciones, `ERR_NON_CANONICAL_CBOR`.
`goQuote` escribe como Go 1.26 (Unicode 15.0.0) los textos entre comillas de los detalles de fallo: usa una tabla de `strconv.IsPrint` generada con Go y no `\p{…}`, porque cada motor JavaScript trae su propia versión de Unicode (V8 en Node 24 ya imprime runas de Unicode 16 que Go escapa, como U+31E4). `bytes.test.ts` fija el SHA-256 del conjunto completo de runas imprimibles. Si el toolchain de la referencia cambia de versión de Unicode, se regenera con `go run scripts/go-isprint-table.go` y se actualizan la tabla y el hash.
UTF-8 inválido en el JSON de un `dk1_` hace fallar el paso 2 de §19 (`ERR_DATEKEY_INVALID`) en las dos implementaciones, también en un miembro que un nombre repetido reemplaza después. Hasta `692cf87` la referencia Go lo sustituía por U+FFFD (`encoding/json`) y en ese caso daba `ERR_DATEKEY_NON_CANONICAL`; el diferencial de esta implementación lo encontró y la referencia se corrigió para seguir al spec. `datekey.test.ts` y el vector *invalid UTF-8 in a member a repeated name overwrites* de `dk1.json` lo fijan.
El lector de schema (`peek`) reproduce a propósito lo que acepta `codec.Peek` de `afb44a3`, incluidas sus rarezas (una versión `null` cuenta como 0, un valor simple `e2` como 2), para que el código `ERR_UNSUPPORTED_VERSION` frente a `ERR_NON_CANONICAL_CBOR` sea el mismo. Si el codec propio de Go (paso 2b) cambia ese comportamiento, habrá que seguirlo.
`goQuote` escribe como Go 1.26 (Unicode 15.0.0) los textos entre comillas de los detalles de fallo: usa una tabla de `strconv.IsPrint` generada con Go y no `\p{…}`, porque cada motor JavaScript trae su propia versión de Unicode (V8 en Node 24 ya imprime runas de Unicode 16 que Go escapa, como U+31E4). `bytes.test.ts` fija el SHA-256 del conjunto completo de runas imprimibles. Si el toolchain de la referencia cambia de versión de Unicode, se regenera con `go run scripts/go-isprint-table.go` y se actualizan la tabla y el hash.
Secretos: `access_material` de un `.dkk` e `I_PAYLOAD` de CONTROL_CBOR se borran en todos los caminos, también cuando la decodificación falla a medias o `unmarshal` rechaza el valor, como el `clear` diferido de Go.
@ -66,7 +66,7 @@ Sitio SvelteKit estático (`@sveltejs/adapter-static`, `strict`): las dos págin
- cada paso con su número, su nombre de la CLI, `superado` o el código normativo, y el detalle (los caracteres invisibles o de control se escriben como `\uXXXX`);
- el veredicto, `capsule_id`, la DateKey compacta y decodificada (red y ronda), el perfil fijado, la fecha de apertura en UTC y en la hora local del navegador, `access_policy`, los campos del prelude, los argumentos del stanza `tlock` frente al perfil fijado y el número y tipo de stanzas de OUTER_TIME_AGE y PAYLOAD_AGE;
- las extensiones de PUBLIC_HEADER según el contrato del plan §8: id (entre comillas y escapado si tiene caracteres no imprimibles), versión, crítica o no, conocida o no, longitud y hex (plegado si pasa de 64 bytes); texto si los bytes son UTF-8 imprimible; vista CBOR con `walk` si son un ítem del perfil de §58, marcada "informativo, no validado por el protocolo"; y el aviso de que son públicas y no están autenticadas hasta el paso 15;
- las extensiones de PUBLIC_HEADER según el contrato del plan §8: id (entre comillas y escapado si tiene caracteres no imprimibles), versión, crítica o no, conocida o no, longitud y hex (plegado si pasa de 64 bytes); texto si los bytes son UTF-8 imprimible; vista CBOR con `walk` si son un ítem del perfil de §58, marcada "informativo, no validado por el protocolo"; y el aviso de que son públicas, de que nada las vincula al resto de la cápsula antes del paso 15 y de que ni entonces prueban autoría (§55.1);
- **Copiar JSON**, que copia exactamente la salida de `datekeys inspect -json` (`cliJSON`: el `json.Encoder` de Go con sangría de dos espacios, `<`, `>`, `&`, U+2028 y U+2029 escapados y salto de línea final). `file` es el nombre del fichero.
No pide ni acepta secretos. Todo el texto leído de la cápsula pasa por interpolación de texto de Svelte (nunca `{@html}`), y ni las entradas de mapas CBOR ni los identificadores de extensión se usan nunca como claves de objetos o `Map` de JavaScript.
@ -133,13 +133,12 @@ Umbrales de cobertura (`vitest.config.ts`): `cbor.ts` al 100 % en líneas, ramas
### Vectores y fixtures
- `testdata/fixtures/*.json`: cada `.dkc` pasa los pasos 1 a 8 con los valores registrados (prelude, PUBLIC_HEADER, DateKey, `capsule_id`, política, `unlock_at`, extensiones con sus bytes exactos, stanzas, `header_binding`, CONTROL_CBOR); cada `.dkk` se decodifica campo a campo y se reencodifica byte a byte. Los fixtures nuevos se recogen solos.
- `testdata/vectors/dk1.json`, `quicknet_rounds.json`, `profile_quicknet.json`: se ejecutan todos.
- Ficheros que Go producirá (plan §6 y §7), ya previstos en `vectors.test.ts`; se saltan mientras no existan y, cuando existen, cada caso se revisa antes de ejecutarlo: falla el fichero cuya estructura no se reconoce y el caso al que le falta un campo, nunca se salta en silencio.
- `vectors/cbor.json`: `accept`/`reject` con `walk` y bloques por schema (`public_header`, `control`, `access_key`, `profile`, `extension`…), cada caso con bytes y `error` o `value`. Un bloque sin decodificador, vacío o con un caso sin bytes falla.
- `vectors/mutations.json`: casos con bytes, código normativo (`error`, `code` o `want`) y `step` numérico; se ejecutan los de los pasos 1 a 8 (al menos 23, plan §7.4) y los demás se muestran como saltados.
- `vectors/inspect_differential.json`: casos con bytes y el veredicto de Go (`valid`, `error` o `checks`, en el caso o bajo `view`, `inspect`, `result`, `go`, `verdict`, `expected` o `want`); un caso sin veredicto falla.
- `**/*.inspect.json`: salida de `datekeys inspect -json` junto a su `.dkc`; se compara la vista entera salvo `file`, y el texto de los pasos superados.
- Bytes de un caso: `hex` es hex estricto; `base64`/`b64`, base64 canónico; `file`/`path`, un fichero bajo `testdata/`; `dkc`, `bytes`, `input` y `data`, hex estricto, si no un fichero bajo `testdata/`, si no base64 canónico. Cualquier otro texto hace fallar el caso.
- `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/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.
## Tooling de desarrollo
@ -176,4 +175,4 @@ 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 `afb44a3`, rama `v0.8.2`).
Copia actual: la de `testdata/SOURCE.json` (commit `692cf87`, rama `v0.8.2`).
['access_type as a byte string',body({type:b('783235353139')}),NC,/^accesskey: key 4: codec: offset 61: a byte string where a text string was expected: ERR/],
['empty verification_metadata',body({ver: map()}),NC,/^accesskey: key 6: empty verification_metadata; an absent one omits key 6: ERR/],
['empty capsule_digest',body({ver: map([0,b('')])}),NC,/^accesskey: key 6: capsule_digest: codec: offset 107: a byte string of 0 bytes outside 32\.\.32: ERR/],
['capsule_digest of 31 bytes',body({ver: map([0,bn(31)])}),NC,/capsule_digest: codec: offset 108: a byte string of 31 bytes outside 32\.\.32/],
['verification_metadata key 1',body({ver: map([1,bn(32)])}),NC,/^accesskey: key 6: verification_metadata key 1 is not defined: ERR/],
['verification_metadata of two keys',body({ver: map([0,bn(32)],[1,u(0)])}),NC,/key 6: codec: offset 105: map of 2 entries, at most 1/],
['null verification_metadata',body({ver:'f6'}),NC,/key 6: codec: offset 104: a float or simple value \(initial byte 0xf6\) is outside the CBOR profile/],
['out of order, access_type y',body({type:t('y'),non: arr(ext('b'),ext('a'))}),NC,/^accesskey: key 8: extension a: array is not in canonical order: ERR/],
expectCode(()=>parseDateKey(s),INVALID,/^datekey: payload is not Base64URL: ERR_DATEKEY_INVALID$/);
}
});
it('reads JSON numbers by their exact decimal value, never as doubles (spec §19)',()=>{
// 1.0000000000000001 is 1 as a double, and not 1: step 3 fails.
expectCode(()=>parseDateKey(dk1('{"version":1.0000000000000001,"network":"datekeys:quicknet:v1","round":1000}')),INVALID,/unsupported version 1\.0000000000000001/);
['out of order',arr(entry('org.b',1),entry('org.a',1)),/^extension org.a: array is not in canonical order: ERR/],
['repeated id',arr(entry('org.a',1),entry('org.a',2)),/^extension org.a: appears more than once: ERR/],
['present but empty',arr(map([0,t('org.a')],[1,u(1)],[2,'40'])),/^extension "org.a": data is present but empty; an extension without data omits key 2: ERR/],
it('checks the CDDL with ERR_NON_CANONICAL_CBOR (layer 3)',()=>{
constcases:[string,Uint8Array,RegExp][]=[
['unknown key 7',header({extra:[[7,u(0)]]}),/^capsule: PUBLIC_HEADER: key 7 is not defined: ERR/],
['missing key 4',h(map([0,t('datekeycap')],[1,u(1)],[2,bn(16)],[3,t(dkRound(1))])),/^capsule: PUBLIC_HEADER: key 4 is missing: ERR/],
['capsule_id of 15 bytes',header({cid: bn(15)}),/^capsule: PUBLIC_HEADER: key 2: codec: offset 17: a byte string of 15 bytes outside 16\.\.16: ERR/],
['DateKey as a byte string',header({dk: b(hx(newTextEncoder().encode(dkRound(1000))))}),/key 3: codec: offset 34: a byte string where a text string was expected/],
['access_policy 2',header({policy: u(2)}),/^capsule: PUBLIC_HEADER: key 4: access_policy 2 is not defined in V1: ERR/],
['access_policy 2^32',header({policy: u(2**32)}),/key 4: access_policy 4294967296 is not defined in V1/],
['access_policy 2^53-1',header({policy: u(2n**53n-1n)}),/key 4: access_policy 9007199254740991 is not defined in V1/],
// With a valid DateKey, the range checks fail with the reference's texts.
expectCode(()=>decodeHeader(header({policy: u(2n**53n)})),NC,/^capsule: access_policy 9007199254740992 is not defined in V1: ERR_NON_CANONICAL_CBOR$/);
expectCode(()=>decodeHeader(header({policy: u(2n**53n-1n)})),NC,/access_policy 9007199254740991 is not defined/);
expectCode(()=>decodeHeader(header({policy: u(2n**64n-1n)})),NC,/access_policy 18446744073709551615 is not defined/);
it('checks the DateKey only after the CDDL (spec §69.1 layer 4)',()=>{
constbad=t('dk1_!!');
expectCode(()=>decodeHeader(header({dk: bad})),'ERR_DATEKEY_INVALID',/^capsule: PUBLIC_HEADER: datekey: payload is not Base64URL: ERR_DATEKEY_INVALID$/);
expectCode(()=>decodeHeader(header({dk: t(dkRound('1.0e3'))})),'ERR_DATEKEY_NON_CANONICAL',/^capsule: PUBLIC_HEADER: datekey: not the canonical encoding dk1_/);
// A header that also breaks the CDDL reports ERR_NON_CANONICAL_CBOR,
// whatever its DateKey. The reference at afb44a3 read access_policy and
// extension_version as any uint64 and bounded them after the DateKey
// (ERR_DATEKEY_INVALID for the first two reproducers below); the codec of
// 3820066 reports the CDDL layer first, as spec §69.1 requires.
expect(at4(header({policy: u(2n**53n)})).checks.at(-1)!.detail).toBe('capsule: access_policy 9007199254740992 is not defined in V1: ERR_NON_CANONICAL_CBOR');
// The CDDL, integer bounds included, before the DateKey (spec §69.1).
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`,
);
});
it('compares the tlock arguments as exact strings, in the order of step 8',()=>{
it('checks the schema head, then the CDDL with the period limit, then the fields (spec §12.1 rules 1 and 2)',async()=>{
constcases:[string,Uint8Array,string,RegExp][]=[
['schema version 2',profile({1: u(2)}),'ERR_UNSUPPORTED_VERSION',/^profile: codec: datekeys-provider-profile schema version 2, want 1: ERR_UNSUPPORTED_VERSION$/],
['type tag of PUBLIC_HEADER',profile({0: t('datekeycap')}),NC,/^profile: codec: type "datekeycap", want "datekeys-provider-profile": ERR/],
['period not shortest',profile({7:'1803'}),NC,/^profile: key 7: codec: offset 205: 3 is not in its shortest form \(initial byte 0x18\): ERR/],
['ten keys',h(QUICKNET_CANONICAL_CBOR.replace(/^ab/,'aa').slice(0,-70)),NC,/^profile: 10 keys, want all 11: ERR/],
['chain_hash of 31 bytes',profile({5: bn(31)}),NC,/^profile: key 5: codec: offset 73: a byte string of 31 bytes outside 32\.\.32: ERR/],
['genesis_seed of 33 bytes and an invalid profile_id',profile({10: bn(33),2: t('X')}),NC,/^profile: key 10: codec: offset 223: a byte string of 33 bytes/],
['period 0',profile({7: u(0)}),NC,/^profile: key 7: period 0: ERR/],
['period 0 and an invalid profile_id',profile({7: u(0),2: t('X')}),NC,/key 7: period 0/],
['period 86401, above the implementation limit',profile({7: u(86401)}),NC,/^profile: period 86401 s out of range: ERR/],
['period 86401 and an invalid profile_id',profile({7: u(86401),2: t('X')}),NC,/period 86401 s out of range/],
// Pinning goes through the encoding: its errors are those of canonicalCBOR and decodeProfile.
awaitexpectCodeAsync(()=>newRegistry({profile:{...QN,period: 0},hash: newUint8Array(32)}),NC,/^profile: period 0s is not a positive whole number of seconds: ERR/);
awaitexpectCodeAsync(()=>newRegistry({profile:{...QN,period: 90000},hash: newUint8Array(32)}),NC,/^profile: period 90000 s out of range: ERR/);
@ -37,19 +37,19 @@ export function stepGloss(step: number): string {
case1:
return'El fichero empieza por la marca DKC1 y trae un prelude completo.';
case2:
return'Versión de trama 1, FLAGS y RESERVED a cero y longitudes dentro de los límites de §57.';
return'Versión de trama 1, FLAGS y RESERVED a cero, y cada longitud entre 1 byte y su límite de §57 (1 MiB y 64 MiB).';
case3:
return'Se leen los bytes exactos de PUBLIC_HEADER.';
case4:
return'CBOR canónico, DateKey canónica, perfil fijado en este lector y ninguna extensión crítica desconocida.';
return'Tipo y versión de esquema, CBOR canónico y esquema, con las extensiones en orden estricto; después, DateKey canónica, perfil fijado en este lector y ninguna extensión crítica desconocida.';
case5:
return'OUTER_TIME_AGE, dentro de SEALED_CONTROL, tiene exactamente un stanza tlock.';
return'La cabecera age de OUTER_TIME_AGE, dentro de SEALED_CONTROL, está bien formada (§28.1) y tiene exactamente un stanza tlock.';
case6:
return'La cabecera age de PAYLOAD_AGE tiene exactamente un stanza X25519.';
return'La cabecera age de PAYLOAD_AGE, que llega hasta el final del fichero, está bien formada (§28.1) y tiene exactamente un stanza X25519.';
case7:
return'La ronda de la DateKey se convierte en fecha de apertura con el perfil fijado, sin red.';
return'La ronda de la DateKey se convierte en fecha de apertura con el perfil fijado, sin red; esa fecha no pasa de 9999-12-31T23:59:59Z (§15).';
case8:
return'El stanza tlock nombra la ronda de la DateKey y la cadena del perfil fijado.';
return'El stanza tlock tiene dos argumentos, la ronda de la DateKey en decimal canónico y la cadena del perfil fijado en hexadecimal en minúsculas, comparados como texto exacto.';
default:
return'';
}
@ -65,27 +65,27 @@ export function errorGloss(code: ErrorCode): string {
case'ERR_INVALID_FLAGS':
return'FLAGS o RESERVED del prelude no valen cero.';
case'ERR_NON_CANONICAL_CBOR':
return'El CBOR no está en la forma canónica del perfil del protocolo (§58) o no sigue su esquema.';
return'El CBOR no está en la forma canónica del perfil del protocolo (§58) o no sigue su esquema: tipos, tamaños, claves u orden de las extensiones.';
case'ERR_UNKNOWN_PROFILE':
return'La DateKey nombra un perfil que este lector no tiene fijado.';
case'ERR_PROFILE_MISMATCH':
return'El stanza tlock usa una cadena distinta de la del perfil fijado.';
case'ERR_DATEKEY_INVALID':
return'La DateKey no es válida para su perfil.';
return'La DateKey no se puede leer como dk1_, o su ronda abriría después de 9999-12-31T23:59:59Z con el perfil fijado.';
case'ERR_DATEKEY_NON_CANONICAL':
return'La DateKey no está en su forma canónica dk1_.';
case'ERR_ROUND_MISMATCH':
return'La ronda del stanza tlock no es la de la DateKey.';
return'La ronda del stanza tlock no es la de la DateKey escrita en decimal canónico.';
case'ERR_RELEASE_UNAVAILABLE':
return'La firma de la ronda todavía no se puede obtener.';
case'ERR_RELEASE_INVALID':
return'La firma de la ronda no verifica con el perfil fijado.';
case'ERR_ACCESS_REQUIRED':
return'La política exige una clave de acceso .dkk.';
return'La política exige una credencial de acceso: una clave .dkk o la identidad X25519 de un destinatario.';
case'ERR_ACCESS_INVALID':
return'La clave de acceso no abre esta cápsula.';
case'ERR_POLICY_STRUCTURE_MISMATCH':
return'El número o el tipo de stanzas age no es el que exige el protocolo.';
return'El número o el tipo de stanzas age, o el número de argumentos del stanza tlock, no es el que exige el protocolo.';
case'ERR_HEADER_BINDING':
return'La cabecera pública no es la que se selló.';
case'ERR_INTEGRITY':
@ -103,7 +103,7 @@ export function policyGloss(policy: string): string {
case'time_only':
return'Solo tiempo: se abre con la firma pública de la ronda.';
case'time_and_key':
return'Tiempo y clave: además de la ronda hace falta una clave de acceso .dkk.';
return'Tiempo y clave: además de la firma de la ronda hace falta una credencial de acceso, una clave .dkk o la identidad X25519 de un destinatario.';
Una cápsula <code>.dkc</code> es un fichero cifrado que nadie puede abrir antes de una fecha: la clave depende de
Una cápsula <code>.dkc</code> es un fichero cifrado que no se puede abrir antes de una fecha: la clave depende de
una firma que todavía no existe. La publicará en esa fecha drand, una red pública que emite una firma nueva cada
pocos segundos. El inspector lee la parte pública de la cápsula y comprueba que está bien formada, con los mismos
pocos segundos; la garantía se apoya en que esa red no revele la firma antes de tiempo. El inspector lee la parte pública de la cápsula y comprueba que está bien formada, con los mismos
pasos que la herramienta <code>datekeys inspect</code>.
</p>
<pclass="actions">
@ -65,8 +65,9 @@
<li>
<h3>Lo que queda para la apertura</h3>
<p>
Superar los pasos 1 a 8 no prueba que la cápsula se pueda abrir. La cabecera pública solo queda autenticada en el
paso 15, al abrirla después de la fecha.
Superar los pasos 1 a 8 no prueba que la cápsula se pueda abrir. La cabecera pública solo queda vinculada al resto
de la cápsula en el paso 15, al abrirla después de la fecha, y ni siquiera entonces prueba quién la escribió ni
- `spec`: true for the 23 mutations listed in spec §64 (the first 23 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
before any release request (spec §63 step 9.a).
- `dkk`: the hex of a complete `.dkk` file (prelude and body) offered as the
access credential; absent when none is offered.
- `identities`: age X25519 identities (`AGE-SECRET-KEY-1…`) offered as access
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`).
- `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
`profile_quicknet.json`; `empty` pins none.
- `extensions`: the extensions the application implements. Each entry is known
at `(id, version)`, and its data is valid only when it equals the bytes of
`valid_data`. Absent: the application knows no extension, the state of the
base protocol V1.
- `network`: whether the failure may come after a release request. When false,
the reader must fail without requesting any release (§27, §63): every failure
of steps 1 to 8 and of step 9 before the request (9.a to 9.c).
- `frozen`: the capsule was built once with age randomness; its bytes are kept
and never regenerated. These cases have no `base`.
- `error`, `step`: the expected code and the step of §63 that fails.
Every case reproduces offline: the recorded release stands in for the network.
A reader that implements only steps 1 to 8 can replay every case whose `step` is
at most 8: 31 cases, 13 of them from §64. Steps 1 to 8 are summarised in "The
checks of steps 1 to 8" below.
### Credentials and the release: step 9
What happens between step 8 and the release request is spec §63 step 9: for
`time_and_key` only, an offered `.dkk` is checked as an object and then bound
to the capsule (9.a), at least one credential must be offered (9.b), and for
either policy a `now` before the round time of the DateKey fails without a
request (9.c); only then is the release requested. For `time_only` the
credentials play no part: the §64 case "access_policy=time_only with
time_and_key structure" offers a `.dkk` whose `capsule_digest` is that of the
unmutated capsule, and fails at step 12, not at step 9. The codes after the
request, for the release (step 10) and for the identities that open each age
file (steps 11, 13 and 17), are those of spec §63 as well. In this corpus:
- identities are not examined before the release (spec §63 step 13);
- a `release` of `null` is `ERR_RELEASE_UNAVAILABLE` at step 9;
- 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.
## The checks of steps 1 to 8
`mutations.json` and `inspect_differential.json` follow the rules of the
specification for steps 1 to 8, with the `default` registry (Quicknet pinned),
no extension known unless a case lists some, and no secret. The first failure
ends the flow, and within one object the first failing layer decides the code
(spec §69.1):
| Step | Rules | Spec |
|---|---|---|
| 1, 2 | magic, truncated prelude, framing version, FLAGS and RESERVED, `PUBLIC_HEADER_LEN` in 1 to 1048576 and `SEALED_CONTROL_LEN` in 1 to 67108864 | §22, §23 |
| 3 | the PUBLIC_HEADER bytes are present | §23 |
| 4 | PUBLIC_HEADER: layers 2 to 4, the `public_header` decoder of the schema vectors, then the pinned profile of the DateKey and the critical extensions; with no extension known, every `critical_extensions` array fails | §63, §69.1 |
| 5 | the SEALED_CONTROL bytes are present, then its age header, parsed within `SEALED_CONTROL_LEN` bytes: malformed is `ERR_INTEGRITY`, one tlock stanza is required | §28.1 |
| 6 | the age header of PAYLOAD_AGE, from its offset to the end of the file: malformed is `ERR_INTEGRITY`, one X25519 stanza is required | §22, §28.1 |
| 7 | the round time of the DateKey is at most 9999-12-31T23:59:59Z (Quicknet: round 83903165811 at most); a `dk1_` round above it passes step 4 and fails here | §15 |
| 8 | the tlock stanza has exactly two arguments, the canonical decimal round and the lowercase hex chain hash, compared as strings | §63 step 8 |
The reference parses age headers with `filippo.io/age` v1.3.2, whose parser
limits (1024 stanzas, 128 arguments after the type, 2 MiB) spec §74 lists as
implementation limits. No case of these corpora depends on them.
## `vectors/inspect_differential.json`
A differential corpus of the pre-unlock checks: 1825 deterministic mutations of
the five official `.dkc` fixtures, with the verdict of steps 1 to 8 of §63 as
the reference computes it (`capsule.Inspect` with the `default` registry, no
extension known, no network, no secret), by the rules that "The checks of
steps 1 to 8" above points to. The file repeats the format below in its