From ba8418cdbf047c1f23091beda33fa3c3cfd66478 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 25 Sep 2026 18:28:15 +0200 Subject: [PATCH] Start the DateKeys TypeScript project Skeleton for the TypeScript implementation of DateKeys v0.8.x: the plans in docs/, testdata/ vendored from g.activething.com/go/DateKeys at 5719f6a with a zero-dependency sync and check script, and the TypeScript and vitest tooling already used by the prototype, which now lives in ../AppOld. Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 6 + .gitignore | 5 + README.md | 50 + docs/PLAN_codec_cbor_y_pagina_svelte.md | 347 ++++++ docs/PLAN_libreria_go.md | 276 +++++ package.json | 20 + scripts/sync-testdata.mjs | 124 ++ testdata/SOURCE.json | 28 + testdata/fixtures/empty_payload.dkc | Bin 0 -> 783 bytes testdata/fixtures/empty_payload.json | 121 ++ testdata/fixtures/empty_payload.plaintext | 0 testdata/fixtures/time_and_key_portable.dkc | Bin 0 -> 1029 bytes testdata/fixtures/time_and_key_portable.dkk | Bin 0 -> 152 bytes .../fixtures/time_and_key_portable.dkk.json | 13 + testdata/fixtures/time_and_key_portable.json | 140 +++ .../fixtures/time_and_key_portable.plaintext | 1 + testdata/fixtures/time_and_key_recipients.dkc | Bin 0 -> 1220 bytes testdata/fixtures/time_and_key_recipients.dkk | Bin 0 -> 152 bytes .../fixtures/time_and_key_recipients.dkk.json | 13 + .../fixtures/time_and_key_recipients.json | 156 +++ .../time_and_key_recipients.plaintext | 1 + testdata/fixtures/time_only.dkc | Bin 0 -> 78799 bytes testdata/fixtures/time_only.json | 121 ++ testdata/fixtures/time_only.plaintext | 1000 +++++++++++++++++ testdata/fixtures/time_only_extensions.dkc | Bin 0 -> 891 bytes testdata/fixtures/time_only_extensions.json | 137 +++ .../fixtures/time_only_extensions.plaintext | 1 + testdata/vectors/dk1.json | 153 +++ testdata/vectors/profile_quicknet.json | 15 + testdata/vectors/quicknet_rounds.json | 107 ++ tsconfig.json | 18 + 31 files changed, 2853 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docs/PLAN_codec_cbor_y_pagina_svelte.md create mode 100644 docs/PLAN_libreria_go.md create mode 100644 package.json create mode 100644 scripts/sync-testdata.mjs create mode 100644 testdata/SOURCE.json create mode 100644 testdata/fixtures/empty_payload.dkc create mode 100644 testdata/fixtures/empty_payload.json create mode 100644 testdata/fixtures/empty_payload.plaintext create mode 100644 testdata/fixtures/time_and_key_portable.dkc create mode 100644 testdata/fixtures/time_and_key_portable.dkk create mode 100644 testdata/fixtures/time_and_key_portable.dkk.json create mode 100644 testdata/fixtures/time_and_key_portable.json create mode 100644 testdata/fixtures/time_and_key_portable.plaintext create mode 100644 testdata/fixtures/time_and_key_recipients.dkc create mode 100644 testdata/fixtures/time_and_key_recipients.dkk create mode 100644 testdata/fixtures/time_and_key_recipients.dkk.json create mode 100644 testdata/fixtures/time_and_key_recipients.json create mode 100644 testdata/fixtures/time_and_key_recipients.plaintext create mode 100644 testdata/fixtures/time_only.dkc create mode 100644 testdata/fixtures/time_only.json create mode 100644 testdata/fixtures/time_only.plaintext create mode 100644 testdata/fixtures/time_only_extensions.dkc create mode 100644 testdata/fixtures/time_only_extensions.json create mode 100644 testdata/fixtures/time_only_extensions.plaintext create mode 100644 testdata/vectors/dk1.json create mode 100644 testdata/vectors/profile_quicknet.json create mode 100644 testdata/vectors/quicknet_rounds.json create mode 100644 tsconfig.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..202cffd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Source files are normalised to LF. +* text=auto eol=lf + +# Vendored Go fixtures and vectors must keep their exact bytes: their SHA-256 is +# recorded in testdata/SOURCE.json and in the fixtures themselves. +testdata/** -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3422192 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.svelte-kit/ +build/ +coverage/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..229016a --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# DateKeys App + +Implementación en TypeScript del protocolo DateKeys v0.8.1 y página de prueba en el navegador. Sustituye al prototipo, archivado en `../AppOld` (API Quicknet en Go, CLI tlock y cliente Svelte, commit `4d2b0a1`). + +La implementación de referencia es la librería Go `g.activething.com/go/DateKeys`, en `../datekeys-go`. El plan de trabajo está en [docs/PLAN_codec_cbor_y_pagina_svelte.md](docs/PLAN_codec_cbor_y_pagina_svelte.md). + +## Contenido previsto + +| Parte | Ubicación | Paso del plan | +|---|---|---| +| Codec CBOR del subconjunto, parsers de schema, DateKey, `inspect` | `src/lib/dkc/` | 4 | +| Página inspector, sin red | SvelteKit, ruta `/inspect` | 5 | +| Cifrado y descifrado en el navegador | fase 2 | 6 | + +## Reglas + +- Los fixtures y vectores del Go son la verdad. Este proyecto nunca genera fixtures propios: `testdata/` es una copia exacta de un commit de la librería Go. +- Dependencias de ejecución: solo `age`, `drand`, `tlock` y lo que ellas arrastran. Ahora mismo no hay ninguna. +- Tooling de desarrollo: solo el de la lista siguiente. Cualquier otra dependencia se propone por escrito y no se instala sin aprobación. + +## Tooling de desarrollo + +| Paquete | Versión | Estado | +|---|---|---| +| `typescript` | 5.9.3 | en `package.json`; ya usado en el prototipo | +| `vitest` | 5.0.1 | en `package.json`; ya usado en el prototipo | +| `@vitest/coverage-v8` | la de `vitest` | pendiente de aprobación; cobertura del 100 % del codec | +| `@types/node` | 24.x | pendiente de aprobación; tests que leen `testdata/` desde disco | +| `svelte`, `@sveltejs/kit`, `@sveltejs/vite-plugin-svelte`, `vite`, `svelte-check` | las del prototipo | paso 5 | +| `@sveltejs/adapter-static` | por fijar | paso 5, pendiente de aprobación: la página es estática y no necesita servidor | + +Todas las versiones se fijan exactas y `package-lock.json` se versiona. + +## testdata + +```bash +npm run testdata:sync +``` + +Copia `testdata/` de `../datekeys-go` en `HEAD`, leyendo los blobs con git para no arrastrar cambios sin commit, y escribe `testdata/SOURCE.json` con el commit completo y el SHA-256 de cada fichero. Para fijar otro commit: `node scripts/sync-testdata.mjs sync --commit `. + +```bash +npm run testdata:check +``` + +Comprueba que los ficheros coinciden con `SOURCE.json`, sin faltantes ni sobrantes, y vuelve a leerlos del repositorio Go en el commit registrado. Sin la opción `--against`, `node scripts/sync-testdata.mjs check` verifica solo la copia local. Ambos comandos usan solo Node y git. + +`.gitattributes` marca `testdata/**` como binario para que git no altere ningún byte. + +Copia actual: 22 ficheros de `5719f6a4b3654f608091e32290d9fcfbc7e38062`. diff --git a/docs/PLAN_codec_cbor_y_pagina_svelte.md b/docs/PLAN_codec_cbor_y_pagina_svelte.md new file mode 100644 index 0000000..a453712 --- /dev/null +++ b/docs/PLAN_codec_cbor_y_pagina_svelte.md @@ -0,0 +1,347 @@ +# Plan: codec CBOR propio y página de prueba en Svelte + +Estado: v2, 25 de septiembre de 2026. Sustituye a la v1 (conservada en `AppOld/docs`) con la revisión, el panel sobre `data` y las decisiones 2, 3 y 4. La decisión 1 queda pendiente de confirmar el límite N (sección 3). + +Alcance: sustituir la dependencia CBOR externa de `datekeys-go` por un codec propio, escribir el mismo codec en TypeScript en el proyecto nuevo `App`, y construir sobre él la página de prueba: primero un inspector de cápsulas, después cifrado y descifrado en el navegador. + +Regla de dependencias del proyecto: en ejecución, solo `age`, `drand`, `tlock` y lo que ellas arrastran, en cualquier lenguaje. El tooling de desarrollo queda fuera de la regla, pero solo se admite el de la lista de la sección 13. Toda dependencia nueva, de ejecución o de tooling, se propone por escrito y no se instala sin aprobación. + +--- + +## 1. Situación de partida + +| Elemento | Estado verificado el 25-09-2026 | +|---|---| +| `datekeys-go`, paquete `codec` | Usa `github.com/fxamacker/cbor/v2` v2.9.4, que arrastra `github.com/x448/float16`. Es la única dependencia directa fuera de la regla. | +| Uso de esa librería | `codec` (Marshal, Unmarshal, Peek, CheckSchema, Valid) y structs con etiquetas `cbor:"N,keyasint"` en `profile`, `capsule`, `accesskey`, `extension` e `internal/testkit`. `extension.Wire.Data` es `cbor.RawMessage`. | +| Fixtures y vectores | Cinco cápsulas `.dkc`, dos `.dkk`, vectores de ronda, `dk1_` y `profile_hash`. Los vectores se regeneran byte a byte. Los `.dkc` se generaron una vez y están congelados: la aleatoriedad de `age` no se puede inyectar, así que regenerar una cápsula cambia `capsule_id`, las claves y todos sus bytes. | +| Proyecto TypeScript | `App`, nuevo y separado. Tiene un esqueleto sin dependencias instaladas, los planes en `docs/` y `testdata/` copiado del commit `5719f6a` de `datekeys-go` con `scripts/sync-testdata.mjs`, que verifica cada fichero contra `testdata/SOURCE.json`. | +| Prototipo | `AppOld`: API Quicknet en Go, CLI tlock y cliente Svelte (`web-client`), commit `4d2b0a1`. No se modifica. | +| `web` | Landing estática de datekeys.com. Fuera de este plan. | + +Fallos de la librería actual que este plan corrige, todos reproducidos el 25-09-2026: + +| Fallo | Reproducción | Se corrige en | +|---|---|---| +| `CheckDisjoint` es cuadrático y `DecodeHeader` lo ejecuta antes de comprobar las extensiones críticas | una PUBLIC_HEADER de 880 KB con 40 000 + 40 000 extensiones se decodifica sin error en 8,3 s, sin red ni secretos | paso 1 (máximo de 64 por array) y paso 2a (fusión lineal) | +| Asimetría entre sellar y abrir | `Encrypt` sella `data` de control con 14 o 15 arrays anidados; al llegar la fecha, `Open` la rechaza en el paso 14 (`exceeded max nested level 16`) y la cápsula ya no se puede abrir | paso 2a (`data` pasa a ser una hoja) y autocomprobación del encoder | +| Veredicto no determinista | la misma cápsula de 810 bytes, con `data` de cabecera `{NaN:0, NaN:1}`, la acepta `Inspect` 437 veces de 500 y `Open` 82 de 100 | paso 2a (fxamacker sale del camino de `data`) | +| `extension.New(id, v, nil)` emite `f6` | contradice §58.1 | paso 2a (`New` recibe `[]byte` y rechaza vacío) | + +--- + +## 2. Subconjunto CBOR del protocolo + +Del CDDL `spec/datekeys.cddl` se deduce que el protocolo solo usa: + +| Tipo CBOR | Tipo mayor | Uso | +|---|---|---| +| Entero sin signo | 0 | versiones, claves de mapa, política, periodo, genesis, versión de extensión | +| Cadena de bytes | 2 | identificadores, hashes, claves, `capsule_digest`, `data` de extensión | +| Cadena de texto | 3 | etiquetas de tipo, ids, DateKey compacta, `access_type` | +| Array | 4 | listas de extensiones | +| Mapa con claves enteras | 5 | todos los objetos | + +El spec lo nombra en §58 como perfil del protocolo: RFC 8949 §4.2.1 restringido a los tipos mayores 0, 2, 3, 4 y 5, con claves enteras sin signo. Con claves enteras coinciden el orden por bytes, el orden por longitud y el orden numérico, así que no hay ambigüedad de perfil. + +Reglas que el codec impone al decodificar y al codificar: + +- enteros y longitudes en su forma más corta; +- longitudes definidas; +- claves de mapa en orden ascendente estricto, sin duplicados; +- mapas cerrados: toda clave no prevista por el schema se rechaza; +- texto UTF-8 válido; +- tamaños acotados por el schema y por los límites de §57, que pasan a ser MUST (sección 9); +- como mucho tres niveles de contenedores (objeto, array de extensiones y extensión). `data` es una hoja y el codec nunca desciende dentro. + +Se rechazan con `ERR_NON_CANONICAL_CBOR`: enteros negativos, flotantes, tags, valores simples, longitudes indefinidas y claves no enteras. Los límites de tamaño usan el código que decida la sección 9. + +Tras decodificar, el decodificador reencodifica y compara byte a byte con la entrada. Es una defensa independiente de la decodificación estricta, y ahora cubre también `data`. + +--- + +## 3. Decisión normativa: `data` de las extensiones + +**Tipo: `bstr` no vacío.** Bytes opacos para el protocolo base. Un panel comparó cuatro opciones con tres jueces independientes (seguridad, interoperabilidad y gobernanza del spec) y un red team contra la ganadora: + +| Opción | Puntos (máx. 30) | +|---|---| +| A. `bstr` opaco no vacío | 25,5 (ganadora para los tres jueces) | +| B. Subconjunto estructurado recursivo | 19 | +| C. `any` fijado a un perfil preciso (CDE o dCBOR) | 11 | +| D. `any` como hoy, fijando el comportamiento de fxamacker | 6,5 | + +A gana porque: +- el protocolo base no analiza nada de `data`, sin superficie de parser ni recursión; +- un lector nunca analiza la `data` de extensiones que no conoce; +- la validez base es una comprobación de longitud, idéntica en todas las implementaciones. + +Es el diseño de X.509 (`extnValue` OCTET STRING) y TLS (`extension_data` opaco). + +B es la rival seria, pero: +- obliga a cada lector a validar datos que §54 dice que no interpreta; +- congela el espacio de valores; +- sus ventajas (canonicidad interna, vista estructurada, comprobación al sellar) se recuperan en A mediante la regla de registro de la sección 9. + +El red team no refutó el tipo. + +**Justificación §76.** Se basa en casos reproducibles, no en el tamaño del código: +1. El CDDL se contradice: las líneas 12 a 14 dicen que `null` y los vacíos nunca representan ausencia, y la línea 82 declara `? 2 => any`. +2. `extension.New(id, v, nil)` emite `f6`. +3. El veredicto no determinista con claves NaN. +4. El fixture oficial `time_only_extensions` usa claves de texto y `true`. +5. La asimetría de profundidad: una cápsula sellada que no se abre tras la espera. Es el caso decisivo. + +**Límite de tamaño: pendiente de confirmar.** El panel propuso N = 65 535 (el límite de TLS). El red team mostró que ninguna evidencia sostiene un N concreto, y que un N uniforme de 64 KiB bloquearía el modo `opaque_onion` del Dead Man Switch de los borradores v0.1 y v0.2, si ese servicio llevara cada capa anterior en una extensión de control. Según una simulación del red team con la librería actual, crece unos 477 bytes por renovación y se bloquea en la renovación 139. El diseño del Dead Man Switch sigue abierto, así que el argumento es condicional. + +Opciones: +1. **Recomendada:** sin N propio. `data` queda acotada por la trama de su contenedor (§57, que pasa a MUST), y cada extensión registrada declara su máximo en §72. +2. Límites por contenedor: 65 535 en PUBLIC_HEADER (pública y leída antes del desbloqueo) y la trama en CONTROL_CBOR y `.dkk`. +3. N uniforme de 65 535, registrando en §76 que limita el `opaque_onion`. + +--- + +## 4. Diseño del codec en Go + +Paquete `codec` reescrito sin reflexión y sin etiquetas de struct. + +```go +// Codificación: se acumula en un []byte. El primer error queda fijado y +// Err lo devuelve; las llamadas posteriores no hacen nada. +type Encoder struct{ buf []byte; err error } +func (e *Encoder) Map(pairs int) +func (e *Encoder) Array(items int) +func (e *Encoder) Uint(v uint64) +func (e *Encoder) Bstr(b []byte) +func (e *Encoder) Text(s string) // rechaza UTF-8 inválido +func (e *Encoder) Out() ([]byte, error) + +// Decodificación: cursor estricto sobre la entrada. El Decoder guarda la +// última clave de cada mapa abierto y exige orden ascendente estricto. +type Decoder struct{ /* entrada, posición, pila de últimas claves */ } +func (d *Decoder) Map(max int) (pairs int, err error) +func (d *Decoder) Key() (uint64, error) +func (d *Decoder) EndMap() error +func (d *Decoder) Array(max int) (items int, err error) +func (d *Decoder) Uint(max uint64) (uint64, error) +func (d *Decoder) Bstr(min, max int) ([]byte, error) // longitud comprobada contra lo que queda antes de copiar +func (d *Decoder) Text(max int) (string, error) +func (d *Decoder) Done() error // rechaza bytes sobrantes + +// Canonicidad: decodifica con decode, reencodifica con encode y compara. +func Unmarshal(in []byte, decode func(*Decoder) error, encode func(*Encoder)) error + +// Lee las claves 0 y 1 de un mapa (tipo y versión) antes de la +// decodificación estricta (spec §70). +func Peek(in []byte) (typeTag string, version uint64, err error) + +// Recorredor genérico del subconjunto, con profundidad y tamaño acotados. +// Es auxiliar: vectores compartidos, fuzzing, vista informativa del +// inspector y extensiones registradas cuyo `data` sea CBOR. Nunca decide +// la validez base de un objeto. +func Walk(in []byte, maxDepth, maxLen int) error +``` + +Cada schema escribe su codificación y decodificación a mano, en su paquete, en sustitución de los structs `wire`: + +| Schema | Paquete | Claves | +|---|---|---| +| Provider Profile | `profile` | 0 a 10 | +| PUBLIC_HEADER | `capsule` | 0 a 4, opcionales 5 y 6 | +| CONTROL_CBOR | `capsule` | 0 a 3, opcionales 4 y 5 | +| `.dkk` body | `accesskey` | 0 a 5, opcionales 6, 7 y 8 | +| `verification_metadata` | `accesskey` | 0 | +| extensión | `extension` | 0, 1, opcional 2 (`bstr` no vacío) | + +Reglas comunes: +- una clave opcional ausente se omite; +- un array vacío, un mapa vacío o un `h''` en una clave opcional es un error explícito, no un efecto de `omitempty`; +- la etiqueta de tipo y la versión se comprueban antes de decodificar el resto; +- los errores envuelven el sentinel de §69 que corresponda. + +Cambios en `extension`: +- `New` recibe `[]byte` y rechaza vacío o fuera de límite; +- el array se rechaza por su cabecera si supera 64 elementos, antes de iterar; +- `CheckDisjoint` se sustituye por una fusión lineal de los dos arrays ordenados. + +Autocomprobación del encoder: `Encrypt`, `EncodeHeader`, `EncodeControl` y el encoder de `.dkk` decodifican lo que producen con el decodificador del lector antes de sellar o escribir. Hay un fuzz target que exige que todo encode correcto implique un decode correcto. + +`go.mod` pierde `github.com/fxamacker/cbor/v2` y `github.com/x448/float16`. No entra nada a cambio. + +--- + +## 5. Diseño del codec en TypeScript + +Mismo diseño y misma API, en `App/src/lib/dkc/cbor.ts`, sobre `Uint8Array`, sin dependencias: + +- texto con `TextDecoder('utf-8', { fatal: true, ignoreBOM: true })`; sin `ignoreBOM`, un BOM inicial desaparece y TS rechaza lo que Go acepta; +- `Text` del encoder comprueba `isWellFormed()` antes de codificar, porque `TextEncoder` sustituye en silencio un surrogate suelto por `efbfbd`; +- los `extension_id` se comparan por sus bytes UTF-8, nunca como cadenas JavaScript, que comparan en orden UTF-16 (el par U+FF61 y U+10000 sale al revés); un port ingenuo divergió en 782 de 200 000 entradas; +- enteros siempre como `number`: todos los `uint` del CDDL quedan acotados por debajo de 2⁵³ (sección 9), y el decodificador rechaza lo que supere `Number.MAX_SAFE_INTEGER`; +- `data` se devuelve como `Uint8Array`; `undefined` significa clave ausente. + +Los parsers de schema van en el mismo directorio: `profile.ts`, `header.ts`, `control.ts`, `accesskey.ts`, `extension.ts`, `datekey.ts` e `inspect.ts`, que cubre los pasos 1 a 8 de §63. + +`datekey.ts` no porta el `roundForTime` del prototipo. Ese código usa `Date.parse`, que trunca a milisegundos, y falla el vector "genesis + 1ns" de `quicknet_rounds.json` (da la ronda 1 en vez de la 2). Hace falta un parser RFC 3339 con precisión de nanosegundos. + +La cabecera `age` de OUTER_TIME_AGE y PAYLOAD_AGE (pasos 5, 6 y 8) se analiza con un parser estricto según la gramática de `age`: stanzas, cuerpo base64 canónico, líneas de 64 columnas y línea MAC, sobre los ficheros binarios que localizan las longitudes del prelude. El filtro de líneas `-> ` del prototipo es demasiado laxo. + +--- + +## 6. Vectores compartidos entre lenguajes + +Fichero `testdata/vectors/cbor.json` en `datekeys-go`, generado desde Go y congelado. `App` lo recibe con `sync-testdata`. Los casos genéricos se ejecutan con `Walk`; los de schema, con el decodificador de cada schema. Los enteros por encima de 2⁵³ van como cadena decimal. + +```json +{ + "accept": [ + { "name": "uint 23 inline", "hex": "17", "value": 23 }, + { "name": "uint 24 one byte", "hex": "1818", "value": 24 }, + { "name": "uint 256 two bytes", "hex": "190100", "value": 256 }, + { "name": "uint 2^32 eight bytes", "hex": "1b0000000100000000", "value": 4294967296 }, + { "name": "empty bstr", "hex": "40" }, + { "name": "map two sorted keys", "hex": "a200010101" }, + { "name": "text with leading BOM", "hex": "64efbbbf61" } + ], + "reject": [ + { "name": "uint 23 with one extra byte", "hex": "1817", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "bstr length not shortest", "hex": "5800", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "keys out of order", "hex": "a201000001", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "duplicate key", "hex": "a200000001", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "text key", "hex": "a1616100", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "indefinite array", "hex": "9f01ff", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "tag", "hex": "c101", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "float", "hex": "f97e00", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "true", "hex": "f5", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "null", "hex": "f6", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "negative int", "hex": "20", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "truncated uint", "hex": "1901", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "length beyond input", "hex": "5affffffff", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "trailing byte", "hex": "0100", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "invalid UTF-8", "hex": "61ff", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "overlong UTF-8", "hex": "62c080", "error": "ERR_NON_CANONICAL_CBOR" }, + { "name": "UTF-8 surrogate", "hex": "63eda080", "error": "ERR_NON_CANONICAL_CBOR" } + ] +} +``` + +Además, un bloque por schema: objeto válido mínimo, clave desconocida, clave obligatoria ausente, tipo incorrecto y tamaño fuera de límite. + +Casos de extensión: +- `data` con `40`, con `5801xx` y en el límite de la sección 3 y uno más; +- `data` con tipo distinto de `bstr`: texto, `null`, `uint`, mapa, tag y longitud indefinida; +- 64 y 65 extensiones en un array; +- un `extension_id` que empieza por BOM; +- el par de orden U+FF61 y U+10000; +- `extension_version` en 2⁵³ − 1 y 2⁵³. + +--- + +## 7. Tests + +1. **Regresión byte a byte.** Tras el paso 2b, `profile_hash` de Quicknet, los hex de prelude, cabecera y control de las cinco cápsulas, y los dos `.dkk` completos tienen que ser idénticos a los fixtures congelados, sin ninguna excepción: el cambio de `data` y la regeneración de su fixture ocurren antes, en el paso 2a. +2. **Asertos sobre los bytes exactos de `data`** en los tests de conformidad, no solo sobre su número (`conformance_test.go:165` y `205`, `accesskey_test.go:54`). +3. **Vectores compartidos** de la sección 6, en Go y en TypeScript. +4. **Corpus de mutaciones exportado.** Los 45 casos de `capsule/mutation_test.go` (23 de ellos en los pasos 1 a 8 y sin red) se exportan como vectores congelados: bytes, código de error y paso. TypeScript tiene que reproducir el código y el paso de cada uno. Además, un corpus diferencial de mutaciones aleatorias con semilla fija y el veredicto de Go. +5. **Fuzzing en Go** de `Decoder`, de `Walk`, de cada decodificador de schema y de la propiedad "encode correcto implica decode correcto". El corpus de mutación existente sigue pasando. +6. **TypeScript con `vitest`.** Carga los fixtures de `App/testdata` y compara `inspect` con la salida congelada de `datekeys inspect -json` de cada fixture, generada en Go y guardada en `testdata`; los tests no ejecutan Go. Pasa `quicknet_rounds.json`, `dk1.json` y `profile_quicknet.json`. +7. **Cobertura** del codec al 100 % en ambos lenguajes. En TypeScript necesita `@vitest/coverage-v8` (sección 13). + +--- + +## 8. La página de prueba en Svelte + +En el proyecto `App`, como sitio estático, sin servidor. + +**Fase 1, inspector.** Ruta `/inspect`. Carga un `.dkc` desde disco o uno de los fixtures, ejecuta los pasos 1 a 8 y muestra, como la CLI Go, el resultado de cada paso: prelude, cabecera, política, DateKey, fecha de apertura, argumentos del stanza `tlock` frente al perfil pinneado y recuento de stanzas de OUTER_TIME_AGE y PAYLOAD_AGE. Sin secretos. Una CSP con `connect-src 'self'` garantiza que la página no habla con ningún otro origen. + +Contrato de las extensiones: +- id, versión, crítica o no, conocida o no, longitud y hex siempre; +- una vista de texto si los bytes son UTF-8 imprimible; +- una vista decodificada con `Walk` si los bytes son CBOR del subconjunto, marcada como informativa y no validada por el protocolo; +- todo el texto escapado; +- nunca se construyen objetos ni `Map` de JavaScript a partir de claves (`__proto__`, colisiones por encima de 2⁵³); +- la `data` de PUBLIC_HEADER y `.dkk` se marca como pública y no autenticada hasta el paso 15. + +**Fase 2, cifrar y descifrar.** Solo cuando la fase 1 pase con todos los fixtures y el corpus exportado. + +Necesita `age-encryption` 0.3.1, la librería `age` oficial en TypeScript, que admite `Recipient` e `Identity` propios. Arrastra `@noble/ciphers`, `@noble/curves` 2, `@noble/hashes` 2, `@noble/post-quantum` y `@scure/base`. Como `tlock-js` 0.9 sigue en `@noble/curves` 1.9.7 y `@noble/hashes` 1.8.0, el bundle tendrá dos versiones mayores de noble. La aprobación se decide al empezar la fase 2 (decisión 4). + +Piezas de `tlock-js` y `drand-client`: +- `tlock-js` aporta `encryptOnG2RFC9380` y `decryptOnG1` desde `crypto/ibe`, que no tiene restricción de `exports`; +- la serialización del cuerpo del stanza (U‖V‖W) no está ahí y se escribe aparte; +- `drand-client` verifica los releases. + +Las identidades `tlock` y X25519 aplican la cardinalidad de §63 (pasos 11, 13 y 17) dentro de `unwrapFileKey`, como en Go. La prueba de aceptación descifra un fixture `time_and_key` en el navegador con la firma embebida, sin red. + +--- + +## 9. Cambios en la especificación y el CDDL + +Todos en un único cambio normativo, con la justificación §76 de la sección 3: + +- **Extensiones.** + - CDDL: `extensions = [1*64 extension]` y `extension = { 0 => extension-id, 1 => extension-version, ? 2 => bstr .size (1..MAX) }`, con MAX según la opción elegida en la sección 3. + - §31 y §54: `data` es una cadena de bytes opaca. El protocolo base nunca la decodifica ni la valida, y la validez de PUBLIC_HEADER, CONTROL_CBOR y `.dkk` nunca depende de su contenido. Una extensión sin datos omite la clave 2, y `h''` es inválido. + - §31 y §54: se elimina la excepción de multiplicidad ("salvo que el schema registrado permita multiplicidad"), que contradice al CDDL y a la implementación de referencia. La multiplicidad va dentro de `data`. + - §54: una extensión crítica conocida con `data` inválida se rechaza con un código definido (nuevo en §69, o uno existente nombrado expresamente). Una no crítica conocida con `data` inválida se marca como inutilizable y el objeto sigue siendo válido. +- **Enteros.** `extension_version`, `period` y `genesis_time` acotados, como mucho a 2⁵³ − 1; para `extension_version` basta con 2³² − 1. En TypeScript, 2⁵³ + 1 se confunde con 2⁵³, y una versión crítica desconocida podría pasar por conocida. +- **§57.** Los límites pasan de "V1 recomienda" a MUST para encoders y decoders. Un único código de error para toda violación de límites; la referencia usa hoy `ERR_INTEGRITY` para las tramas. +- **§58.** Nombra el perfil de la sección 2, aplica también a la cabecera del `bstr` de `data` y nunca a su contenido. +- **§72.** + - Cada extensión registrada declara la codificación de su `data`, su forma canónica, su longitud máxima y sus vectores. + - Si la codificación es CBOR, usa el perfil de §58 de forma recursiva, con contenedores no vacíos y sin `null`. + - Sus lectores rechazan codificaciones no canónicas, y su encoder decodifica lo que produce antes de sellar. + - Las extensiones de firma firman los bytes exactos de `data`. +- **§64.** Mutaciones nuevas: `data` con tipo incorrecto, `data` vacía, `data` por encima del límite y 65 extensiones. +- **§68.** Un fixture `.dkk` con una extensión. +- **§74.** Se retira "formato exacto de extensiones" de lo provisional. +- **Documentación.** Se actualizan `docs/traceability.md` (decisión 6) y `CHANGELOG.md`, que recoge la rotura de API: `extension.New` recibe `[]byte`, y `data` distinta de `bstr` o vacía deja de ser válida. El fixture se regenera en el mismo commit. + +--- + +## 10. Orden de trabajo y criterios de aceptación + +| Paso | Contenido | Hecho cuando | +|---|---|---| +| 1 | Cambios de spec y CDDL de la sección 9 | texto aprobado | +| 2a | Con fxamacker todavía dentro: `data` como `bstr`, `New([]byte)`, máximo de 64, fusión lineal, autocomprobación del encoder. Se regenera `time_only_extensions` con `-force` (cabecera: los bytes de "public label", que no son CBOR; control: un ejemplo CBOR del subconjunto) y se añade el `.dkk` con extensión. | tests verdes; los tres fallos de la sección 1 tienen test de regresión; commit en Gitea | +| 2b | Codec propio en Go y baja de fxamacker | cero cambios en `testdata/`; regresión completa de la sección 7; `govulncheck` sin cambios; una noche de fuzzing limpia; commit en Gitea | +| 3 | Vectores CBOR, corpus de mutaciones exportado y salidas de `inspect -json` congeladas | Go los genera y los pasa; ficheros congelados | +| 4 | En `App`: `sync-testdata`, codec y parsers TypeScript, `inspect.ts` | `vitest` reproduce fixtures, vectores y corpus exportado; cobertura del codec al 100 % | +| 5 | Ruta `/inspect` | abre los fixtures y un `.dkc` arrastrado; la CSP bloquea cualquier otro origen | +| 6 | Fase 2 | `age-encryption` aprobada; un fixture `time_and_key` se descifra en el navegador | + +--- + +## 11. Riesgos + +- **Dos codecs propios** en vez de una librería auditada por muchos. Mitigación: subconjunto mínimo, `data` opaca, vectores compartidos, regresión byte a byte sin excepciones, corpus de mutaciones exportado y fuzzing. +- **Desviación entre Go y TypeScript.** Los fixtures y vectores del Go son la verdad; TypeScript nunca genera fixtures propios, y `testdata/SOURCE.json` fija el commit de origen y el hash de cada fichero. +- **Parser estricto de cabecera `age` en TypeScript.** Cubierto por las mutaciones de los pasos 5, 6 y 8. +- **`data` opaca aplaza la validación a cada extensión.** Es el tipo de fallo de CVE-2020-1971. Mitigación: la regla de §72, el perfil con nombre y el recorredor compartido. +- **Límite de `data`.** Si se elige un N uniforme, puede limitar servicios futuros, como el `opaque_onion` del Dead Man Switch. + +--- + +## 12. Decisiones + +1. **`data`:** `bstr` no vacío (recomendado, sección 3). Pendiente de confirmar el límite: la trama del contenedor (recomendado), límites por contenedor o un N uniforme de 65 535. +2. **Proyecto TypeScript:** aparte, en `App`. El prototipo queda en `AppOld`. Decidido. +3. **Regla de dependencias:** afecta a las de ejecución. El tooling de desarrollo se admite solo desde la lista explícita de la sección 13. Decidido. +4. **`age-encryption` 0.3.1:** se decide al empezar la fase 2. Decidido. + +--- + +## 13. Tooling de desarrollo de `App` + +| Paquete | Versión | Estado | +|---|---|---| +| `typescript` | 5.9.3 | en `package.json`; ya usado en el prototipo | +| `vitest` | 5.0.1 | en `package.json`; ya usado en el prototipo | +| `@vitest/coverage-v8` | la de `vitest` | pendiente de aprobación (sección 7) | +| `@types/node` | 24.x | pendiente de aprobación; tests que leen `testdata/` | +| `svelte`, `@sveltejs/kit`, `@sveltejs/vite-plugin-svelte`, `vite`, `svelte-check` | las del prototipo | paso 5 | +| `@sveltejs/adapter-static` | por fijar | pendiente de aprobación; paso 5, sitio estático | + +Todas las versiones se fijan exactas y `package-lock.json` se versiona. diff --git a/docs/PLAN_libreria_go.md b/docs/PLAN_libreria_go.md new file mode 100644 index 0000000..4bc4312 --- /dev/null +++ b/docs/PLAN_libreria_go.md @@ -0,0 +1,276 @@ +# Plan de implementación: `datekeys-go` + +Implementación de referencia en Go de la **DateKeys Protocol Specification v0.8.1**. + +Estado: borrador para revisión. Fecha: 25 de septiembre de 2026. + +--- + +## 1. Objetivo y principios + +Construir desde cero una librería Go que implemente el protocolo base tal como lo fija la v0.8.1, con estas reglas: + +1. **La especificación manda.** El código no añade semántica. Si el código descubre un problema en el spec, se abre un caso reproducible según la política de cambios de la sección 76. +2. **Cero criptografía propia.** Solo `age`, `tlock` y la verificación BLS de `drand`. La librería aporta framing, CBOR, bindings, reglas de verificación y flujo. +3. **Validar todo lo verificable localmente antes de tocar red o secretos.** Sección 63. +4. **Fallar cerrado.** Ningún plaintext parcial, ningún error enmascarado, ningún "verified: true" ajeno. +5. **Superficie pequeña y auditable.** Pocas dependencias, todas pinneadas, builds reproducibles, y un mapa de trazabilidad spec ↔ código para la revisión externa. +6. **Nueva librería, no refactor.** Lo aprovechable del prototipo se copia y se adapta; el repositorio actual no se modifica. + +--- + +## 2. Alcance de la primera versión de la librería (v0.1.0) + +Implementa: + +- DateKey: resolución fecha → ronda, `dk1_` canónico, vectores (secciones 14 a 19). +- Provider Profile Quicknet: struct, CBOR canónico, `profile_hash`, registro pinneado (secciones 10 a 13). +- Adaptador drand: obtención de releases por relays, verificación BLS local, modo estricto (secciones 35, 45 a 52). +- `.dkc`: prelude, PUBLIC_HEADER, CONTROL_CBOR, `header_binding`, `time_only` y `time_and_key`, flujo de cifrado y descifrado completo con SHOULD y MUST (secciones 20 a 39, 61 a 63). +- `.dkk`: framing, body, identity portable de un solo uso (secciones 40 a 44). +- Extensiones genéricas con reglas de duplicados y orden canónico (sección 54). +- Catálogo de errores (sección 69). Límites de parser (57). Canonicidad CBOR y omisión de opcionales (58, 58.1). +- Fixtures, mutaciones, fuzzing y CLI mínima. + +No implementa, a propósito: + +- Servidor de Release API ni Release Queue. La librería define la interfaz de fuente de releases; el servidor actual podrá consumirla más adelante. +- Almacenamiento, entrega de `.dkk`, servicios, extensiones concretas. +- Cliente TypeScript. Vendrá después con la misma suite de fixtures. + +--- + +## 3. Decisiones de diseño + +### 3.1 Módulo y layout + +Repositorio: Gitea propio, `https://g.activething.com/go/DateKeys.git`. Módulo: `g.activething.com/go/DateKeys`, la ruta que el propio Gitea anuncia en su etiqueta `go-import`. Si más adelante se quiere una ruta en `datekeys.com` que no dependa del servidor, es un cambio de una línea en `go.mod` y un `sed` en las importaciones, siempre antes de publicar. Go 1.26, con `go.mod` fijando la última versión de parche de la serie. No se usa GitHub para nada del proyecto; las dependencias alojadas allí se obtienen como módulos Go, igual que cualquier otra. + +```text +datekeys-go/ + go.mod + LICENSE Apache-2.0 (código) + README.md SECURITY.md CONTRIBUTING.md TRADEMARKS.md CHANGELOG.md + spec/ + DateKeys_Protocol_Specification_v0.8.1.md copia congelada + datekeys.cddl schema normativo de todos los CBOR + datekey/ DateKey, resolución, dk1_ canónico + profile/ Provider Profile, CBOR canónico, hash, registro pinneado, quicknet.go + provider/ interfaces Condition, Release, ReleaseSource, Verify + provider/drand/ relays HTTP, verificación BLS, modo estricto + codec/ CBOR determinista: Encode, Decode con comprobación por reencodificación, límites + codec/bech32/ codificación Bech32 para la forma humana de identities (ver 4.3) + agewrap/ identities y recipients envolventes de age: cardinalidad, sonda de inspección, stanza tlock + extension/ tipos y reglas de extensiones + capsule/ .dkc: framing, cabecera, control, binding, Encrypt, Inspect, Open + accesskey/ .dkk: framing, body, identity + errors.go sentinel errors del catálogo de la sección 69 + internal/testkit/ generación de fixtures, escritor de cabeceras age malformadas, mutadores + testdata/fixtures/ fixtures oficiales y sus valores esperados + docs/traceability.md mapa sección del spec → paquete, función, test + cmd/datekeys/ CLI: encrypt, decrypt, inspect, datekey, profile +``` + +### 3.2 API pública, deliberadamente pequeña + +```go +// datekey +type DateKey struct { ProfileID string; Round uint64 } +func Resolve(p *profile.Profile, at time.Time) (DateKey, error) // §15, precisión completa +func Parse(s string) (DateKey, error) // §19, solo forma canónica +func (d DateKey) Compact() string // dk1_... +func (d DateKey) UnlockAt(p *profile.Profile) time.Time + +// profile +type Profile struct { ID, Provider, Network string; ChainHash [32]byte; PublicKey []byte; + Period time.Duration; GenesisTime int64; Scheme string; GenesisSeed [32]byte } +func (p *Profile) CanonicalCBOR() []byte +func (p *Profile) Hash() [32]byte // §11 +var Quicknet *Profile // §12, pinneado en el binario +type Registry interface { Lookup(id string) (*Profile, bool) } + +// provider +type Condition struct { Round uint64 } +type Release struct { Round uint64; Signature []byte } +type ReleaseSource interface { Fetch(ctx context.Context, p *profile.Profile, c Condition) (Release, error) } +func Verify(p *profile.Profile, c Condition, r Release) error // §51, BLS local siempre + +// capsule +type Policy uint8 // TimeOnly = 0, TimeAndKey = 1 +type EncryptOptions struct { + Profile *profile.Profile + UnlockAt time.Time + Policy Policy + Recipients []age.Recipient // X25519 de destinatarios conocidos + NewPortableKey bool // genera I_ACCESS y devuelve la .dkk + Critical, Noncritical []extension.Extension +} +type Result struct { DateKey datekey.DateKey; CapsuleID [16]byte; PortableKey *accesskey.AccessKey } +func Encrypt(dst io.Writer, src io.Reader, opts EncryptOptions) (*Result, error) + +type Inspection struct { /* prelude, cabecera, política, ronda, unlock, resultado de cada comprobación */ } +func Inspect(r io.ReadSeeker, reg profile.Registry) (*Inspection, error) // pasos 1 a 8, sin red ni secretos + +type OpenOptions struct { + Registry profile.Registry + Source provider.ReleaseSource + Identities []age.Identity // X25519 propias del destinatario + AccessKey *accesskey.AccessKey // .dkk portable + Now func() time.Time // inyectable para tests +} +func Open(dst io.Writer, r io.ReadSeeker, opts OpenOptions) error // pasos 9 a 18 + +// accesskey +type AccessKey struct { CredentialID, CapsuleID [16]byte; Type string; Material []byte; + Verification *Verification; Critical, Noncritical []extension.Extension } +func Encode(w io.Writer, k *AccessKey) error +func Decode(r io.Reader) (*AccessKey, error) +func (k *AccessKey) Identity() (age.Identity, error) +``` + +Reglas de la API: + +- `io.Reader` y `io.Writer` en todo; el payload nunca se carga entero en memoria. +- `Open` recibe `io.ReadSeeker` para poder inspeccionar la cabecera de PAYLOAD_AGE en el offset `16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN` antes de pedir el release. Si el lector no permite seek, se omite el SHOULD y se aplica el MUST al abrir. +- El reloj se inyecta. Ningún paquete llama a `time.Now` directamente salvo la CLI. +- Ningún tipo imprime secretos en `String()`. Las identities se pasan como tipos de `age`, no como bytes sueltos, salvo en `accesskey`. + +### 3.3 Cómo se usan `age` y `tlock` + +- **PAYLOAD_AGE e INNER_ACCESS_AGE:** `age.Encrypt` y `age.Decrypt` con recipients e identities X25519 de la API pública de `age`. +- **OUTER_TIME_AGE:** `age.Encrypt` con un recipient propio en `agewrap` que llama a `tlock.TimeLock` y `tlock.CiphertextToBytes`, ambos exportados, y emite el stanza `tlock ` con el mismo formato que `tlock`. Para abrir, una identity propia que comprueba el conjunto completo de stanzas, la ronda y el chain hash contra la DateKey y el perfil, y llama a `tlock.TimeUnlock`, que verifica el beacon antes de descifrar. + - Motivo: `tlock.New(...).Decrypt` usa una identity no exportada en la que no podemos imponer la cardinalidad ni evitar que todo error se convierta en `ErrTooEarly`. Usando su núcleo exportado se mantiene la compatibilidad de stanza con `tle` y se gana control del flujo. +- **Cardinalidad MUST:** en las tres identities envolventes, dentro de `Unwrap`, que recibe todos los stanzas del fichero. Sin APIs internas. +- **Inspección SHOULD:** `age.ExtractHeader` lee únicamente la cabecera del fichero, y `age.DecryptHeader` con una identity sonda, cuyo `Unwrap` registra los stanzas recibidos y devuelve un error centinela distinto de `ErrIncorrectIdentity`, entrega los stanzas parseados por `age` sin descifrar nada ni usar secretos. Ambas funciones son API pública de `age` 1.3. Para PAYLOAD_AGE se aplica en el offset del prelude. No hace falta parser propio. + +--- + +## 4. Dependencias + +| Dependencia | Versión | Uso | Nota | +|---|---|---|---| +| `filippo.io/age` | v1.3.2 | ficheros age, X25519, `Stanza`, `Recipient`, `Identity` | única implementación de STREAM; no se reimplementa | +| `github.com/drand/tlock` | v1.2.0 | `TimeLock`, `TimeUnlock`, `CiphertextToBytes`, `BytesToCiphertext` | no se usa `tlock.New`, `Encrypt` ni `Decrypt` | +| `github.com/drand/drand/v2` | v2.1.7 | `crypto.Scheme`, `VerifyBeacon`, `chain.Info` | arrastra gRPC y otros; `govulncheck` ya confirmó que no son alcanzables | +| `github.com/drand/kyber` y `kyber-bls12381` | transitivas | pairing BLS12-381 | sobre `kilic/bls12-381`, archivado; vigilar y documentar en SECURITY.md | +| `github.com/fxamacker/cbor/v2` | v2.9.4 | CBOR determinista | `CoreDetEncOptions` para codificar; límites en decodificación | + +Sin otras dependencias en la librería. Tests con la biblioteca estándar. CLI con `flag`. HTTP con `net/http`. + +### 4.1 Política de versiones + +`go.mod` pinneado, `go.sum` verificado, `go mod verify` en CI, `govulncheck` en cada PR, Renovate (compatible con Gitea) solo para parches, con revisión manual de cualquier cambio en `age`, `tlock`, `drand` o `kyber`. + +### 4.2 Canonicidad CBOR + +Codificar con Core Deterministic Encoding. Al decodificar, además de los límites de `fxamacker` (sin longitudes indefinidas, sin tags, sin claves duplicadas, profundidad y tamaños acotados), se **reencodifica y se compara byte a byte** con la entrada. Cualquier diferencia es `ErrNonCanonicalCBOR`. Es el mismo principio que `dk1_` y no depende de que una librería prometa rechazar todas las formas no canónicas. + +### 4.3 Bech32 + +`age` solo acepta identities y recipients X25519 en Bech32 por su API pública, y su paquete Bech32 es `internal`. El `.dkk` guarda 32 bytes crudos por spec. Se necesita un codificador Bech32 de unas cien líneas para pasar de bytes a `AGE-SECRET-KEY-1...` y de `age1...` a bytes. Es codificación, no criptografía. Opciones: copiar el paquete interno de `age` con su licencia BSD y atribución, o reimplementar BIP-173 con vectores de prueba. Propuesta: copiar, para no divergir. + +--- + +## 5. Qué se copia del prototipo + +| Origen | Destino | Cambios | +|---|---|---| +| `pkg/datekeys/key.go`: `RoundForTime`, `TimeForRound`, `ParseDateKey`, `FromRound` | `datekey/` | quitar campos de API y el alias `EncryptionKey`; el struct de respuesta HTTP se queda en el servidor | +| `pkg/datekeys/key_test.go`, `FuzzRecipient` | `datekey/` | renombrar, ampliar con vectores de la sección 65 | +| `internal/quicknet/trust.go` | `profile/quicknet.go` | añadir CBOR canónico y hash; conservar la autocomprobación de chain hash | +| `internal/drand/client.go` y tests | `provider/drand/` | carrera entre relays, respuestas malformadas, cancelación; adaptar a `ReleaseSource` | +| `pkg/datekeys/crypto.go` | no se copia | sustituido por `agewrap` sobre el núcleo exportado de `tlock`; se conserva la doble verificación BLS | +| `tests/integration/quicknet_test.go` | `provider/drand/`, build tag `integration` | prueba en vivo contra Quicknet | +| `cmd/datekeys` | reescrito | misma ergonomía, nueva API | +| `internal/api`, `web-client` | no se tocan | consumirán la librería más adelante | + +--- + +## 6. Estilo de código + +- Identificadores, comentarios de código, mensajes de error y commits en **inglés**. Documentación de usuario en inglés con versión en español. Motivo: la revisión externa y la comunidad de `age` y drand. +- `gofmt`, `goimports`, `go vet`, `staticcheck`, `golangci-lint` con configuración corta: `errcheck`, `govet`, `staticcheck`, `ineffassign`, `unparam`, `gosec` en modo aviso. +- Errores: un sentinel por entrada del catálogo de la sección 69, envueltos con `%w` y contexto; nunca se sustituye un error por otro más cómodo. Los tests de mutación comprueban `errors.Is`. +- Parsers: límites antes de reservar memoria, `io.LimitReader`, ningún `panic` alcanzable desde la entrada. Todo parser tiene un objetivo de fuzzing. +- Sin estado global mutable. El registro de perfiles se pasa explícitamente; el registro por defecto contiene solo Quicknet. +- Comentarios de paquete y de función citan la sección del spec que implementan, por ejemplo `// Spec §26`. +- Secretos: no se registran en logs, no aparecen en `String()`, se borran de buffers propios al terminar, sin prometer más de lo que Go garantiza. +- Salida atómica en la CLI: fichero temporal en el mismo directorio y renombrado al final; nunca sobrescribir destinos existentes. +- Versionado semántico. `v0.x` hasta que el spec sea v1.0. Sin promesa de estabilidad de API antes de `v1.0.0`. + +--- + +## 7. Estrategia de tests + +1. **Unitarios** por paquete, tabla-driven, biblioteca estándar. +2. **Vectores de spec**, generados por el código y guardados como golden files versionados: ronda (frontera, un segundo antes y después, fracción de segundo tras frontera, 2030-01-01, cercanos al genesis), `dk1_` (objeto, JSON canónico, base64url, cadena), `profile_hash` de Quicknet. +3. **Fixtures `.dkc` y `.dkk`** (secciones 67 y 68): generados una vez con `internal/testkit` sobre una **ronda ya publicada**, con la firma BLS embebida en el fixture. Los tests verifican la firma contra la clave pinneada y descifran **sin red**. Se comparan todos los valores intermedios: cabecera, prelude, binding, control, `I_PAYLOAD`, plaintext y el error esperado en cada etapa. +4. **Corpus de mutaciones** (sección 64): las veinte mutaciones como funciones sobre un fixture válido; cada una afirma su sentinel error concreto. +5. **Escritor de cabeceras age malformadas** en `testkit`: añade stanzas o cambia tipos manteniendo el MAC válido, porque en el test se conoce la file key. Demuestra que la comprobación estructural rechaza lo que el MAC aceptaría. Es la prueba directa de la amenaza del creador. Implementación: según la especificación C2SP, el MAC de cabecera es HMAC-SHA-256 con clave HKDF-SHA-256 de la file key e info `header`, sobre el texto de la cabecera; `testkit` lo recalcula con `golang.org/x/crypto/hkdf`, ya presente en el grafo por `age`. Para abrir ficheros de test con una file key conocida se usa `age.NewInjectedFileKeyIdentity`, también API pública. +6. **Propiedades:** `Encode(Decode(x)) == x` para todo fixture; toda reordenación de claves o ensanchamiento de enteros se rechaza como no canónico; `Parse(Compact(d)) == d`. +7. **Fuzzing** nativo de Go: prelude, PUBLIC_HEADER, CONTROL_CBOR, `.dkk`, `dk1_`, cabecera age vía sonda. Corpus sembrado con fixtures y mutaciones. Presupuesto corto en cada PR, largo en nightly. +8. **Integración en vivo** con build tag: cifrar hacia ahora más treinta segundos, esperar, abrir por relays reales. Nightly, no en cada PR. +9. **Interoperabilidad con herramientas ajenas:** extraer SEALED_CONTROL de un fixture `time_only` y abrirlo con la CLI `tle` oficial; extraer PAYLOAD_AGE y abrirlo con la CLI `age` usando `I_PAYLOAD`. Si ambas pasan, la afirmación "son ficheros age estándar" queda demostrada por terceros. +10. **Cobertura:** mínimo 90 % en `codec`, `capsule`, `accesskey`, `datekey`, `agewrap`; `-race` siempre. +11. **CDDL:** `spec/datekeys.cddl` valida los fixtures en un job opcional de CI con la herramienta `cddl`; si no se quiere otra toolchain, se mantiene como documento normativo y se comprueba a mano en cada cambio de schema. + +--- + +## 8. CI, releases y cadena de suministro + +- Gitea Actions, con la misma sintaxis que GitHub Actions: `test` en Linux y Windows con la versión estable de Go y la anterior, macOS cuando haya runner; `lint`; `vuln` con `govulncheck`; `fuzz-short`; `integration` nightly por cron; `sbom` con `cyclonedx-gomod`. +- Releases con `goreleaser`, que publica en Gitea: `-trimpath`, `CGO_ENABLED=0`, checksums publicados, firma con `cosign` con clave propia, ya que la firma sin clave depende de un proveedor OIDC que Gitea no ofrece. +- `SECURITY.md` con canal de reporte, versiones soportadas y la dependencia de `kilic/bls12-381` declarada como riesgo conocido y vigilado. +- `TRADEMARKS.md`: "DateKeys" reservado; "implements the DateKey protocol" permitido. + +--- + +## 9. Hitos y criterios de aceptación + +**M0. Cimientos.** Esqueleto, `go.mod` pinneado, CI con lint y `govulncheck`, `codec` con canonicidad por reencodificación y límites, `spec/datekeys.cddl` inicial, `profile` con Quicknet, su CBOR y su hash como primer vector oficial. +Hecho cuando: el hash del perfil está congelado en un golden file y CI está verde en tres sistemas. + +**M1. Objetos.** `datekey` copiado y ampliado; framing DKC1 y DKK1; codecs de PUBLIC_HEADER, CONTROL_CBOR y `.dkk`; `header_binding`; `extension`; catálogo de errores; fuzzing de todos los parsers. +Hecho cuando: los vectores de ronda y `dk1_` están generados y versionados y el fuzzing lleva una noche sin fallos. + +**M2. `time_only`.** `agewrap` con recipient e identity tlock estrictos, sonda de inspección, PAYLOAD_AGE, `Encrypt`, `Inspect`, `Open` con los 18 pasos; `provider/drand` copiado. +Hecho cuando: un fixture `time_only` sobre ronda pasada se abre offline, la prueba con `tle` y `age` externas pasa, y las mutaciones aplicables fallan con su error exacto. + +**M3. `time_and_key`.** INNER_ACCESS_AGE con varios recipients, `.dkk` portable de un solo uso, `Identities` y `AccessKey` en `Open`. +Hecho cuando: fixtures `time_and_key` con uno y varios recipients se abren offline, la reutilización de `I_ACCESS` está prohibida en `Encrypt` y probada, y las veinte mutaciones están cubiertas. + +**M4. Conformidad.** Suite de fixtures oficial, golden files, `docs/traceability.md` completo, integración nightly, SBOM y release firmada `v0.1.0`. +Hecho cuando: cada sección normativa de la 14 a la 69 apunta a al menos un test, y `v0.1.0` se construye de forma reproducible. + +**M5. CLI.** `datekeys encrypt`, `decrypt`, `inspect`, `datekey resolve`, `profile hash`. `inspect` ejecuta solo los pasos 1 a 8 y nunca pide un release ni usa secretos. +Hecho cuando: la CLI abre los fixtures y reproduce la ergonomía del prototipo con salida atómica. + +Fuera de estos hitos: SDK TypeScript, migración del cliente web, servidor de Release API sobre la librería, segundo perfil `evmnet`. + +--- + +## 10. Trazabilidad + +`docs/traceability.md` mantiene una tabla sección → paquete → función → test. Se actualiza en el mismo PR que cualquier cambio de código normativo. Es el documento que se entrega al revisor externo junto con el spec, los fixtures y el corpus de mutaciones. + +--- + +## 11. Decisiones a revisar + +1. Decidida: ruta `g.activething.com/go/DateKeys`. Queda abierto si se pasa a una ruta en `datekeys.com` antes de publicar. Nota: el servidor ejecuta Gitea 1.18.3, sin Actions; los workflows de `.gitea/workflows` quedan listos para Gitea 1.21 o superior con un `act_runner`, y hasta entonces `scripts/check.sh` es el control obligatorio antes de cada push. +2. Inglés para código, errores y commits. +3. Licencia Apache-2.0 para el código, CC-BY-4.0 para el spec. +4. Copiar el Bech32 interno de `age` frente a reimplementarlo. +5. Depender del módulo `drand/v2` completo o extraer solo la verificación BLS a un paquete propio sobre `kyber-bls12381`. La primera opción es más simple; la segunda reduce el grafo y el ruido de `govulncheck`. +6. Incluir en la librería un cliente de la Release API de DateKeys además del de relays drand, o dejarlo para el servidor. +7. Validación CDDL en CI con toolchain externa, o solo como documento. +8. Si `Open` exige `io.ReadSeeker` o acepta `io.Reader` y degrada el SHOULD. + +--- + +## 12. Riesgos conocidos + +- `kilic/bls12-381` archivado bajo `drand` y `tlock`. Mitigación: pinneado, vigilancia, plan de fork o de migración documentado en `SECURITY.md`. +- `tlock` sin etiqueta desde agosto de 2024. Mitigación: pinneado por versión; solo se usa su núcleo exportado. +- Fixtures sobre rondas pasadas dependen de que la firma embebida sea auténtica. Mitigación: se verifica BLS en cada ejecución contra la clave pinneada. +- Desviación spec ↔ código. Mitigación: trazabilidad, política de cambios de la sección 76, y una segunda implementación en TypeScript como prueba de interoperabilidad. diff --git a/package.json b/package.json new file mode 100644 index 0000000..0839dae --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "datekeys-app", + "version": "0.0.0", + "private": true, + "description": "DateKeys in TypeScript: canonical CBOR codec, DKC1/DKK1 parsers, capsule inspector and, later, browser encryption and decryption.", + "type": "module", + "engines": { + "node": ">=20" + }, + "scripts": { + "test": "vitest run", + "typecheck": "tsc --noEmit", + "testdata:sync": "node scripts/sync-testdata.mjs sync", + "testdata:check": "node scripts/sync-testdata.mjs check --against ../datekeys-go" + }, + "devDependencies": { + "typescript": "5.9.3", + "vitest": "5.0.1" + } +} diff --git a/scripts/sync-testdata.mjs b/scripts/sync-testdata.mjs new file mode 100644 index 0000000..9bd4bbf --- /dev/null +++ b/scripts/sync-testdata.mjs @@ -0,0 +1,124 @@ +#!/usr/bin/env node +// Vendors testdata/ from the Go reference implementation (g.activething.com/go/DateKeys) +// at one pinned commit and verifies it. The Go fixtures and vectors are the source of +// truth: this project never generates its own. +// +// node scripts/sync-testdata.mjs sync [--repo ../datekeys-go] [--commit HEAD] +// node scripts/sync-testdata.mjs check [--against ../datekeys-go] +// +// sync copies every blob under testdata/ at the commit (read with git, so uncommitted +// changes in the Go checkout are never picked up) and writes testdata/SOURCE.json with +// the full commit id and the SHA-256 of each file. check verifies that the files on disk +// match SOURCE.json exactly, with no missing or extra files; with --against it also +// re-reads every blob from the Go repository at the recorded commit. +// No dependencies: Node and git only. + +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, join, relative, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const MODULE = 'g.activething.com/go/DateKeys'; +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const TESTDATA = join(ROOT, 'testdata'); +const SOURCE = join(TESTDATA, 'SOURCE.json'); +const DEFAULT_REPO = resolve(ROOT, '..', 'datekeys-go'); + +function git(repo, args) { + return execFileSync('git', ['-C', repo, ...args], { encoding: 'buffer', maxBuffer: 1 << 30 }); +} + +function sha256(buf) { + return createHash('sha256').update(buf).digest('hex'); +} + +// Files under testdata/ at commit, as paths relative to testdata/ with forward slashes. +function listBlobs(repo, commit) { + const out = git(repo, ['ls-tree', '-r', '-z', '--full-tree', commit, '--', 'testdata']).toString('utf8'); + return out.split('\0').filter(Boolean).map(line => { + const [meta, path] = line.split('\t'); + const [mode, type] = meta.split(' '); + if (type !== 'blob' || mode === '120000') throw new Error(`unsupported entry ${path} (${mode} ${type})`); + return path.slice('testdata/'.length); + }).sort(); +} + +function readBlob(repo, commit, path) { + return git(repo, ['cat-file', 'blob', `${commit}:testdata/${path}`]); +} + +// Files on disk under testdata/, excluding SOURCE.json. +function listLocal(dir = TESTDATA) { + let files = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) files = files.concat(listLocal(full)); + else if (full !== SOURCE) files.push(relative(TESTDATA, full).split(sep).join('/')); + } + return files.sort(); +} + +function sync(repo, rev) { + const commit = git(repo, ['rev-parse', '--verify', `${rev}^{commit}`]).toString('utf8').trim(); + const paths = listBlobs(repo, commit); + if (paths.length === 0) throw new Error(`no testdata/ at ${commit}`); + // Read everything before touching the local copy, so a failed read leaves it intact. + const blobs = paths.map(path => [path, readBlob(repo, commit, path)]); + rmSync(TESTDATA, { recursive: true, force: true }); + const files = {}; + for (const [path, data] of blobs) { + const target = join(TESTDATA, ...path.split('/')); + mkdirSync(dirname(target), { recursive: true }); + writeFileSync(target, data); + files[path] = sha256(data); + } + writeFileSync(SOURCE, JSON.stringify({ module: MODULE, commit, files }, null, 2) + '\n'); + console.log(`testdata: ${paths.length} files from ${MODULE} at ${commit}`); +} + +function check(against) { + const source = JSON.parse(readFileSync(SOURCE, 'utf8')); + const expected = Object.keys(source.files).sort(); + const actual = listLocal(); + const errors = []; + for (const path of expected) { + if (!actual.includes(path)) { errors.push(`missing ${path}`); continue; } + if (sha256(readFileSync(join(TESTDATA, ...path.split('/')))) !== source.files[path]) errors.push(`modified ${path}`); + } + for (const path of actual) if (!(path in source.files)) errors.push(`extra ${path}`); + if (against) { + const upstream = listBlobs(against, source.commit); + if (upstream.join('\n') !== expected.join('\n')) errors.push(`file list differs from ${source.commit}`); + for (const path of upstream) { + if (sha256(readBlob(against, source.commit, path)) !== source.files[path]) errors.push(`differs from upstream ${path}`); + } + } + if (errors.length) { + for (const e of errors) console.error(`testdata: ${e}`); + process.exit(1); + } + console.log(`testdata: ${expected.length} files match ${source.module} at ${source.commit}${against ? ' (verified against the repository)' : ''}`); +} + +function option(args, name, fallback) { + const i = args.indexOf(name); + if (i === -1) return fallback; + if (i + 1 >= args.length) throw new Error(`${name} needs a value`); + return args[i + 1]; +} + +const [command, ...args] = process.argv.slice(2); +try { + if (command === 'sync') sync(resolve(option(args, '--repo', DEFAULT_REPO)), option(args, '--commit', 'HEAD')); + else if (command === 'check') { + const against = option(args, '--against', null); + check(against && resolve(against)); + } else { + console.error('usage: sync-testdata.mjs sync [--repo PATH] [--commit REV] | check [--against PATH]'); + process.exit(2); + } +} catch (err) { + console.error(`testdata: ${err.message}`); + process.exit(1); +} diff --git a/testdata/SOURCE.json b/testdata/SOURCE.json new file mode 100644 index 0000000..b7f3072 --- /dev/null +++ b/testdata/SOURCE.json @@ -0,0 +1,28 @@ +{ + "module": "g.activething.com/go/DateKeys", + "commit": "5719f6a4b3654f608091e32290d9fcfbc7e38062", + "files": { + "fixtures/empty_payload.dkc": "871e9bf05b52bbae17f3adfbbf97b46e7f0e53aa8f57bcaa506e43f36f53a9d4", + "fixtures/empty_payload.json": "584f7a5a8982d5a9184ee6a4699fa39ad82c92a3d01d573c3e94e603db9b0956", + "fixtures/empty_payload.plaintext": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "fixtures/time_and_key_portable.dkc": "2e97878078bae6358037a9c264f379a3cbe839f767d69836b0343f35657b2972", + "fixtures/time_and_key_portable.dkk": "e528fa2c832c91119f0684bb9d6fb3c4c2d0d55183482890e7c4fe92f668426a", + "fixtures/time_and_key_portable.dkk.json": "6f940bc94f4eb13e41ccbbb76c69098ec9c5bc32cb86963e57b839c56ebf9534", + "fixtures/time_and_key_portable.json": "74936213a5d481dc216ad36115523bebc6973823cf89782b50a96eb4649ffcbe", + "fixtures/time_and_key_portable.plaintext": "937492203d207d6fe36161b8696bf1f05b8b4cc56d855c44853f4b76aad3a05b", + "fixtures/time_and_key_recipients.dkc": "69ac110380f5d768b5b6afaa157a50ed17d8ceccfbd4604ffa5b6da38539b635", + "fixtures/time_and_key_recipients.dkk": "19f6c47150c3194712d454f43c7392b7344e6b4e7b074d83e9ca5f563a8e072f", + "fixtures/time_and_key_recipients.dkk.json": "e2f8adde5da559be559647951d95b949c7eec6bffb1fea8e477a5b5521971f3a", + "fixtures/time_and_key_recipients.json": "446815de9bff62e6dcc00a106ba7ab05a3601c748b6de8c53a0c3ef5ce7ce843", + "fixtures/time_and_key_recipients.plaintext": "0e9fd50e98a85953aa9cf07a11ee3c62bb3d7622f344f1c6ce744d1ed111659f", + "fixtures/time_only.dkc": "99e915810d595f1092700b728f5e5081d78efe83f5343e76325b1bcc2c33ccf2", + "fixtures/time_only.json": "a38a6287b10ad944669f07b3327cd7405e9f76778240ec935a408580261d0e84", + "fixtures/time_only.plaintext": "53b8ee821fb7b678e89d4f93da1812339f6cc1ab83aac6ed1432db99df784be5", + "fixtures/time_only_extensions.dkc": "1e7effd58016d9447f9a2e7c9645c658604979c4e35af675d1dc6c4ae12ac444", + "fixtures/time_only_extensions.json": "c70ead5b8e196b942f89dc6c105e651d2ed2acc4289d1fd2dca98bd9eeb83293", + "fixtures/time_only_extensions.plaintext": "1129768e195e2f1e50b7a6f926b6eebef120212c29b5642c8a662c503b2a9131", + "vectors/dk1.json": "5127b8015b93c8f2b7cc92b75cbbad0a22073b39381901851a79656383d83426", + "vectors/profile_quicknet.json": "caa840b2ae49b2c220fb31c0a71ade98d170c1d5ab2e2210e980b98c88d58bec", + "vectors/quicknet_rounds.json": "fb9fb27671e5c1db2a80a2492e82fe3a702a5ddbf7afffe95c64f842d07bc54f" + } +} diff --git a/testdata/fixtures/empty_payload.dkc b/testdata/fixtures/empty_payload.dkc new file mode 100644 index 0000000000000000000000000000000000000000..f94e3b0cbbef3ccf8d0d3dc3affba429402c3029 GIT binary patch literal 783 zcmZ{hO^6$10EV-z;4%n`2urCfIjIoRe3SWRW~YaRWHy`0%qElU>7ea~0+4rLF#jz1{iWx6PwdLHE#gBVmiO$fin< zifV2!U7^`X7FRJ9W5);Gk*mu#>p^rg29PkY!zY@$uDL@KhV4^3ILkLi@Hjw3JDCLX}}~5#UvL1rt5%& zkW^3;ha^d2xT#|%L7*g6R22#m0B8%)phy(4l??{($$G8HHL6QS%uA|nyPTXxP(k4+ zr{rwZob;NNiW8b+u1OV398;)UPLvoh9EM1#G|-E=TwE$Moys7s!W6H0b}rr+m3hM} z1e2;LFSiDxZim((T~_^Nc{&ZO2`uv?E5}C&%G6i;QBobTlG$GcMuBWeWpvoKOnE5O zp{qy(DEH%%T>wZvpU>J(PvRg{J8fNoP9aA#G(Dmke8H8%s?50fLKrR$fU&G=2nXiB zWd1rid(VI8((6C$A3c8xyYkpx8qmf3Kl88tc`?5G@#XhCWan@8ixa263a`)2UOd;& ze}msxyU@P20^a}W$G6vN>CLClewEI?tlj?h0zz1>ZX9fyAvu78W!a=MVi3qirV%)0PgP*MkMT>-t{1KGycyVeopr>#mQxuDw1p znL08@Mu-SXgG&$w`(xlH3L$PGK5#l5*~DRDl*g2TbIIHUl&E94@W0=GU%ua8-}lv7 zv|y3sv2qegamPtXI7o?1gvrrVyr`(yUbny0T&imYyHB-^4Yd8U>4~f>7y)-NIkU=7 zn)8$wOPRt#wq9#a`AH`ffXs;{&-o2{!0#2K3~5DZ(i8Wo#HHMU1j9ORiOmHdPs*<~ zA=XAbV>JTHil`8yV0b!_i;JAVZxIq9Wg3(!w#&q*Ku2UC001&r6%1ej3I!N|rU4dV zC>GTKP%sFfYBfrMEUZRR6oT0x#3BfYVyXazsSyC6HEIS!L8${9uB$z~tr zv!&s<$^e=SAf-`8iELiXSx{Qz26;_A<%L32N~g+bRG9?r(OLLrDZyh_i!qCHfKacu z2i+hGpoD{W`h1+lnRMc4AP|oc;S3?@O)6HjyX~=nQ7xohpdaIrG%oef8QdNR8{IAv z&xkgMXq5AeEwfY_V)tX(d|iEGoF&Y9oQy~n3WbbwCj&&PE=;+a^t=hB z=RW=(zf!&J<}WyK2O5Pho;lW3VHiKsI)9_EIAYRQJGggU5Mmn^Fq@sE#fl|jG8M$hftXMVcS zd*Jf)bNyEeKM#Itn2zMpo%{W{+0WBNM!n~fEfV>g_@sYf;;%hJcRPpoMIYX-{`R-| z^W0POC+`2rEDrp!@>I^(`{Jtdx4)F!dlPB$atq?nH^%6!uwl6Cq4s#bFg+>$SIVI=`rP)Mdx)uZEAR5?I17`O)q=X1sPAhg@vRSZ8qC?Ac6GohCwcv~=~^ z9qq(S)5W{Xe7#}KA@%6HV(vP*VP;)P_tpoyD?Zvab>urGH?eZ;z~Vc}&X;a=RXShT geRgg8W=>X7{d~uecWuqYwYP4s=}_PJYVx!H0Hb%48UO$Q literal 0 HcmV?d00001 diff --git a/testdata/fixtures/time_and_key_portable.dkk b/testdata/fixtures/time_and_key_portable.dkk new file mode 100644 index 0000000000000000000000000000000000000000..08166da12dc4d6bc0b2dcf089e16116f72a88c91 GIT binary patch literal 152 zcmZ?q_BLc>0E3?848P7) zEpvcNpRkv(YmMQY)%EAr*Z-1dNvkk2H8r$kjZm0unoxxSP2!THKm2L z7H~=oRp3Z=6x#h^j%V1y6xLq=1R-Ja2O{3f+C3r&1R)U$(>AG%!a)aUw}Vd1K{;^= zh|n~Mi^VtvIZ-i=;|S`c5hsR0xD>WgQZWXCc8S;_#UWuphU#-D5>%2{m~@4mc&?Al z(VGbcD1+@0O`4SnBZShX1|q5=NFP+AdNey%jR#a&kdLy9147b>DIlHOmIa#(pkRsW#FB>QNnnoLe7Zu8KLy_ke9=f z6aZVq5!y(ogk~c}8dgf@Qkr5wUXtlGNF~DLWvBheSqxv#rNC0zdYObJo=63o?y;>dE>c` znkf2C$7Y~w`|T*PyX5m%Pjwvx7T0H$HasInjsk1AhW20ZTJYQZdD0sxccQ7sgjfGK z(u7v6e5(30bH?MPszb=wNZhqD!hY@VZqxSqWnV1oVmj~B&DiX=JGEe_w&on$yYuI! z`1sk|E0j-Kj(#$>b6)gK&3Dh(Md^u+)9?Ca>J@uSD~=~JwO4Ni z2FxRhx#I)?@?NE8G%^0< zNONn0=Lk3BTpwFE?cmh>ttrFxS8ST0P3`vHc_7 zo`3rQBrrJ}TY#>*s)>gKJ!k7{&TP4!-}q$lnu{B5wZyI~_x@|}j9waxt*gBF(`!8s zmFt`DsDCZ=oL>HE<=}S)>SBXfl(BaiX0)Sn$v@6N|VxOws2O0OfNS2 zbs!mrJw|tgbJ3IEEwbCBwm*s3-US;_XixX#3n~0I%$zNNhKWBRqv74T#tlS zjotX|h`F?1b@+JpXS3#TOUlK!)*gJ6F>iHM!OZ=qmqv<*uXed!f{t~P_SV{#Uht5v Rvs(XjUDN8zSD^S&@;_(Z`y>DW literal 0 HcmV?d00001 diff --git a/testdata/fixtures/time_and_key_recipients.dkk b/testdata/fixtures/time_and_key_recipients.dkk new file mode 100644 index 0000000000000000000000000000000000000000..c38ea6d389497f4722fff302f2ebc00f174a9677 GIT binary patch literal 152 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 DF^pekRH@BQ0O^xt^N}gc9H!FQPA5(vrT-lo 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

Hso>gTk!1PmLZ`_$sp;ckDTJW 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 Sxs~ndjmzrl!t%-?Kl~3NH9wXB literal 0 HcmV?d00001 diff --git a/testdata/fixtures/time_only_extensions.json b/testdata/fixtures/time_only_extensions.json new file mode 100644 index 0000000..7deb42e --- /dev/null +++ b/testdata/fixtures/time_only_extensions.json @@ -0,0 +1,137 @@ +{ + "description": "time_only capsule with a noncritical PUBLIC_HEADER extension and a noncritical CONTROL_CBOR extension", + "spec": "0.8.1", + "file": "time_only_extensions.dkc", + "sha256": "1e7effd58016d9447f9a2e7c9645c658604979c4e35af675d1dc6c4ae12ac444", + "release": { + "round": 2000, + "signature": "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e" + }, + "prelude": "444b4331010000000000009f000001e2", + "public_header": "a6006a646174656b657963617001010250d2fd9af55dc0253132fb36b8dc0d016b037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d6a41774d483004000681a300716f72672e6578616d706c652e6c6162656c0101026c7075626c6963206c6162656c", + "datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MjAwMH0", + "capsule_id": "d2fd9af55dc0253132fb36b8dc0d016b", + "access_policy": "time_only", + "structure": "time_only", + "unlock_at": "2023-08-23T16:49:24Z", + "header_binding": "a4fa1e3c5a47a01313173cdf23df157ff8c949011df95dde8f092ff6749dc154", + "outer_stanzas": [ + { + "type": "tlock", + "args": [ + "2000", + "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + ] + } + ], + "payload_stanzas": [ + { + "type": "X25519", + "args": [ + "pUUIuCTHAZ3+kpMwJQQ9dc3EJO1zPagucu+VdFovSy0" + ] + } + ], + "control_cbor": "a50070646174656b6579732d636f6e74726f6c0101025820a4fa1e3c5a47a01313173cdf23df157ff8c949011df95dde8f092ff6749dc1540358201b27d7029c50dc20ef1f9269a1a944a1384f526f9ea76cdc9c76a6456bb1fbb80581a300706f72672e6578616d706c652e6e6f7465010202a2616e07667365616c6564f5", + "payload_identity": "1b27d7029c50dc20ef1f9269a1a944a1384f526f9ea76cdc9c76a6456bb1fbb8", + "plaintext_file": "time_only_extensions.plaintext", + "plaintext_sha256": "1129768e195e2f1e50b7a6f926b6eebef120212c29b5642c8a662c503b2a9131", + "header_extensions": [ + { + "critical": false, + "id": "org.example.label", + "version": 1, + "data": "6c7075626c6963206c6162656c" + } + ], + "control_extensions": [ + { + "critical": false, + "id": "org.example.note", + "version": 2, + "data": "a2616e07667365616c6564f5" + } + ], + "stages": [ + { + "step": 1, + "name": "parse DKC1", + "ok": true + }, + { + "step": 2, + "name": "prelude", + "ok": true + }, + { + "step": 3, + "name": "public header", + "ok": true + }, + { + "step": 4, + "name": "header validation", + "ok": true + }, + { + "step": 5, + "name": "sealed control structure", + "ok": true + }, + { + "step": 6, + "name": "payload structure", + "ok": true + }, + { + "step": 7, + "name": "condition", + "ok": true + }, + { + "step": 8, + "name": "tlock stanza", + "ok": true + }, + { + "step": 9, + "name": "release", + "ok": true + }, + { + "step": 10, + "name": "release verification", + "ok": true + }, + { + "step": 11, + "name": "open sealed control", + "ok": true + }, + { + "step": 12, + "name": "policy structure", + "ok": true + }, + { + "step": 14, + "name": "control", + "ok": true + }, + { + "step": 15, + "name": "header binding", + "ok": true + }, + { + "step": 16, + "name": "payload identity", + "ok": true + }, + { + "step": 18, + "name": "commit", + "ok": true + } + ] +} diff --git a/testdata/fixtures/time_only_extensions.plaintext b/testdata/fixtures/time_only_extensions.plaintext new file mode 100644 index 0000000..9bab1c0 --- /dev/null +++ b/testdata/fixtures/time_only_extensions.plaintext @@ -0,0 +1 @@ +DateKeys fixture with extensions. diff --git a/testdata/vectors/dk1.json b/testdata/vectors/dk1.json new file mode 100644 index 0000000..a3655d2 --- /dev/null +++ b/testdata/vectors/dk1.json @@ -0,0 +1,153 @@ +{ + "spec": "0.8.1", + "description": "Canonical dk1_ strings and rejected encodings (spec §18, §19, §66), generated by the reference implementation.", + "vectors": [ + { + "name": "round 1", + "network": "datekeys:quicknet:v1", + "round": 1, + "canonical_json": "{\"version\":1,\"network\":\"datekeys:quicknet:v1\",\"round\":1}", + "base64url": "eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MX0", + "dk1": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MX0" + }, + { + "name": "round 1000", + "network": "datekeys:quicknet:v1", + "round": 1000, + "canonical_json": "{\"version\":1,\"network\":\"datekeys:quicknet:v1\",\"round\":1000}", + "base64url": "eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0", + "dk1": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0" + }, + { + "name": "normative 2030-01-01 round", + "network": "datekeys:quicknet:v1", + "round": 66884212, + "canonical_json": "{\"version\":1,\"network\":\"datekeys:quicknet:v1\",\"round\":66884212}", + "base64url": "eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6NjY4ODQyMTJ9", + "dk1": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6NjY4ODQyMTJ9" + }, + { + "name": "last Quicknet round", + "network": "datekeys:quicknet:v1", + "round": 83903165811, + "canonical_json": "{\"version\":1,\"network\":\"datekeys:quicknet:v1\",\"round\":83903165811}", + "base64url": "eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6ODM5MDMxNjU4MTF9", + "dk1": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6ODM5MDMxNjU4MTF9" + }, + { + "name": "whitespace in JSON", + "input": "dk1_eyJ2ZXJzaW9uIjogMSwgIm5ldHdvcmsiOiAiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCAicm91bmQiOiA2Njg4NDIxMn0", + "error": "ERR_DATEKEY_NON_CANONICAL" + }, + { + "name": "keys reordered", + "input": "dk1_eyJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJ2ZXJzaW9uIjoxLCJyb3VuZCI6NjY4ODQyMTJ9", + "error": "ERR_DATEKEY_NON_CANONICAL" + }, + { + "name": "trailing whitespace", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6NjY4ODQyMTJ9Cg", + "error": "ERR_DATEKEY_NON_CANONICAL" + }, + { + "name": "exponent notation", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6Ni42ODg0MjEyZTd9", + "error": "ERR_DATEKEY_NON_CANONICAL" + }, + { + "name": "fraction notation", + "input": "dk1_eyJ2ZXJzaW9uIjoxLjAsIm5ldHdvcmsiOiJkYXRla2V5czpxdWlja25ldDp2MSIsInJvdW5kIjo2Njg4NDIxMn0", + "error": "ERR_DATEKEY_NON_CANONICAL" + }, + { + "name": "escaped character", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXNcdTAwM2FxdWlja25ldDp2MSIsInJvdW5kIjo2Njg4NDIxMn0", + "error": "ERR_DATEKEY_NON_CANONICAL" + }, + { + "name": "duplicate key", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MSwicm91bmQiOjY2ODg0MjEyfQ", + "error": "ERR_DATEKEY_NON_CANONICAL" + }, + { + "name": "padded Base64URL", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0=", + "error": "ERR_DATEKEY_NON_CANONICAL" + }, + { + "name": "non-zero trailing bits", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH1", + "error": "ERR_DATEKEY_NON_CANONICAL" + }, + { + "name": "extra field", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6NjY4ODQyMTIsInB1YmxpY19rZXkiOiIwMCJ9", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "missing field", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEifQ", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "version 2", + "input": "dk1_eyJ2ZXJzaW9uIjoyLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6NjY4ODQyMTJ9", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "round 0", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MH0", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "negative round", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6LTF9", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "fractional round", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MS41fQ", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "round above 2^53-1", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6OTAwNzE5OTI1NDc0MDk5Mn0", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "round as string", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6IjY2ODg0MjEyIn0", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "uppercase network", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiREFURUtFWVM6UVVJQ0tORVQ6VjEiLCJyb3VuZCI6NjY4ODQyMTJ9", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "trailing data", + "input": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6NjY4ODQyMTJ9eA", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "byte order mark", + "input": "dk1_77u_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6NjY4ODQyMTJ9", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "not Base64", + "input": "dk1_!!!", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "missing prefix", + "input": "eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6NjY4ODQyMTJ9", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "uppercase prefix", + "input": "DK1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6NjY4ODQyMTJ9", + "error": "ERR_DATEKEY_INVALID" + } + ] +} diff --git a/testdata/vectors/profile_quicknet.json b/testdata/vectors/profile_quicknet.json new file mode 100644 index 0000000..4fa8959 --- /dev/null +++ b/testdata/vectors/profile_quicknet.json @@ -0,0 +1,15 @@ +{ + "spec": "0.8.1", + "description": "Quicknet Provider Profile V1: exact Deterministic CBOR and profile_hash (spec §11, §12, §75 item 2), generated by the reference implementation.", + "profile_id": "datekeys:quicknet:v1", + "provider": "drand", + "network": "quicknet", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971", + "public_key": "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", + "period_seconds": 3, + "genesis_time": 1692803367, + "genesis_seed": "f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e", + "scheme": "bls-unchained-g1-rfc9380", + "canonical_cbor": "ab007819646174656b6579732d70726f76696465722d70726f66696c6501010274646174656b6579733a717569636b6e65743a763103656472616e640468717569636b6e657405582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e97106586083cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a0703081a64e62127097818626c732d756e636861696e65642d67312d726663393338300a5820f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e", + "profile_hash": "4147645109798ecbc9f630c2f709835bb5846fe7911a6bd5c5755e25ade2ada4" +} diff --git a/testdata/vectors/quicknet_rounds.json b/testdata/vectors/quicknet_rounds.json new file mode 100644 index 0000000..f631d03 --- /dev/null +++ b/testdata/vectors/quicknet_rounds.json @@ -0,0 +1,107 @@ +{ + "spec": "0.8.1", + "profile": "datekeys:quicknet:v1", + "description": "Quicknet date to round resolution (spec §15, §16, §65), generated by the reference implementation.", + "vectors": [ + { + "name": "genesis exactly: round 1", + "requested": "2023-08-23T15:09:27Z", + "round": 1, + "effective": "2023-08-23T15:09:27Z" + }, + { + "name": "genesis + 1ns: next round", + "requested": "2023-08-23T15:09:27.000000001Z", + "round": 2, + "effective": "2023-08-23T15:09:30Z" + }, + { + "name": "genesis + 1s", + "requested": "2023-08-23T15:09:28Z", + "round": 2, + "effective": "2023-08-23T15:09:30Z" + }, + { + "name": "genesis + one period: round 2", + "requested": "2023-08-23T15:09:30Z", + "round": 2, + "effective": "2023-08-23T15:09:30Z" + }, + { + "name": "genesis + one period + 1ns: round 3", + "requested": "2023-08-23T15:09:30.000000001Z", + "round": 3, + "effective": "2023-08-23T15:09:33Z" + }, + { + "name": "genesis - 1s: before the profile", + "requested": "2023-08-23T15:09:26Z", + "error": "ERR_DATEKEY_INVALID" + }, + { + "name": "round 1000 boundary exactly", + "requested": "2023-08-23T15:59:24Z", + "round": 1000, + "effective": "2023-08-23T15:59:24Z" + }, + { + "name": "one second before the round 1000 boundary", + "requested": "2023-08-23T15:59:23Z", + "round": 1000, + "effective": "2023-08-23T15:59:24Z" + }, + { + "name": "one second after the round 1000 boundary", + "requested": "2023-08-23T15:59:25Z", + "round": 1001, + "effective": "2023-08-23T15:59:27Z" + }, + { + "name": "1ns after the round 1000 boundary", + "requested": "2023-08-23T15:59:24.000000001Z", + "round": 1001, + "effective": "2023-08-23T15:59:27Z" + }, + { + "name": "half a second after the round 1000 boundary", + "requested": "2023-08-23T15:59:24.5Z", + "round": 1001, + "effective": "2023-08-23T15:59:27Z" + }, + { + "name": "normative vector 2030-01-01 (spec §16)", + "requested": "2030-01-01T00:00:00Z", + "round": 66884212, + "effective": "2030-01-01T00:00:00Z" + }, + { + "name": "1ns after 2030-01-01", + "requested": "2030-01-01T00:00:00.000000001Z", + "round": 66884213, + "effective": "2030-01-01T00:00:03Z" + }, + { + "name": "normative vector round 66432123 (spec §16)", + "requested": "2029-12-16T07:15:33Z", + "round": 66432123, + "effective": "2029-12-16T07:15:33Z" + }, + { + "name": "offset timezone equals UTC instant", + "requested": "2026-10-22T19:00:00.001+02:00", + "round": 33295013, + "effective": "2026-10-22T17:00:03Z" + }, + { + "name": "last representable round time", + "requested": "9999-12-31T23:59:57Z", + "round": 83903165811, + "effective": "9999-12-31T23:59:57Z" + }, + { + "name": "after the last representable round", + "requested": "9999-12-31T23:59:59Z", + "error": "ERR_DATEKEY_INVALID" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..75452c3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2024", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +}