Specification 0.16 draft: a seal without accuracy, drand's JSON read strictly

The draft v0.16 of datekeys-go at 4f78854 (branch v0.16): SPEC_VERSION
0.16, and testdata, wordlists and annex synced from that commit. The
annex is §79 of the draft, with the CC BY-ND 4.0 license of the
specification in its title and the key of words in 79.7.

A seal without accuracy proves nothing before the opening date (§29.7,
§29.11, as 7e3b810): a valid seal is S4 only when its token carries
accuracy and t plus the accuracy is before round_time; otherwise S5,
with the first reason that holds: late, no accuracy under the BTSP
policy of ETSI EN 319 421 (0.4.0.2023.1.1), or no accuracy. cms.ts reads
hasAccuracy and the policy of the token (tokenIsBTSP); securitycms.ts
gives SealReason, Detail.sealReason and SignerLine.reason; S5 has no
fixed text any more, and verdictLines writes it and the line of a signer
of F6 with the reason, the texts of Go byte for byte (sealReasonText).
encryptFiles returns the verdicts of the area it wrote in
Encrypted.security, as Result.Security of Go, so that a writer warns of
a seal without accuracy (§62.1 rule 19).

drand's JSON is read strictly (§47.1, as b570338): parseDrandJSON, as
ParseDrandJSON of Go, reads RFC 8259 JSON in valid UTF-8 whose value is
an object, with no name repeated in any object, names compared exactly
once their escapes are decoded, a lone escaped surrogate malformed, the
round a number without sign, fraction or exponent from 1 to 2^53 - 1,
and signature and randomness strings, with the error texts of Go.
ParsedRelease is now a Release: no round above 2^53 - 1 is read. The
page reads the answers of the relays with it (drand.ts), as the client
of Go does, and the pasted release with strictJSON and jsonRound
(release-input.ts), so that it never reads another round than step 10.

Tests: security_cms.json with seal_reason (143 cases), the 38 JSON
inputs of release.json, the new cases of signature2_test.go and
drandjson_test.go (with the escapes written as escapes), and the new
fixtures: format3_time_and_key_words opens with the identity that the
words of its words_text give with normalizeWords and wordKey, in the
library and in the page, and format3_full_chunk, whose PAYLOAD_AGE ends
in a full STREAM chunk, opens. check-build.mjs counts words_text among
the secrets of the fixtures.

Reference files made again with Go at 4f78854: mutation-texts.json (its
spec field only), ibe-vectors.json (the two new fixtures, the rest
unchanged) and signing-vectors.json, in an export of 4f78854 with the
same frozen samples read again: the capsules are the same, and the
tokens of the sealer, without accuracy, now give S5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
dev 5 hours ago
parent e8b5d35858
commit f79d8e2de8

@ -4,6 +4,17 @@ Cambios notables de la librería TypeScript y de la página. El proyecto usa ver
## 0.5.0 — sin publicar
### El borrador de la especificación 0.16 (07-10-2026)
El borrador v0.16 de la rama `v0.16` de `datekeys-go`, en `4f78854`, aún sin aprobar: lo que encontró la revisión de Astra de la v0.15. No cambia ningún formato; cambian el veredicto de un sello sin `accuracy` y la lectura del JSON de drand (§76, «Cambios normativos de la v0.16»).
- `SPEC_VERSION` pasa a `0.16`. `testdata`, `wordlists` y `annex` se sincronizan con `4f78854` (`node scripts/sync-testdata.mjs sync --commit 4f78854`): el campo `spec` de cada fichero dice `0.16`; `vectors/security_cms.json` se hizo de nuevo, con 143 casos y `seal_reason`; `vectors/release.json` gana 26 casos del JSON de drand; `vectors/wordkey.json`, el texto del vector del anexo, también con sus marcas sueltas, y su llave; y llegan dos fixtures, `format3_time_and_key_words` y `format3_full_chunk`. El anexo de recuperación es el §79 del borrador v0.16, con la licencia de la especificación, CC BY-ND 4.0, en su título (`70d907b`) y la llave de palabras en 79.7.
- **El sello sin `accuracy`** (§29.7, §29.11, como `7e3b810` de Go). Un sello válido es S4 solo si el token lleva `accuracy` y t más la precisión es anterior a `round_time`; si no, S5, con el primer motivo que se cumple: `late`, sellado después o demasiado cerca; `no accuracy, BTSP`, sin `accuracy` en un token de la política BTSP de ETSI EN 319 421 (0.4.0.2023.1.1), que la exige; o `no accuracy`. Una `accuracy` de 0 segundos, o vacía, es una precisión de 0 y vale. `cms.ts`: el token gana `hasAccuracy` y `policy`, y `tokenIsBTSP`. `securitycms.ts`: `SealReason`, `Detail.sealReason` y `SignerLine.reason`. `security.ts`: S5 ya no tiene texto fijo; `verdictLines` escribe «No acredita que se sellara antes de la fecha de apertura: ‹motivo›.», y la línea de un firmante de F6 cuyo sello no lo acredita, «sin acreditar que fuera antes de la fecha de apertura: ‹motivo›», con los textos de `sealReasonText`, los de Go byte a byte.
- **El escritor avisa** (§62.1, regla 19). `encryptFiles` devuelve en `Encrypted.security` los veredictos del área que escribió, como `Result.Security` de Go: un sello sin `accuracy` se escribe, y quien escribe ve su S5, o la línea de su firmante, con el motivo. `/create` no sella, así que no cambia.
- **El JSON de drand, estricto** (§47.1, como `b570338` de Go). `releaseobject.ts` cambia el lector que imitaba `encoding/json` por `parseDrandJSON`, como `ParseDrandJSON` de Go: JSON de RFC 8259 en UTF-8 válido cuyo valor es un objeto, ningún nombre repetido en ningún objeto, nombres comparados exactos tras decodificar sus escapes (`"round"` es `round`, y `ROUND` otro nombre), un sustituto escapado sin su pareja es JSON mal formado, `round` es un número sin signo, fracción ni exponente de 1 a 2⁵³ − 1, `signature` y `randomness` son cadenas, y `randomness`, si viene, aunque sea vacía, es el SHA-256 de la firma. Los textos de error no cambian. Como la ronda ya no pasa de 2⁵³ − 1, `ParsedRelease` es un `Release`, sin `bigint`. `drand.ts` lee las respuestas de los relays con `parseDrandJSON`, como el cliente de Go, y `release-input.ts`, lo pegado, con `strictJSON` y `jsonRound`, para que la página no lea otra ronda que el paso 10.
- **Las pruebas.** `vectors.test.ts` compara `seal_reason` en cada caso de `security_cms.json` y en cada firmante, y corre los 38 JSON de `release.json`. `securitycms.test.ts` lleva los casos nuevos de `signature2_test.go` (sin `accuracy` años antes y después, BTSP con ella y sin ella, una `accuracy` de 0 segundos y vacía, y un firmante cuyo sello no la lleva), y `releaseobject.test.ts`, los de `drandjson_test.go`, con los escapes escritos como escapes, que el generador de `release.json` escribió sin escapar. `format3_time_and_key_words` abre con la identidad que dan las palabras de su `words_text` con `normalizeWords` y `wordKey`, en la librería y en la página, y `format3_full_chunk`, cuyo `PAYLOAD_AGE` acaba en un trozo completo, abre a su fichero. `check-build.mjs` cuenta `words_text` entre los secretos de los fixtures.
- **Los ficheros de `testing/` generados con Go**, de nuevo con `4f78854`: `mutation-texts.json` (solo cambia su campo `spec`); `ibe-vectors.json`, con los dos fixtures nuevos y el resto idéntico; y `signing-vectors.json`, en una exportación de `4f78854`, con las mismas cinco muestras, cuyo `ts` se volvió a leer: las cápsulas salen iguales, y como los tokens del sellador no llevan `accuracy`, sus sellos dan ahora S5.
### Las palabras al azar de la llave de palabras (07-10-2026)
El SHOULD de §38.1, ofrecer palabras al azar de una lista pública, como hace `datekeys encrypt -new-words` desde `c49c67c` de `datekeys-go`. No cambia ningún formato ni la derivación.

@ -21,12 +21,12 @@ Hay tres números de versión, cada uno con su significado, como en la referenci
| Versión | Dónde | Cambia cuando |
|---|---|---|
| Formato | Dentro de los objetos: el formato de la cápsula, el `VERSION` del prelude de DKC1, 1, 2 o 3 al leer, que fija también la versión de schema de CONTROL_CBOR; y 1 en la trama DKK1 y en el schema de los demás objetos | Cambia el formato. Un lector rechaza una versión que no conoce (§22, §70) |
| Especificación | `SPEC_VERSION` de `src/lib/dkc/version.ts`, hoy `0.15`: la del tag `spec-v0.15` de `datekeys-go`, que aprobó el autor el 7 de octubre de 2026. `testdata` está en ese tag (`fe50885`), y todos sus ficheros dicen `0.15` | Cambia el texto normativo |
| Especificación | `SPEC_VERSION` de `src/lib/dkc/version.ts`, hoy `0.16`: el borrador v0.16 de la rama `v0.16` de `datekeys-go`, aún sin aprobar, en `4f78854`. `testdata` está en ese commit, y todos sus ficheros dicen `0.16` | Cambia el texto normativo |
| Librería | `VERSION` de `src/lib/dkc/version.ts`, igual al campo `version` de `package.json` | Cambia la API o el comportamiento. Versionado semántico, sin promesa de estabilidad antes de 1.0.0 |
`version.test.ts` comprueba que `VERSION` coincide con `package.json` y con su lockfile, y que `SPEC_VERSION` es la versión que nombran los vectores y fixtures compartidos; `vectors.test.ts` exige esa versión a cada fichero. El pie de la página muestra las dos.
La versión en desarrollo es `0.5.0-dev`. La última publicada es la `0.4.0`, del 7 de octubre de 2026, con el tag `v0.4.0`, que cubre:
La versión en desarrollo es `0.5.0-dev`, que sigue el borrador v0.16: el sello sin `accuracy` no prueba nada antes de la fecha de apertura, y el JSON de drand se lee estricto. La última publicada es la `0.4.0`, del 7 de octubre de 2026, con el tag `v0.4.0`, que cubre:
- la especificación 0.15 (el tag `spec-v0.15` de `datekeys-go`): lee los formatos de cápsula 1 a 3 y escribe el 3, y el 2 solo como generador de vectores (§62.1, regla 1);
- el objeto release y el release en la mano (§47.1, §49, §50, §63 pasos 9.c y 10): lee el objeto release y el JSON de drand con los textos de Go, busca la ronda en un archivo de releases local y abre con un release en la mano aunque el reloj vaya atrasado; `/inspect` acepta el release pegado o en un fichero;
- la firma de autor de `alg` 1, con las claves `dkauthor1…`, y la de `alg` 2 con certificados, y el sello RFC 3161: las evalúa al abrir con los veredictos de Go y las escribe con los enganches del escritor;
@ -57,9 +57,9 @@ La inspección (pasos 1 a 8) no importa ninguna dependencia. Funciona en navegad
| `bls12381.ts` | Pertenencia de claves públicas BLS12-381 comprimidas (G1 y G2) al subgrupo, como `FromCompressed` de kilic | `kyber-bls12381` |
| `ibe.ts` | IBE-CCA de tlock sobre G2 para Quicknet (§63 paso 11): `decryptOnG2` y `encryptOnG2RFC9380` (Qid = H(id) en G1 con el DST de RFC 9380, sigma aleatorio, U = r·G2), con la puerta de codificación canónica de `bls12381.ts` sobre la firma y U; H2 sobre GT serializado en el orden de kilic (nunca `Fp12.toBytes` de noble), H3 (`h3Base` y `h3Try`, que desplaza el primer byte de cada intento un bit a la derecha, como kyber) y H4; `roundIdentity` y `hashToG1`, el hash a G1 de RFC 9380 que usan también `release.ts` y el cifrado; el cuerpo `U ‖ V ‖ W` de 128 bytes del stanza. Errores `IbeError` con motivo (`length`, `encoding`, `identity`, `proof`) y texto fijos, sin ningún valor del cálculo; borra sigma y los hashes derivados. Sobre `@noble/curves` 2.4.0; lleva el aviso MIT de `tlock-js`, cuya estructura sigue. Lo usa la apertura (`open.ts`) | `encrypt/ibe` de drand/kyber (`DecryptCCAonG2`), `tlock.BytesToCiphertext` y `TimeUnlock` |
| `release.ts` | Verificación local del release (§17, §51, §63 paso 10), en el orden y con los textos de `provider.Verify`:<br>1. el rango de la ronda (`ERR_DATEKEY_INVALID`);<br>2. la ronda del release antes que la firma (`ERR_ROUND_MISMATCH`);<br>3. la longitud de la firma;<br>4. la clave pinneada (`ERR_UNKNOWN_PROFILE`);<br>5. la firma: codificación canónica de un punto de G1 que no sea el infinito, y firma BLS válida de la ronda sobre `@noble/curves` 2.4.0, con el DST de RFC 9380 para G1 (`ERR_RELEASE_INVALID`).<br>Nada de noble se copia a los errores. Solo verifica el scheme de Quicknet, el único que admite un perfil desde la v0.14: un `Profile` de otro scheme, que `validateProfile` rechaza, falla aquí con `ERR_UNKNOWN_PROFILE` tras las comprobaciones de ronda (decisión 3 del plan de la fase 2). También define `ReleaseSource`, con su contrato de fuentes de red y de la corrección 6, y `suppliedRelease`, el release que entrega quien llama. Desde la v0.15, antes de la ronda compara la cadena que nombra el release, si nombra una, con la del perfil fijado (`ERR_PROFILE_MISMATCH`), y reexporta `releaseobject.ts` | `provider` (`Verify`, `ReleaseSource`) |
| `releaseobject.ts` | El objeto release de la v0.15 (§47.1), sin noble, como `provider/release.go` y `provider/archive.go` de Go, con sus textos byte a byte:<br>- `encodeRelease`, `newReleaseObject` y `decodeRelease`, con las capas del paso 10: el tamaño, de 1 a 1024 bytes, antes de decodificar; el tipo y la versión; el schema (`ERR_NON_CANONICAL_CBOR` o `ERR_UNSUPPORTED_VERSION`);<br>- `parseRelease`, que lee además el JSON de drand cuando su primer byte que no es un espacio es `{`, como lo lee `encoding/json` de Go en su struct: claves sin distinguir mayúsculas, la última gana, `null` deja el campo sin poner, una ronda que no es un entero de 0 a 2⁶⁴ − 1 o un campo de otro tipo hacen fallar la entrada, y una ronda por encima de 2⁵³ − 1 se guarda como `bigint` para el texto de `ERR_ROUND_MISMATCH`; todo fallo es `ERR_RELEASE_INVALID`;<br>- `ReleaseSupplier`, el release en la mano (`provider.Supplier`), y `encodedRelease`;<br>- `ReleaseArchive`, el archivo de releases local, formato informativo de §50: de un `Blob` lee solo la cabecera y una firma, y cada fallo, una ronda a ceros incluida, es `ERR_RELEASE_UNAVAILABLE` | `provider` (`EncodeRelease`, `DecodeRelease`, `ParseRelease`, `Supplier`, `Encoded`, `Archive`) |
| `releaseobject.ts` | El objeto release de la v0.15 (§47.1), sin noble, como `provider/release.go`, `provider/drandjson.go` y `provider/archive.go` de Go, con sus textos byte a byte:<br>- `encodeRelease`, `newReleaseObject` y `decodeRelease`, con las capas del paso 10: el tamaño, de 1 a 1024 bytes, antes de decodificar; el tipo y la versión; el schema (`ERR_NON_CANONICAL_CBOR` o `ERR_UNSUPPORTED_VERSION`);<br>- `parseRelease`, que lee además el JSON de drand cuando su primer byte que no es un espacio es `{`, con `parseDrandJSON`, el lector estricto de la v0.16, como `ParseDrandJSON` de Go: como mucho 8 KiB de JSON de RFC 8259 en UTF-8 válido cuyo valor es un objeto; ningún objeto repite un nombre, y los nombres se comparan exactos, punto de código a punto de código, tras decodificar sus escapes, así que `"round"` es `round` y `ROUND` otro nombre, que se ignora; el escape de un sustituto sin su pareja es JSON mal formado; `round` es un número sin signo, fracción ni exponente, de 1 a 2⁵³ − 1; `signature` y `randomness` son cadenas, y `randomness`, si viene, el SHA-256 de la firma en hexadecimal, en cualquier caja; todo fallo es `ERR_RELEASE_INVALID`. `strictJSON` y `jsonRound` son el lector y la ronda, que usan también `drand.ts` y `release-input.ts` de la página;<br>- `ReleaseSupplier`, el release en la mano (`provider.Supplier`), y `encodedRelease`;<br>- `ReleaseArchive`, el archivo de releases local, formato informativo de §50: de un `Blob` lee solo la cabecera y una firma, y cada fallo, una ronda a ceros incluida, es `ERR_RELEASE_UNAVAILABLE` | `provider` (`EncodeRelease`, `DecodeRelease`, `ParseRelease`, `ParseDrandJSON`, `Supplier`, `Encoded`, `Archive`) |
| `open.ts` | Los pasos 9 a 18 de §63 sobre los pasos 1 a 8 de `inspectWith`, con los checks, códigos y textos de `capsule.Open`:<br>- las credenciales y el release (paso 9), que cualquier fallo de la fuente convierte en `ERR_RELEASE_UNAVAILABLE` (corrección 6). El release llega de `source`, una fuente de red, a la que no se pide nada antes de `round_time`, o de `release`, un release en la mano (v0.15), que no se compara con el reloj: `Opened.clockBehind` dice si el reloj iba por detrás, y el paso 10 empieza por decodificarlo;<br>- la verificación del release (10);<br>- `OUTER_TIME_AGE` (11), la estructura frente a `access_policy` (12) e `INNER_ACCESS_AGE` (13);<br>- `CONTROL_CBOR` (14), `header_binding` (15), `I_PAYLOAD` (16), `PAYLOAD_AGE` (17) y el commit (18).<br>Lee los dos formatos (§22, §70). En el formato 2, `INNER_ACCESS_AGE` tiene exactamente 16 stanzas (paso 12); `CONTROL_CBOR` es de la versión de schema 2, con L y la regla de relleno (14); el paso 16 calcula P, y el 17 exige un texto en claro de exactamente P bytes con ceros tras el contenido, `ERR_INTEGRITY` en otro caso. Solo se entregan los L primeros bytes, nunca el relleno (§29.1, §56). `Opened` da el formato y, en los formatos 2 y 3, L, la regla y P.<br>En el formato 3, el paso 17 lo hace `open3.ts`, y los ficheros van a `sink`; sin él, `open` rechaza con un `TypeError` justo tras el paso 2, antes de pedir nada, como `ErrSinkRequired`. `Opened` da entonces el head, los veredictos del área de seguridad y el tamaño del área.<br>Abre los tres ficheros `age` con el `Decrypter` de `age-encryption` y con identidades propias que aplican las reglas de `agewrap`: la de tiempo, sobre `ibe.ts`; las de acceso y payload, sobre `x25519.ts`, stanza a stanza. Los fallos de `age` que no informa una identidad son `ERR_INTEGRITY` con el motivo fijo de su fase, cabecera o STREAM, sin copiar el texto de `age-encryption`.<br>La entrada puede ser un `Uint8Array` o un `Blob`, como un `File`. De un `Blob` solo se lee el prefijo de los pasos 1 a 8 (`prefix.ts`), el `capsule_digest` de la `.dkk` se calcula sobre su stream (`digest.ts`) y `PAYLOAD_AGE` se descifra en streaming.<br>El texto en claro va a memoria o a `output`, un `WritableStream`. Se escribe a medida que `age` autentica cada chunk, se cierra solo tras el paso 18 y se aborta ante cualquier fallo, en cualquier paso (§56). Un fallo del stream de salida es `ERR_INTEGRITY` con su texto, como en Go. El `WritableStream` de un fichero OPFS guarda lo escrito en un fichero de intercambio hasta el cierre: comprobado en el navegador, un fallo de STREAM deja intacto el contenido anterior | `capsule.Open`, `agewrap` (`TimeIdentity`, `AccessIdentity`, `PayloadIdentity`) |
| `encrypt.ts`, `writer.ts` | Los writers. `encryptFiles(files, opts)` escribe un `.dkc` de formato 3, como `capsule.EncryptFiles`: comprueba las rutas y los textos con las reglas del lector y con los textos de Go, pone los ficheros en el orden de los bytes de sus rutas, mide L con un head de sal y hashes a cero, lee cada fichero dos veces y falla si cambió entre las dos lecturas; el head, el control y el área de seguridad se decodifican antes de escribir. El área de seguridad mide 32 KiB sea lo que sea lo que guarde la cápsula (§62.1, regla 13), y va vacía o con la firma y el sello de los enganches de Go: `authorKey` firma con `alg` 1 (un `AuthorKey` de `authorkey.ts` o cualquier `AuthorSigner`), `cmsSigner` con `alg` 2, la firma con certificados, y `sealer` pide el sello de `seal_type` 2; `largeArea` deja ensanchar el área a 64 KiB solo si lo firmado no cabe en 32 KiB. Los enganches pueden ser asíncronos. Se comprueban como en `newSealer` de Go, en su orden y con sus textos: una sola firma, y con `cmsSigner` los sellos van dentro de cada firma. Se llaman cuando el control y el head ya son los finales y antes de escribir nada: la firma se compromete con ellos y el sello con la firma (§29.8, §29.11). El área se evalúa con el lector de la librería en el contexto de la cápsula antes de escribirla, como `security` de Go, y una firma que no daría F4 o F6, o un sello que no daría S4 o S5, la hace fallar con el texto de Go (reglas 17, 19 y 21). Lo que lanza `cmsSigner` o `sealer` llega con `capsule: signing: ` o `capsule: sealing: ` y su mensaje, y el error como `cause`. `publicNote` es la nota pública de la cabecera (§24.1), que se rechaza con los textos de `extension.CheckNote` tras `capsule: `, y nunca se corrige; las opciones se comprueban en el orden de `newSealer` de Go. Con un área de 512 bytes, que solo puede pedir un generador de vectores, reproduce byte a byte `PRELUDE`, PUBLIC_HEADER, CONTROL_CBOR y `BODY` de los cinco fixtures que escribió `EncryptFiles` en la v0.10. `fileSource` hace la fuente de un `File`.<br>El formato 2 solo lo escribe un generador de vectores (§62.1, regla 1). `encrypt(src, opts)` tiene la forma de `capsule.Encrypt`, pero sus opciones no pueden pedirlo, así que falla con el texto de Go. Lo que solo pide un generador, el formato 2 y otra área (`TestVectors`, como `EncryptOptions.TestVectors` de Go), solo lo pasan al núcleo los ayudantes de `testing/encrypt.ts` (`encryptVectors`, `encryptWith` y `encryptFilesWith`), que ninguna página puede cargar. Con ellos, las pruebas y los scripts escriben un `.dkc` de formato 2, sin head, área ni nota, y, si se pide, una `.dkk` portable (§61, §62, §62.1), en el orden y con los textos y códigos de `capsule.Encrypt`:<br>- el formato 2 siempre; L conocida de antemano (el tamaño de un `Uint8Array` o un `Blob`, o `length` con un `ReadableStream`), y una fuente que da más o menos bytes falla con los textos de Go;<br>- el relleno `reforzado` por defecto, o `bloque256`;<br>- de 1 a 16 credenciales, canónicas y no de orden bajo, un señuelo en cada hueco libre, cuyo escalar se borra al derivar su clave pública, y un orden uniforme de los 16 (`random.ts`);<br>- `SEALED_CONTROL_LEN` con la fórmula del §62.1, comprobada con el sellado real;<br>- las autocomprobaciones de la regla 11 y dos más: `OUTER_TIME_AGE` con las reglas del lector, y la cabecera de `PAYLOAD_AGE`, que `I_PAYLOAD` abre antes de escribir nada.<br>Nada se escribe hasta que todo lo anterior al contenido está comprobado. El contenido va en trozos de 64 KiB, seguido de los ceros del relleno, con presión inversa, hacia memoria (hasta `MAX_MEMORY_DKC`, 1 GiB) o hacia `output`, que se cierra solo con la cápsula completa y comprobada y se aborta ante cualquier fallo. Los errores de la fuente y de la salida se relanzan tal cual.<br>El núcleo, `writer.ts`, recibe la aleatoriedad de quien lo llama: `encrypt.ts` le da la de `crypto.getRandomValues`, y solo `testing/encrypt.ts` la fija, para reproducir los fixtures de Go | `capsule.Encrypt`, `accesskey.Encode` |
| `encrypt.ts`, `writer.ts` | Los writers. `encryptFiles(files, opts)` escribe un `.dkc` de formato 3, como `capsule.EncryptFiles`: comprueba las rutas y los textos con las reglas del lector y con los textos de Go, pone los ficheros en el orden de los bytes de sus rutas, mide L con un head de sal y hashes a cero, lee cada fichero dos veces y falla si cambió entre las dos lecturas; el head, el control y el área de seguridad se decodifican antes de escribir. El área de seguridad mide 32 KiB sea lo que sea lo que guarde la cápsula (§62.1, regla 13), y va vacía o con la firma y el sello de los enganches de Go: `authorKey` firma con `alg` 1 (un `AuthorKey` de `authorkey.ts` o cualquier `AuthorSigner`), `cmsSigner` con `alg` 2, la firma con certificados, y `sealer` pide el sello de `seal_type` 2; `largeArea` deja ensanchar el área a 64 KiB solo si lo firmado no cabe en 32 KiB. Los enganches pueden ser asíncronos. Se comprueban como en `newSealer` de Go, en su orden y con sus textos: una sola firma, y con `cmsSigner` los sellos van dentro de cada firma. Se llaman cuando el control y el head ya son los finales y antes de escribir nada: la firma se compromete con ellos y el sello con la firma (§29.8, §29.11). El área se evalúa con el lector de la librería en el contexto de la cápsula antes de escribirla, como `security` de Go, y una firma que no daría F4 o F6, o un sello que no daría S4 o S5, la hace fallar con el texto de Go (reglas 17, 19 y 21). Desde la v0.16, `Encrypted.security` da los veredictos del área escrita, como `Result.Security` de Go: un sello sin `accuracy` se escribe, pero da S5, o la línea de su firmante en F6, con su motivo, para que quien escribe avise de él y ofrezca pedir otro (regla 19). Lo que lanza `cmsSigner` o `sealer` llega con `capsule: signing: ` o `capsule: sealing: ` y su mensaje, y el error como `cause`. `publicNote` es la nota pública de la cabecera (§24.1), que se rechaza con los textos de `extension.CheckNote` tras `capsule: `, y nunca se corrige; las opciones se comprueban en el orden de `newSealer` de Go. Con un área de 512 bytes, que solo puede pedir un generador de vectores, reproduce byte a byte `PRELUDE`, PUBLIC_HEADER, CONTROL_CBOR y `BODY` de los cinco fixtures que escribió `EncryptFiles` en la v0.10. `fileSource` hace la fuente de un `File`.<br>El formato 2 solo lo escribe un generador de vectores (§62.1, regla 1). `encrypt(src, opts)` tiene la forma de `capsule.Encrypt`, pero sus opciones no pueden pedirlo, así que falla con el texto de Go. Lo que solo pide un generador, el formato 2 y otra área (`TestVectors`, como `EncryptOptions.TestVectors` de Go), solo lo pasan al núcleo los ayudantes de `testing/encrypt.ts` (`encryptVectors`, `encryptWith` y `encryptFilesWith`), que ninguna página puede cargar. Con ellos, las pruebas y los scripts escriben un `.dkc` de formato 2, sin head, área ni nota, y, si se pide, una `.dkk` portable (§61, §62, §62.1), en el orden y con los textos y códigos de `capsule.Encrypt`:<br>- el formato 2 siempre; L conocida de antemano (el tamaño de un `Uint8Array` o un `Blob`, o `length` con un `ReadableStream`), y una fuente que da más o menos bytes falla con los textos de Go;<br>- el relleno `reforzado` por defecto, o `bloque256`;<br>- de 1 a 16 credenciales, canónicas y no de orden bajo, un señuelo en cada hueco libre, cuyo escalar se borra al derivar su clave pública, y un orden uniforme de los 16 (`random.ts`);<br>- `SEALED_CONTROL_LEN` con la fórmula del §62.1, comprobada con el sellado real;<br>- las autocomprobaciones de la regla 11 y dos más: `OUTER_TIME_AGE` con las reglas del lector, y la cabecera de `PAYLOAD_AGE`, que `I_PAYLOAD` abre antes de escribir nada.<br>Nada se escribe hasta que todo lo anterior al contenido está comprobado. El contenido va en trozos de 64 KiB, seguido de los ceros del relleno, con presión inversa, hacia memoria (hasta `MAX_MEMORY_DKC`, 1 GiB) o hacia `output`, que se cierra solo con la cápsula completa y comprobada y se aborta ante cualquier fallo. Los errores de la fuente y de la salida se relanzan tal cual.<br>El núcleo, `writer.ts`, recibe la aleatoriedad de quien lo llama: `encrypt.ts` le da la de `crypto.getRandomValues`, y solo `testing/encrypt.ts` la fija, para reproducir los fixtures de Go | `capsule.Encrypt`, `accesskey.Encode` |
| `tlock.ts` | `timeRecipient`, el `Recipient` de `age-encryption` para `OUTER_TIME_AGE` (§32, §35), como `agewrap.TimeRecipient`: cifra la file key con `ibe.ts` para una ronda de un perfil pinneado y escribe el stanza `tlock <ronda> <chain hash>` de tlock. Comprueba el perfil y luego el rango de la ronda, con los textos de `NewTimeRecipient`. `age-encryption` no tiene etiquetas, así que quien escriba `OUTER_TIME_AGE` (fase 3) lo añade como único recipient | `agewrap.TimeRecipient` |
| `lengths.ts` | El tamaño de un `.dkc` de formato 2 o 3 antes de escribirlo. Para el formato 3, `bodyLength` da L con `headLength`, que mide el head por los tamaños de sus elementos CBOR sin codificarlo ni cargar las tablas de Unicode, y `mtimeSeconds` y `headComment` dan la mtime y el comentario tal como el writer los guarda. Para los dos formatos: `sealedControlLength`, la fórmula de `SEALED_CONTROL_LEN` del §62.1 con la que el writer comprueba su sellado, y `capsuleLength`, el tamaño exacto que escriben los writers para una ronda, una política, L, el relleno, las extensiones y la nota pública, que la página muestra antes de cifrar porque cualquiera con el fichero lo ve (§55.2). Sin noble, `age-encryption` ni tablas de Unicode | `capsule.Encrypt`, que mide un borrador sellado |
| `padding.ts` | El relleno del formato 2 (§29.1): los códigos 1 (`bloque256`) y 2 (`reforzado`), `paddedLength`, exacta hasta L_MAX = 2⁵³ − 2⁴⁶ (`bitlen` con `BigInt` y los redondeos con `ceil`, exactos en doubles; nunca operaciones de 32 bits, `Math.clz32` ni `Math.log2`), y la longitud de `PAYLOAD_AGE` | `capsule/padding.go` |
@ -71,15 +71,15 @@ La inspección (pasos 1 a 8) no importa ninguna dependencia. Funciona en navegad
| `bech32.ts` | Bech32 (BIP 173) tal como `internal/bech32` de `age`, que la referencia copia como `codec/bech32`; conserva su aviso MIT | `codec/bech32` |
| `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, …)`; `compareInstants` e `isInstant`; `LONG_HORIZON_SECONDS` e `isLongHorizon`, el umbral de 365 días de los avisos de §53 y §50, una política de producto | `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. CONTROL_CBOR se lee y se escribe para un formato: versión de schema 1 sin las claves 6 y 7, o 2 y 3 con `payload_length` (8 bytes, hasta L_MAX) y `padding` (1 o 2); 103 bytes sin extensiones sea cual sea L | `capsule`, `accesskey` |
| `body.ts`, `security.ts`, `head.ts` | El formato 3 (§29.2 a §29.7): la trama de `BODY` (`AREA_LEN`, `SECURITY_LEN` y `HEAD_LEN`) y los ceros del área, `ERR_INTEGRITY`; el `SECURITY_CBOR` que escriben los writers, vacío o con la firma y el sello (`encodeSecurityWith`, `encodeAuthorSignatureItem` y `encodeSealItem`), y los veredictos de la v0.11 con sus textos y las líneas que los muestran, las de un certificado con los textos del borrador v0.12 (cada nombre entre « y », la autoridad del sello de cada firmante de F6 y el aviso de que DateKeys no comprueba quién emitió los sellos): X; F0 a F6, con la firma de `alg` 1 y la de `alg` 2 comprobadas en el contexto de la cápsula; y S0 a S5, con el sello de `seal_type` 2. `evaluateSecurity` nunca lanza: una excepción al evaluar la firma da F1, y una al evaluar el sello, S2, cada una sin tocar el otro veredicto. Y el head, con las capas de §69.1: R1 y R8 en el CDDL, R8 por los bytes UTF-8 y no por el orden UTF-16 de las cadenas de JavaScript, y luego el comentario, el autor declarado, las rutas, la maquetación, R7 y R9, todo `ERR_HEAD_INVALID`, y las extensiones críticas del objeto `head` | `capsule/format3.go`, `capsule/signature.go` |
| `body.ts`, `security.ts`, `head.ts` | El formato 3 (§29.2 a §29.7): la trama de `BODY` (`AREA_LEN`, `SECURITY_LEN` y `HEAD_LEN`) y los ceros del área, `ERR_INTEGRITY`; el `SECURITY_CBOR` que escriben los writers, vacío o con la firma y el sello (`encodeSecurityWith`, `encodeAuthorSignatureItem` y `encodeSealItem`), y los veredictos de la v0.11 con sus textos y las líneas que los muestran, las de un certificado con los textos del borrador v0.12 (cada nombre entre « y », la autoridad del sello de cada firmante de F6 y el aviso de que DateKeys no comprueba quién emitió los sellos): X; F0 a F6, con la firma de `alg` 1 y la de `alg` 2 comprobadas en el contexto de la cápsula; y S0 a S5, con el sello de `seal_type` 2. Desde la v0.16, S5 no tiene texto fijo: «No acredita que se sellara antes de la fecha de apertura: ‹motivo›.», con el motivo de `sealReasonText`, y la línea de un firmante de F6 cuyo sello no lo acredita dice «sin acreditar que fuera antes de la fecha de apertura: ‹motivo›». `evaluateSecurity` nunca lanza: una excepción al evaluar la firma da F1, y una al evaluar el sello, S2, cada una sin tocar el otro veredicto. Y el head, con las capas de §69.1: R1 y R8 en el CDDL, R8 por los bytes UTF-8 y no por el orden UTF-16 de las cadenas de JavaScript, y luego el comentario, el autor declarado, las rutas, la maquetación, R7 y R9, todo `ERR_HEAD_INVALID`, y las extensiones críticas del objeto `head` | `capsule/format3.go`, `capsule/signature.go` |
| `authorkey.ts` | Las claves de autor de `alg` 1 (§29.9, §29.12), como el paquete `authorkey` de Go en `spec-v0.12`, con sus comprobaciones en su orden y sus textos byte a byte: `AuthorKey` (`generate` con una fuente de azar inyectable, `fromSeed`, `publicKey`, `sign`, `clear`, `secret`, y `toString`, `toJSON` y `util.inspect` que ocultan el secreto), `authorPublicString`, `parseAuthorPublic` (canónica, en la curva y no de orden pequeño), `parseAuthorSecret` y `marshalAuthorKey`. Las cadenas se leen como bytes de Go: la mayúscula y la minúscula son las de `strings.ToLower` y `strings.ToUpper` de Go, y los espacios de una línea los de `strings.TrimSpace`, con las tablas de `gounicode.ts`; un `Uint8Array` es una cadena de Go que puede no ser UTF-8. El fichero de clave: `encryptAuthorKey` lo escribe con el `Encrypter` de `age-encryption` y una frase de paso, scrypt con logN 16; `readAuthorKey` lee uno cifrado o en claro de hasta 64 KiB, con las líneas de `bufio.Scanner`, y del cifrado lee la cabecera con `age.ts`, comprueba el stanza scrypt como `ScryptIdentity` de Go con un factor máximo de 16, deja a `age-encryption` el scrypt y el MAC, y descifra el STREAM, todo con los textos de `age` de Go. A diferencia de Go, una clave borrada lanza al usarla. JavaScript no promete tiempo constante ni borrar la memoria: se borran las copias propias, no las del motor, `age-encryption` o noble, ni las cadenas. En Node 24.9, firmar tarda unos 6 ms, y escribir o leer un fichero de clave cifrado, unos 0,3 s, los del scrypt de 64 MiB | `authorkey` |
| `ed25519sign.ts` | La firma Ed25519 (RFC 8032, 5.1.5 y 5.1.6) en código propio: `crypto_sign` de TweetNaCl, como el port de Dart, con el SHA-512 de `@noble/hashes`. Aritmética exacta en `Float64Array` (16 miembros de 16 bits en el cuerpo, 64 de 8 bits para los escalares módulo ℓ); los secretos nunca pasan por `BigInt`, una rama o un índice que dependa de ellos. Da la firma de Go byte a byte, también si la clave pública que se le da es otra | `crypto/ed25519` |
| `gounicode.ts` | Las tablas de `unicode.ToLower`, `unicode.ToUpper` y `unicode.IsSpace` de Go 1.26 (Unicode 15.0.0) que usan `strings.ToLower`, `strings.ToUpper` y `strings.TrimSpace`, en tramos. Las genera `scripts/go-unicode-tables.go` con el SHA-256 de cada conjunto, que una prueba recalcula | `unicode` |
| `author.ts` | Lo que se firma y se sella (§29.8, §29.11): `payload_commit`, `control_commit` sobre `CONTROL_SIG`, `head_digest`, `signers_digest`, `AUTHOR_MESSAGE` (99 bytes de ASCII) y su código, que se toma byte a byte como en Go, `SIG_PART` y `SEAL_SUBJECT`. Nada se guarda: todo se recalcula de la cápsula abierta | `capsule/signature.go` |
| `ed25519strict.ts` | La verificación estricta de la firma de `alg` 1 (§29.9): las cuatro condiciones del perfil, con la aritmética del grupo de `@noble/curves`, cuyo `verify` usa la ecuación con cofactor y acepta lo que el perfil rechaza. Da la respuesta de Go en los 18 vectores de `ed25519_strict.json` | `internal/ed25519strict` |
| `der.ts` | La comprobación estricta de DER que hace la firma CMS antes de mirar dentro (§29.10): longitudes definidas y mínimas, BOOLEAN, INTEGER, NULL, OID y BIT STRING canónicos, UTCTime y GeneralizedTime en sus formas de X.690 y con una fecha que existe (`parseTime` los lee), solo los tipos universales que usan los certificados, las firmas y los tokens, los tipos de cadena restringidos entre ellos, y una profundidad de 32; `setOfSorted` para los SET OF cuyo esquema conoce quien llama, que pueden repetir un elemento | `internal/der` |
| `cms.ts` | La firma CMS (RFC 5652) de `alg` 2 y el token RFC 3161 del sello (§29.10, §29.11), con el orden de comprobaciones y los resultados de Go y la tabla cerrada de algoritmos: RSA PKCS #1 v1.5 y PSS con `BigInt`, de 2048 a 4096 bits y con un módulo impar, y ECDSA sobre P-256, P-384 y P-521 con la aritmética de `@noble/curves`, solo con el punto sin comprimir. Lee el certificado campo a campo con el perfil del §29.10 del borrador v0.12, como Go: a quién nombra (su `givenName` y su `surname` antes que su `commonName`), el emisor que dice (su `commonName` o su `organizationName`), su validez y su clave; nunca comprueba quién lo emitió ni si se revocó. Compara los OID por los bytes de su DER, acepta un SET OF que repite un elemento y cuenta como uno un certificado repetido, y un nombre conserva un U+FEFF inicial, como en Go | `internal/cms` |
| `securitycms.ts` | Los veredictos de una firma de `alg` 2, F1, F2, F5 y F6, con cada firmante requerido y ajeno nombrado como el §29.7 del borrador v0.12 (su nombre si cumple las reglas del autor declarado, tiene como mucho 64 puntos de código y no lleva dos espacios seguidos, y si no el SHA-256 del certificado; su emisor, o el SHA-256 de su `Name`; y la autoridad de su sello), y los de un sello, S1 a S5, con su autoridad y t. `encodeSigners` escribe `SIGNERS` para el escritor, ordenado y con los textos de Go | `capsule/signature2.go` |
| `cms.ts` | La firma CMS (RFC 5652) de `alg` 2 y el token RFC 3161 del sello (§29.10, §29.11), con el orden de comprobaciones y los resultados de Go y la tabla cerrada de algoritmos: RSA PKCS #1 v1.5 y PSS con `BigInt`, de 2048 a 4096 bits y con un módulo impar, y ECDSA sobre P-256, P-384 y P-521 con la aritmética de `@noble/curves`, solo con el punto sin comprimir. Lee el certificado campo a campo con el perfil del §29.10 del borrador v0.12, como Go: a quién nombra (su `givenName` y su `surname` antes que su `commonName`), el emisor que dice (su `commonName` o su `organizationName`), su validez y su clave; nunca comprueba quién lo emitió ni si se revocó. Compara los OID por los bytes de su DER, acepta un SET OF que repite un elemento y cuenta como uno un certificado repetido, y un nombre conserva un U+FEFF inicial, como en Go. Del token lee además, desde la v0.16, si lleva `accuracy` (`hasAccuracy`) y su política, y `tokenIsBTSP` dice si es la BTSP de ETSI EN 319 421 (0.4.0.2023.1.1) | `internal/cms` |
| `securitycms.ts` | Los veredictos de una firma de `alg` 2, F1, F2, F5 y F6, con cada firmante requerido y ajeno nombrado como el §29.7 del borrador v0.12 (su nombre si cumple las reglas del autor declarado, tiene como mucho 64 puntos de código y no lleva dos espacios seguidos, y si no el SHA-256 del certificado; su emisor, o el SHA-256 de su `Name`; y la autoridad de su sello), y los de un sello, S1 a S5, con su autoridad y t. Desde la v0.16, un sello válido es S4, o la línea de un firmante dice «antes de la fecha de apertura», solo si el token lleva `accuracy` y t más la precisión es anterior a `round_time`; si no, S5, con el primer motivo que se cumple (`SealReason`, como en Go): `late`, sellado después o demasiado cerca; `no accuracy, BTSP`, sin `accuracy` en un token de la política BTSP, que la exige; o `no accuracy`. `encodeSigners` escribe `SIGNERS` para el escritor, ordenado y con los textos de Go | `capsule/signature2.go`, `capsule/format3.go` (`SealReason`) |
| `note.ts` | La nota pública (§24.1): la extensión `datekeys.note` de la cabecera, de 1 a 1024 bytes de UTF-8 que cumplen las reglas del autor declarado, con los textos y el orden de `extension.CheckNote`. Una cadena con un sustituto suelto se rechaza: nunca se escribe con U+FFFD. `checkNoteData` comprueba los bytes de una nota en el orden de Go: la longitud, el UTF-8 y las reglas. `publicNote` la lee como Go, con un U+FEFF inicial que la deja inservible, y `unusableNote` distingue una nota inservible de ninguna | `extension` (`CheckNote`, `NewNote`, `Note`), `capsule` (`Header.UnusableNote`) |
| `wordkey.ts` | La llave de palabras (§38.1): `normalizeWords` (NFD con las tablas de Unicode 18.0.0, sin las marcas U+0300 a U+036F, la minúscula simple de cada punto de código, partido por los espacios de la lista), `checkWords` (al menos 6 palabras distintas de 3 letras o más, sin controles, invisibles ni puntos sin asignar), `wordRules` (las dos, con las tablas cargadas una vez) y `wordKey`, PBKDF2-SHA256 de 600 000 rondas de Web Crypto con la sal de la cadena, la ronda y `capsule_id`. `quickWords`, `hiddenCodePoint` y `countedWords` leen con las tablas de la plataforma, para un formulario que no puede esperar a las de Unicode 18.0.0 | `wordkey.Normalize`, `Check`, `Key` |
| `wordlist.ts` | Las palabras al azar de la llave de palabras (el SHOULD de §38.1), con los textos de Go: `generateWords` sortea `DEFAULT_WORD_COUNT` palabras distintas, 7, con `randomIndex` y `crypto.getRandomValues`; `wordBits` da su fuerza, 90 bits para 7 de 7 776; `checkWordList` rechaza una lista de menos de 2 048 palabras, con dos que son una al normalizarlas o con un carácter que no es una letra del alfabeto de su idioma, que da el código y no la lista (para `es`, de la `a` a la `z`, `á`, `é`, `í`, `ó`, `ú`, `ü` y `ñ`; para `en`, de la `a` a la `z` y el guion de las cuatro palabras compuestas de la lista de la EFF): una letra cirílica que parece latina se volvería a escribir con la latina, y la cápsula no se abriría; y `readWordList` solo acepta una lista con el SHA-256 fijado para su idioma en `WORD_LIST_SHA256`, que sea UTF-8 y que `checkWordList` acepte. Ninguna lista se da por buena, tampoco las de DateKeys. Los dados, para quien no se fía del azar del ordenador: `diceNumber` numera las palabras de una lista de 7 776 del 11111 al 66666, `diceWord` da la palabra de un número de cinco dados, `diceWords` las de varios (al menos 6 y ninguna repetida) y `diceList` es la lista numerada para imprimirla, como la publica la EFF: la de `en` es su fichero, byte a byte | `wordkey.Generate`, `CheckList`, `Bits`, `List`, `DiceNumber`, `DiceWord`, `DiceWords`, `DiceList` |
@ -157,7 +157,7 @@ Todo el texto leído de la cápsula pasa por interpolación de texto de Svelte (
Tras los pasos 1 a 8, si la cápsula es válida, la página ofrece abrirla: pasos 9 a 18 de §63 con `open` (fase 2, paso 8). Si según el reloj del dispositivo la fecha no ha llegado, lo dice, no ofrece pedir la firma a drand y deja dar un release que la persona ya tenga, porque el reloj puede ir atrasado (v0.15, paso 9.c); si ese release abre la cápsula, el resultado dice que el reloj parece ir atrasado.
- **El release lo da quien abre** (decisión 4 del plan de la fase 2, confirmada el 28-09-2026): la página nunca lo pide a la red. Se pega la respuesta JSON de drand o la firma sola en hexadecimal (`release-input.ts`), y solo se leen `round` y `signature`: cualquier otro campo, como una clave pública, se ignora, porque la raíz de confianza es el perfil fijado (§11, §13). La página enlaza la URL de drand de esa ronda (`https://api.drand.sh/<chain hash>/public/<ronda>`, con `rel="noopener noreferrer"`), que abre la persona en otra pestaña. Es un release que suministra quien llama: el paso 10 lo verifica y da sus códigos (`ERR_ROUND_MISMATCH`, `ERR_RELEASE_INVALID`). En los fixtures oficiales el campo viene relleno con el release de su registro, que es el que publicó drand. Desde la v0.15 también se puede elegir un fichero con el release: el objeto release, la respuesta de drand en JSON o un archivo de releases local. Va tal cual a `open` como release en la mano (`opener.ts`), y el paso 10 lo lee con los códigos de Go; un fichero de más de 8 KiB que no es un archivo de releases no se lee, y la página lo dice. Lo pegado va como el JSON de drand, que no nombra cadena.
- **El release lo da quien abre** (decisión 4 del plan de la fase 2, confirmada el 28-09-2026): la página nunca lo pide a la red. Se pega la respuesta JSON de drand o la firma sola en hexadecimal (`release-input.ts`), y solo se leen `round` y `signature`, con el lector estricto de la librería (v0.16, §47.1), para que la página nunca lea otra ronda que la del paso 10: un nombre repetido es JSON no válido, `ROUND` es otro campo y la ronda va de 1 a 2⁵³ − 1. Cualquier otro campo, como una clave pública, se ignora, porque la raíz de confianza es el perfil fijado (§11, §13). La página enlaza la URL de drand de esa ronda (`https://api.drand.sh/<chain hash>/public/<ronda>`, con `rel="noopener noreferrer"`), que abre la persona en otra pestaña. Es un release que suministra quien llama: el paso 10 lo verifica y da sus códigos (`ERR_ROUND_MISMATCH`, `ERR_RELEASE_INVALID`). En los fixtures oficiales el campo viene relleno con el release de su registro, que es el que publicó drand. Desde la v0.15 también se puede elegir un fichero con el release: el objeto release, la respuesta de drand en JSON o un archivo de releases local. Va tal cual a `open` como release en la mano (`opener.ts`), y el paso 10 lo lee con los códigos de Go; un fichero de más de 8 KiB que no es un archivo de releases no se lee, y la página lo dice. Lo pegado va como el JSON de drand, que no nombra cadena.
- **Credenciales**, solo en `time_and_key`: una `.dkk` (se leen como mucho 16 MiB + 13 bytes, `readAccessKey`) o identidades `AGE-SECRET-KEY-1…`, una por línea, como un fichero de identidades de `age`. La página explica de dónde sale la identidad: del fichero que se creó con `age-keygen`, que se puede pegar entero. Un error de una línea se da por su número, nunca por su contenido, y el foco va al campo; las identidades se borran tras usarlas. En `time_only` la página no las pide y `open` no las usa.
- **El código de la apertura se carga bajo demanda**: la página importa `opener.ts` con `import()` al pulsar "Abrir", y con él `open.ts`, noble y `age-encryption`. `opening.ts`, que construye lo que se muestra, solo importa tipos de `open.ts`. `check-build.mjs` comprueba que ninguna página carga noble, `@scure/base` ni `age-encryption` en la primera carga.
- **El texto en claro** se muestra, cuando la cápsula se abre y es texto (`plaintextPreview`): UTF-8 imprimible, hasta 100 000 caracteres de sus primeros 128 KiB. Se muestra el CR LF de Windows como salto de línea y se omite el BOM, como hace un editor; la descarga conserva los bytes exactos. Lo que no es texto solo se ofrece para descargar. El de un fixture se abre en memoria, con su SHA-256 comparado con el del registro. El contenido va justo debajo del veredicto, antes de los pasos 9 a 18.
@ -219,7 +219,7 @@ Rendimiento, informativo, en ese navegador con la ventana en segundo plano: una
| `src/lib/inspector/diagnostic.ts` | Notación de diagnóstico CBOR (RFC 8949 §8) de `walk`, acotada a 16 384 caracteres |
| `src/lib/dkc/prefix.ts` | Lectura por prefijo, que también usa la apertura: de un `.dkc` grande solo se leen 16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN + 2 MiB + 1 bytes, y solo 16 si los pasos 1 y 2 rechazan el prelude (otro tipo de fichero, un `.dkk`, longitudes fuera de §57), siempre con el mismo resultado que el fichero entero (lo comprueba `prefix.test.ts`) |
| `src/lib/inspector/fixtures.ts` | Los fixtures oficiales, empaquetados desde `testdata/fixtures` |
| `src/lib/inspector/release-input.ts` | Lee el release pegado (respuesta de drand o firma sola) y construye la URL de drand de la ronda; un fichero con el release lo lee `opener.ts` |
| `src/lib/inspector/release-input.ts` | Lee el release pegado (respuesta de drand, con `strictJSON` y `jsonRound` de la librería, o firma sola) y construye la URL de drand de la ronda; un fichero con el release lo lee `opener.ts` |
| `src/lib/inspector/opener.ts` | La apertura, cargada bajo demanda: identidades, `.dkk`, `open` con el release suministrado, SHA-256 del texto en claro |
| `src/lib/inspector/opening.ts` | `buildOpenReport`: el modelo de la apertura (pasos 9 a 18, release, extensiones de CONTROL_CBOR, texto en claro), sin DOM ni reloj; nombre del fichero descifrado |
| `src/lib/inspector/tempfile.ts` | El fichero temporal de OPFS, con un directorio y un Web Lock por pestaña y por zona (la apertura y crear), la cuota libre y la limpieza de lo que quedó. Su `writable` acepta además trozos con posición (`TempChunk`), como `FileSystemWritableFileStream`, para parchear el CRC-32 del ZIP |
@ -238,7 +238,7 @@ Rendimiento, informativo, en ese navegador con la ventana en segundo plano: una
### Fixtures
`fixtures.ts` importa con `import.meta.glob` los `.dkc` de `testdata/fixtures` como URL (`?url`) y, de cada registro JSON, solo tres campos públicos: `description`, `release` (la ronda y la firma que publicó drand, con las que la página abre el fixture) y `plaintext_sha256` (para comparar con lo descifrado). `testdata/` sigue siendo la única fuente: Vite copia cada `.dkc` como fichero con hash en `_app/immutable/assets/` y nunca lo incrusta como `data:` (`assetsInlineLimit: 0`), y no se copia nada más. Los `.dkk`, los textos en claro y los demás campos de los registros (`payload_identity`, `control_cbor`…) no llegan al sitio; `check-build.mjs` lo comprueba. En desarrollo, `server.fs.allow` deja que Vite sirva `testdata/fixtures`.
`fixtures.ts` importa con `import.meta.glob` los `.dkc` de `testdata/fixtures` como URL (`?url`) y, de cada registro JSON, solo tres campos públicos: `description`, `release` (la ronda y la firma que publicó drand, con las que la página abre el fixture) y `plaintext_sha256` (para comparar con lo descifrado). `testdata/` sigue siendo la única fuente: Vite copia cada `.dkc` como fichero con hash en `_app/immutable/assets/` y nunca lo incrusta como `data:` (`assetsInlineLimit: 0`), y no se copia nada más. Los `.dkk`, los textos en claro y los demás campos de los registros (`payload_identity`, `control_cbor`, `words_text`…) no llegan al sitio; `check-build.mjs` lo comprueba. La descripción de `format3_time_and_key_words` (v0.16) nombra sus palabras, las del vector público del anexo, 79.7, así que en la página se abre escribiéndolas. En desarrollo, `server.fs.allow` deja que Vite sirva `testdata/fixtures`.
### Sin red: la Content-Security-Policy
@ -250,12 +250,12 @@ img-src 'self'; manifest-src 'self'; object-src 'none'; script-src 'self' 'sha25
style-src 'self'; style-src-attr 'unsafe-hashes' 'sha256-…'; base-uri 'none'; form-action 'none'
```
- `connect-src`: `fetch` llega al propio origen, para los fixtures, y a los tres relays públicos de drand que usa la CLI de la referencia, solo cuando la persona pulsa «Pedir la firma a drand» en `/inspect` (`drand.ts`: en carrera, 6 s, como mucho 8 KiB, sin redirecciones, y la aleatoriedad comprobada contra la firma, que el paso 10 verifica con la clave fijada). El relay ve la IP y la ronda pedida. Tampoco llega a URL `blob:`: la descarga del texto en claro es una navegación.
- `connect-src`: `fetch` llega al propio origen, para los fixtures, y a los tres relays públicos de drand que usa la CLI de la referencia, solo cuando la persona pulsa «Pedir la firma a drand» en `/inspect` (`drand.ts`: en carrera, 6 s, como mucho 8 KiB, sin redirecciones, y la respuesta leída con `parseDrandJSON`, el lector estricto que usa desde la v0.16 el cliente de Go, con la aleatoriedad comprobada contra la firma, que el paso 10 verifica con la clave fijada). El relay ve la IP y la ronda pedida. Tampoco llega a URL `blob:`: la descarga del texto en claro es una navegación.
- `script-src`: los módulos del sitio y el hash SHA-256 del único script en línea, el arranque de SvelteKit (los nonces no sirven en HTML prerenderizado).
- `style-src 'self'`: solo hojas de estilo del sitio; sin fuentes web ni CDN, con las fuentes del sistema.
- `style-src-attr`: solo el atributo `style` del anunciador de rutas de SvelteKit, por su hash (`ANNOUNCER_STYLE_HASH`, válido para `@sveltejs/kit` 2.70.3; `app.css` lo oculta también si el navegador bloquea el atributo).
`npm run build` ejecuta después `scripts/check-build.mjs` (`postbuild`; también `npm run build:check`), que falla si una ruta no tiene su HTML prerenderizado; si una página no tiene exactamente esa política, con la etiqueta antes de cualquier elemento que cargue recursos; si un script en línea no está en `script-src` o sobra un hash; si `style-src-attr` no coincide con los atributos `style` del bundle; si hay estilos en línea, manejadores de eventos en atributos, `@import` o URL a otro origen; si algún `.dkc` oficial no está byte a byte; si aparece en el sitio algún secreto de los fixtures (`.dkk`, textos en claro, identidades, `payload_identity`, `access_material`, `control_cbor`); si el bundle del cliente contiene `tlock-js`, `drand-client` o helpers de Babel, o una copia anidada de un paquete que no sea la de noble bajo `@noble/post-quantum`; si contiene un test o un módulo de `src/lib/dkc/testing/`, cuyos ayudantes escriben lo que solo puede escribir un generador de vectores; si una página carga noble, `@scure/base` o `age-encryption` en su primera carga, o las claves de autor y su firma (`authorkey.ts`, `ed25519sign.ts` y `gounicode.ts`), que `/inspect` no carga ni bajo demanda, o si `/inspect` y `/create` no pueden cargar bajo demanda `age-encryption`, `@noble/curves` y `@noble/ciphers`, y `/create` también `@noble/hashes`; o si a `licenses.txt` le falta el aviso de un paquete del bundle, las líneas de copyright de un módulo de `src/` derivado de otro proyecto o la licencia del sitio. `vite.config.ts` registra los módulos de cada chunk en `.svelte-kit/output/client-modules.json`, fuera del sitio. Al terminar informa del JavaScript que carga cada página, en bytes y con gzip, en la primera carga y bajo demanda, y de los paquetes npm que lleva el bundle.
`npm run build` ejecuta después `scripts/check-build.mjs` (`postbuild`; también `npm run build:check`), que falla si una ruta no tiene su HTML prerenderizado; si una página no tiene exactamente esa política, con la etiqueta antes de cualquier elemento que cargue recursos; si un script en línea no está en `script-src` o sobra un hash; si `style-src-attr` no coincide con los atributos `style` del bundle; si hay estilos en línea, manejadores de eventos en atributos, `@import` o URL a otro origen; si algún `.dkc` oficial no está byte a byte; si aparece en el sitio algún secreto de los fixtures (`.dkk`, textos en claro, identidades, el texto de una llave de palabras, `payload_identity`, `access_material`, `control_cbor`); si el bundle del cliente contiene `tlock-js`, `drand-client` o helpers de Babel, o una copia anidada de un paquete que no sea la de noble bajo `@noble/post-quantum`; si contiene un test o un módulo de `src/lib/dkc/testing/`, cuyos ayudantes escriben lo que solo puede escribir un generador de vectores; si una página carga noble, `@scure/base` o `age-encryption` en su primera carga, o las claves de autor y su firma (`authorkey.ts`, `ed25519sign.ts` y `gounicode.ts`), que `/inspect` no carga ni bajo demanda, o si `/inspect` y `/create` no pueden cargar bajo demanda `age-encryption`, `@noble/curves` y `@noble/ciphers`, y `/create` también `@noble/hashes`; o si a `licenses.txt` le falta el aviso de un paquete del bundle, las líneas de copyright de un módulo de `src/` derivado de otro proyecto o la licencia del sitio. `vite.config.ts` registra los módulos de cada chunk en `.svelte-kit/output/client-modules.json`, fuera del sitio. Al terminar informa del JavaScript que carga cada página, en bytes y con gzip, en la primera carga y bajo demanda, y de los paquetes npm que lleva el bundle.
### Avisos de licencia: `licenses.txt`
@ -302,12 +302,13 @@ Umbrales de cobertura (`vitest.config.ts`), al 100 % en líneas, ramas, funcione
- `testdata/vectors/padding.json`: P con las dos reglas y la longitud de `PAYLOAD_AGE` de cada L, incluidas las fronteras en que fallan las operaciones de 32 bits y un logaritmo en coma flotante, y las longitudes por encima de L_MAX, que se rechazan. `padding.test.ts` contrasta además `paddedLength` con el §29.1 escrito en `BigInt` sobre 20 000 longitudes de todo el rango.
- `testdata/vectors/tlock_ibe.json`: el vector de H2 del IBE de tlock (§63 paso 11). Hasta que llegue `ibe.ts` (fase 2), el test lo recalcula con `@noble/curves` 2.4.0: los puntos son canónicos para `bls12381.ts`, el pairing serializado en el orden de kilic es el GT del vector y su H2 coincide; el orden propio de noble (`Fp12.toBytes`) da otro hash.
- `testdata/vectors/tlock_steps.json`: los pasos 10 y 11 de §63 para Quicknet, valor a valor (v0.14), con el código de la librería: M con `roundIdentity`, H(M) con `hashToG1`, el release con `verifyRelease` y la ecuación de pairing; las tres partes del stanza con `ciphertextFromBody`, e(firma, U) con `gtBytes`, H2, sigma, H4 y la file key; la base de H3 con `h3Base`, cada intento con `h3Try` y su digest, el desplazamiento del primer byte y su aceptación, r con `h3`, y r·G2 = U con `proofHolds`; y `decryptOnG2` sobre el cuerpo entero. También las lecturas erróneas que descarta el generador: el DST de G2 o la ronda sin SHA-256 no verifican, y poner a cero el bit más alto en vez de desplazar el byte da otra r, que U no prueba; una firma de otra ronda y un V o un W editados no descifran.
- `testdata/vectors/release.json` y `releases/` (v0.15): los 36 objetos y los 12 JSON de drand pasan por `parseRelease` y `verifyRelease` frente a Quicknet y la ronda de cada caso, con el resultado, el texto de Go y lo que decodifican; un objeto se reescribe a sus bytes. Cada fichero de `releases/` es el objeto de su ronda, que verifica, y las 7 consultas del archivo dan lo que dice el fichero, en memoria y desde un `Blob`. La guarda de `testdata` exige cada fichero de `releases/`.
- `testdata/vectors/release.json` y `releases/` (v0.15): los 36 objetos y los 38 JSON de drand, 26 de ellos de la lectura estricta de la v0.16, pasan por `parseRelease` y `verifyRelease` frente a Quicknet y la ronda de cada caso, con el resultado, el texto de Go y lo que decodifican; un objeto se reescribe a sus bytes. Cada fichero de `releases/` es el objeto de su ronda, que verifica, y las 7 consultas del archivo dan lo que dice el fichero, en memoria y desde un `Blob`. La guarda de `testdata` exige cada fichero de `releases/`.
- `testdata/vectors/mutations.json`: se leen los 222 casos enteros (ediciones sobre un fixture o hex congelado, release, su fuente, reloj, registro, extensiones, `.dkk`, identidades y, en los once del formato 3 que abren, sus veredictos). Cada caso usa la fuente que dice su campo `source`, como `singleSource` de Go: `supplied`, un release en la mano que se da a `open` como objeto release, con la cadena de Quicknet salvo que el caso nombre otra, o `network`, una fuente que verifica el release y lo descarta. Los 222 pasan por `open` con su release, su reloj, su registro, sus extensiones, su `.dkk`, sus identidades y un `MemorySink`, y dan el mismo código en el mismo paso que Go y el mismo texto que `capsule.Open` (`testing/mutation-texts.json`); los cuatro de seguridad del formato 3 abren con los veredictos X, F1, F2 y S1 del registro, y siete de la lista de la v0.11 con los suyos. «round not reached yet», del formato 1, abre con el release en la mano y el reloj atrasado. También pasan como `Blob` con un stream de salida, que termina abortado en los 210 que fallan, y un sumidero que nunca se publica. Son los 178 del §64: las 33 mutaciones de las dos primeras listas en cada formato, las 23 de la lista del formato 2, las 48 de la del formato 3 y las 8 de la lista de la v0.11; y 44 más. Ninguno de los que fallan sin red pide un release. Los 57 de los pasos 1 a 8 pasan además por `inspect`, y un test fija los recuentos y el orden. Las `.dkk` ofrecidas se decodifican.
- `testdata/vectors/inspect_differential.json`: las 5 110 mutaciones de los catorce fixtures de base dan el mismo veredicto, código y paso que Go; los `bases` se comprueban por su SHA-256.
- `testdata/vectors/paths.json` y `path_fold.json`, con las tablas cuyo digest nombran: cada ruta pasa por las reglas de una entrada con el texto exacto, cada árbol por la decodificación de un head de ficheros de 0 bytes, y cada segmento da su NFD y su clave de R7.
- `testdata/vectors/head_schema.json` y `security.json`: cada head pasa por `decodeHead` con el código de la primera capa que falla y el texto exacto de `ERR_HEAD_INVALID`, y un head válido se reescribe a sus bytes; cada área de seguridad da, en el contexto del fichero, sus veredictos y sus líneas.
- `testdata/vectors/security_cms.json`: los 135 casos de `alg` 2 y de `seal_type` 2 dan, en su contexto, los veredictos, el resultado de cada firmante exigido y de cada ajeno, la autoridad y la hora del sello y las líneas de Go, byte a byte. `ed25519_strict.json` lo corre `ed25519strict.test.ts`.
- `testdata/vectors/security_cms.json`: los 143 casos de `alg` 2 y de `seal_type` 2, hechos de nuevo para la v0.16, dan, en su contexto, los veredictos, el resultado de cada firmante exigido y de cada ajeno, la autoridad y la hora del sello, el motivo (`seal_reason`) de un S5 o de un firmante cuyo sello no acredita la fecha, y las líneas de Go, byte a byte.
- `format3_time_and_key_words` y `format3_full_chunk` (v0.16): el primero abre con la identidad que dan las palabras de su `words_text`, normalizadas con `normalizeWords` y derivadas con `wordKey` con la cadena, la ronda y su `capsule_id`, que es la de `identities`, y también en la página con el texto tal cual y con sus marcas sueltas; el segundo, cuyo `PAYLOAD_AGE` acaba en un trozo STREAM completo, abre a su fichero. `ed25519_strict.json` lo corre `ed25519strict.test.ts`.
- `testdata/vectors/note.json`: cada nota pasa por `checkNoteData`, con su resultado y el texto exacto de la regla que incumple, y por `publicNote`, `unusableNote` y `newNote`.
- `testdata/vectors/locator.json`: se corre entero, como `TestLocatorVectors` de Go, con los textos de Go de `testing/locator-vectors.json` y `testing/locator-uris.json`: la extensión se lee, el localizador se abre con el release de su ronda y da su texto en claro de 4096 bytes y sus campos, el resto se encuentra en el host en su desplazamiento y abre el sobre; las 36 bases de relleno; las 247 direcciones, con el veredicto del fichero y el texto de Go; el localizador mixto, del que se usa solo la dirección que se acepta y que un escritor no escribe; los 5 restos, los 8 datos de la extensión, con `ERR_EXTENSION_DATA_INVALID` como único código, y los 16 textos en claro.
- `src/lib/dkc/testing/locator-uris.json` y `locator-vectors.json`: el localizador sin su escritura contra Go en `spec-v0.12`, que comprueban `locator.test.ts` y `envelope.test.ts`, todo con el resultado y el texto de Go. Los escribe `scripts/locator-go-vectors.go`, el generador de la etapa 7a de `datekeys-dart` con las semillas de este repositorio:
@ -319,7 +320,7 @@ Umbrales de cobertura (`vitest.config.ts`), al 100 % en líneas, ramas, funcione
- `src/lib/dkc/testing/locator-seal.json`: lo que escriben `Seal` y `NewEnvelope` de Go mientras `crypto/rand` lee el keystream de una semilla (ChaCha20 bajo el SHA-256 de la semilla, nonce a cero), con cada valor que saca. Lo escribe `scripts/locator-seal-go-vectors.go`, y `envelope.test.ts` lo comprueba con `seededFill` de `testing/seeded.ts`, la misma fuente: `seal` y `newEnvelope` sacan los mismos valores en el mismo orden y escriben los mismos bytes en los 10 sellados, de uno a tres bloques y de la ronda 1 a la última de Quicknet, en los 8 sobres, de 0 bytes a 1 MiB, y en el camino entero; y rechazan las 12 entradas que rechaza Go, con su texto y antes de sacar nada. Se regenera como el anterior, con `-source spec-v0.12`.
- `src/lib/dkc/testing/locator-interop.json`: Go abre lo que escribe esta librería. `scripts/locator-ts-samples.mjs` escribe los ficheros de las recetas de `testing/locator-interop.ts`, siete localizadores sellados con sus sobres, de un `.dkc` de 0 bytes a uno de 16 MiB y un byte, en el que el contador del nonce de STREAM pasa de un byte; `scripts/locator-go-verdicts.go` los abre con `locator.Open`, comprueba que `Marshal` da el texto sellado y que se usan todas sus direcciones, abre el sobre con `OpenEnvelope` y busca el resto escondido con `Hide` y `RestIn`. `locator.interop.test.ts` vuelve a escribir cada fichero de su receta, exige el SHA-256 que leyó Go y lo abre también. Para regenerarlo: `node scripts/locator-ts-samples.mjs DIR`, y desde la exportación de `datekeys-go`, `go run .../scripts/locator-go-verdicts.go -source spec-v0.12 -testdata .../testdata -samples DIR > locator-interop.json`.
- `src/lib/dkc/testing/zip-vectors.json`: cómo lee `archive/zip` de Go los ZIP de la página. `scripts/zip-ts-samples.mjs` escribe con `ZipSink` las muestras de `testing/zip.ts` en un directorio: ficheros en carpetas con nombres fuera de ASCII y todas las clases de fecha (1970, 2³¹ − 1, 2³¹, 9999 y ninguna, que toma la hora de la ronda), un fichero dentro de una carpeta, y 65 535 ficheros, que hacen el ZIP64 por número de entradas. `scripts/zip-go-read.go`, solo con la biblioteca estándar, registra el SHA-256 de cada ZIP y una línea por entrada: nombre, bit 11, método, CRC-32, tamaños, fecha leída de los campos extra y SHA-256 del contenido, leído con el CRC comprobado. `zipsink.test.ts` vuelve a escribir cada muestra, exige su SHA-256 y calcula las líneas que Go tiene que dar. Para regenerarlo: `node scripts/zip-ts-samples.mjs DIR > zip-samples.json` y `go run scripts/zip-go-read.go DIR zip-samples.json > zip-vectors.json`.
- `src/lib/dkc/testing/mutation-texts.json`: el texto del error de `capsule.Open` para cada caso de `mutations.json`, u `ok`. Lo escribe `scripts/mutation-go-texts.go`, que reproduce los casos como `internal/testkit` de la referencia, que un módulo de fuera no puede importar: el perfil de Quicknet o ninguno, una fuente con el release del caso, como `OpenOptions.Release` o como `OpenOptions.Source` según su campo `source` (v0.15), la `.dkk` ya decodificada, las identidades, las extensiones del caso con los textos de `testkit.KnownExtensions` y un sumidero que descarta. Comprueba cada código contra el corpus. Para regenerarlo, desde un módulo Go temporal como el de abajo: `go run mutation-go-texts.go ../datekeys-ts/testdata > mutation-texts.json`.
- `src/lib/dkc/testing/mutation-texts.json`: el texto del error de `capsule.Open` para cada caso de `mutations.json`, u `ok`. Lo escribe `scripts/mutation-go-texts.go`, que reproduce los casos como `internal/testkit` de la referencia, que un módulo de fuera no puede importar: el perfil de Quicknet o ninguno, una fuente con el release del caso, como `OpenOptions.Release` o como `OpenOptions.Source` según su campo `source` (v0.15), la `.dkk` ya decodificada, las identidades, las extensiones del caso con los textos de `testkit.KnownExtensions` y un sumidero que descarta. Comprueba cada código contra el corpus. Para regenerarlo, desde un módulo Go temporal como el de abajo: `go run mutation-go-texts.go ../datekeys-ts/testdata > mutation-texts.json`. Se regeneró con `4f78854` (v0.16): solo cambió su campo `spec`.
- `src/lib/dkc/testing/ibe-vectors.json`: los valores de referencia de `ibe.ts`. Los escribe `scripts/ibe-go-vectors.go` con kyber, tlock y `age`, las librerías de la referencia Go, y `ibe.test.ts` los comprueba todos:
- el GT de e(G1, G2) y de su cuadrado, con H2 de 16 y 32 bytes;
- H3 y H4 sobre entradas fijas, entre ellas una H3 aceptada en la segunda iteración y otra en la tercera;
@ -334,7 +335,7 @@ Umbrales de cobertura (`vitest.config.ts`), al 100 % en líneas, ramas, funcione
Para regenerarlo: `node scripts/tlock-ts-samples.mjs > ts-samples.json`, y desde el mismo módulo Go temporal, `go run tlock-go-vectors.go ts-samples.json > tlock-vectors.json`.
En `ibe-vectors.json`, H2, H3 y H4 no son públicas en kyber: el script las reescribe con sus etiquetas y las comprueba en cada fixture contra la file key de tlock y contra U = r·G2. Los cifrados de kyber usan un sigma aleatorio, así que el fichero se genera una vez y se congela. Para regenerarlo, desde un módulo Go temporal que requiera la referencia (`replace g.activething.com/go/DateKeys => ../datekeys-go`, `GOFLAGS=-mod=mod`, y la directiva `go` de la referencia, para que se use su toolchain): `go run ibe-go-vectors.go ../datekeys-ts/testdata/fixtures > ibe-vectors.json`. Los siete fixtures del formato 2 se añadieron el 29-09-2026 así, sobre `spec-v0.9`, tomando solo el bloque `fixtures`: los valores de los cinco anteriores salieron idénticos, y el resto del fichero no cambió. Los nueve del formato 3 se añadieron igual el 30-09-2026, sobre `spec-v0.10`, con los doce anteriores idénticos. El 01-10-2026, sobre `spec-v0.11`, se añadieron `format3_signed`, `format3_signed_cms` y `format3_sealed`, y se rehicieron los dos de `format3_signature_unsupported` y `format3_seal_unsupported`, que Go regeneró con `alg` 4294967295; los demás salieron idénticos.
En `ibe-vectors.json`, H2, H3 y H4 no son públicas en kyber: el script las reescribe con sus etiquetas y las comprueba en cada fixture contra la file key de tlock y contra U = r·G2. Los cifrados de kyber usan un sigma aleatorio, así que el fichero se genera una vez y se congela. Para regenerarlo, desde un módulo Go temporal que requiera la referencia (`replace g.activething.com/go/DateKeys => ../datekeys-go`, `GOFLAGS=-mod=mod`, y la directiva `go` de la referencia, para que se use su toolchain): `go run ibe-go-vectors.go ../datekeys-ts/testdata/fixtures > ibe-vectors.json`. Los siete fixtures del formato 2 se añadieron el 29-09-2026 así, sobre `spec-v0.9`, tomando solo el bloque `fixtures`: los valores de los cinco anteriores salieron idénticos, y el resto del fichero no cambió. Los nueve del formato 3 se añadieron igual el 30-09-2026, sobre `spec-v0.10`, con los doce anteriores idénticos. El 01-10-2026, sobre `spec-v0.11`, se añadieron `format3_signed`, `format3_signed_cms` y `format3_sealed`, y se rehicieron los dos de `format3_signature_unsupported` y `format3_seal_unsupported`, que Go regeneró con `alg` 4294967295; los demás salieron idénticos. El 07-10-2026, sobre `4f78854` (v0.16), se añadieron `format3_full_chunk` y `format3_time_and_key_words`, con los 26 anteriores y el resto del fichero idénticos.
- El writer (`encrypt.test.ts`, `encrypt.stream.test.ts`, `encrypt.internal.test.ts`, `encrypt.property.test.ts`):
- con los valores de su registro, reproduce byte a byte el PRELUDE, PUBLIC_HEADER, `header_binding` y CONTROL_CBOR de los siete fixtures de formato 2 de Go, con las mismas longitudes; cada credencial cae en el hueco del registro y la `.dkk` sale igual, salvo su `capsule_digest`;
- lo que escribe se abre con `open`: las dos políticas, de 1 a 16 credenciales, cada una sola y todas juntas; contenidos en todos los bordes de trozo y de relleno, hasta 5 000 000 de bytes, con las dos reglas; rondas 1000, 1001 y 2000;
@ -355,13 +356,15 @@ Umbrales de cobertura (`vitest.config.ts`), al 100 % en líneas, ramas, funcione
Para regenerarlo, en una exportación `git archive` de `datekeys-go` en el tag `spec-v0.12`, sin tocar el repositorio: las órdenes están en la cabecera del script. Todos los valores salen de Go, y cada ejecución escribe los mismos bytes.
- `src/lib/dkc/gounicode.ts` lo escribe `scripts/go-unicode-tables.go` con el toolchain de Go 1.26 (`go run scripts/go-unicode-tables.go -out src/lib/dkc/gounicode.ts`), y comprueba sus tramos contra las funciones de Go en cada punto de código.
- `src/lib/dkc/testing/signing-vectors.json`: los enganches del escritor contra `capsule.EncryptFiles` de Go, que comprueba `encrypt.signing.test.ts`. Lo escribe `scripts/signing-go-vectors_test.go`, que importa paquetes internos y corre como prueba en una exportación `git archive` de `spec-v0.12`:
- `src/lib/dkc/testing/signing-vectors.json`: los enganches del escritor contra `capsule.EncryptFiles` de Go, que comprueba `encrypt.signing.test.ts`. Lo escribe `scripts/signing-go-vectors_test.go`, que importa paquetes internos y corre como prueba en una exportación `git archive` de `datekeys-go`, hoy de `4f78854`:
- 23 recetas de formato 3 en `time_only`, para la ronda 1000. Go escribe cada cápsula con `crypto/rand` leyendo un flujo ChaCha20 fijo, y guarda cada valor en su orden, y lo que recibió y devolvió cada enganche: una clave de autor de una semilla, y las firmas CMS y los tokens RFC 3161 de `internal/cms/cmstest`, cuyos ECDSA y RSA fija `cryptotest.SetGlobalRandom`;
- con esos valores y esas firmas, `encryptFiles` escribe las ocho cápsulas de Go byte a byte: `alg` 1, `alg` 1 y un sello, un sello solo, uno posterior a la fecha (S5), `alg` 2 con dos firmantes sellados, `alg` 2 en un área de 64 KiB, `largeArea` sin ensanchar y un área de 512 bytes de generador de vectores. Pide la firma y el sello sobre los mismos mensajes, y lee en lo que escribe los veredictos y las líneas de `capsule.Open`, también con la clave guardada (F3). La prueba entrega a `age-encryption` y a `ibe.ts` los valores de Go por `crypto.getRandomValues`, sin el sellado de medida de Go, que esta librería calcula con una fórmula, ni las etiquetas al azar de sus recipients, que `age-encryption` no saca, y deja pasar el cegado de las multiplicaciones de noble, que no cambia ningún resultado;
- con esos valores y esas firmas, `encryptFiles` escribe las ocho cápsulas de Go byte a byte: `alg` 1, `alg` 1 y un sello, un sello solo, uno posterior a la fecha (S5, `late`), `alg` 2 con dos firmantes sellados, `alg` 2 en un área de 64 KiB, `largeArea` sin ensanchar y un área de 512 bytes de generador de vectores. Pide la firma y el sello sobre los mismos mensajes, y lee en lo que escribe los veredictos y las líneas de `capsule.Open`, también con la clave guardada (F3). La prueba entrega a `age-encryption` y a `ibe.ts` los valores de Go por `crypto.getRandomValues`, sin el sellado de medida de Go, que esta librería calcula con una fórmula, ni las etiquetas al azar de sus recipients, que `age-encryption` no saca, y deja pasar el cegado de las multiplicaciones de noble, que no cambia ningún resultado;
- las otras 15 fallan con el texto de Go: las exclusiones, el área de prueba con `largeArea`, una clave de 31 bytes, una firma de ceros (F2), una firma CMS sin un firmante exigido (F5, con el nombre), sin sellos o que no es una firma (F1), un área que no cabe en 32 KiB o en 64 KiB, `SIGNERS` vacío o repetido, y la aplicación de firma o la autoridad que fallan;
- `samples`: cinco cápsulas que `scripts/signing-ts-samples.mjs` escribe con `encryptFiles`, sus propios valores al azar, un `AuthorKey` nuevo y los certificados, firmas y tokens de `testing/cmsbuild.ts`. `capsule.Open` de Go las abre a sus ficheros y les da los mismos veredictos y las mismas líneas que esta librería.
Para regenerarlo: `node scripts/signing-ts-samples.mjs > ts-signing.json`, y la prueba de Go con `-samples ts-signing.json`; las órdenes están en la cabecera del script. Las cápsulas de Go salen igual en cada ejecución; las muestras son aleatorias y se congelan.
Se regeneró el 07-10-2026 en una exportación de `4f78854` (v0.16), con las mismas cinco muestras, cuyo `ts` se volvió a leer con esta librería. Las cápsulas y los errores salen byte a byte iguales; cambian lo que lee `capsule.Open` y un texto de error: los tokens del sellador de Go y los de las muestras no llevan `accuracy`, así que los cinco sellos válidos dan ahora S5, «el sello no dice su precisión», el posterior a la fecha da su motivo, `late`, y la firma de ceros con sello falla con «F2 and S5».
- `src/lib/dkc/testing/capsule-vectors.json`: la interoperabilidad de los writers con Go a nivel de cápsula (plan de la fase 3, sección 8, punto 9, y paso 4 del plan del formato 3), que comprueba `interop.test.ts`. Se regeneró el 02-10-2026 con el escritor de la v0.11, contra `spec-v0.11`. `scripts/capsule-ts-samples.mjs` escribe con `encryptVectors` de `testing/encrypt.ts`, como generador de vectores, trece cápsulas de formato 2 para las rondas 1000, 1001 y 2000:
- `time_only` de 0, 46, 65 536 y 78 000 bytes, con las dos reglas de relleno;
- `time_and_key` con una clave portable, con tres recipients y una clave portable, y con dieciséis recipients;
@ -472,7 +475,7 @@ Comprueba que los ficheros coinciden con `SOURCE.json`, sin faltantes ni sobrant
`.gitattributes` marca `testdata/**` y `wordlists/**` como binarios para que git no altere ningún byte.
Copia actual: la de `testdata/SOURCE.json`, `datekeys-go` en `aefc8f6`, de la rama `v0.15` después del tag `spec-v0.15`, que se sincroniza con `node scripts/sync-testdata.mjs sync --commit aefc8f6`. Del mismo commit vienen `wordlists/` y `annex/`, el anexo de recuperación. Su `testdata` es el del tag (`fe50885`), que añade `vectors/release.json`, los ficheros de `releases/` y el campo `source` de `mutations.json`; y trae `wordkey/lists`, con la lista inglesa de la EFF y la española. La página solo publica la española.
Copia actual: la de `testdata/SOURCE.json`, `datekeys-go` en `4f78854`, el borrador v0.16 de la rama `v0.16`, que se sincroniza con `node scripts/sync-testdata.mjs sync --commit 4f78854`. Del mismo commit vienen `wordlists/` y `annex/`, el anexo de recuperación, que es ya el §79 del borrador v0.16, con la licencia CC BY-ND 4.0 de la especificación en su título y la llave de palabras en 79.7. Sobre el `testdata` de `spec-v0.15` (`fe50885`), el de la v0.16 cambia el campo `spec` de cada fichero a `0.16`, hace de nuevo `vectors/security_cms.json`, con 143 casos y `seal_reason`, añade a `vectors/release.json` los 26 casos de la lectura estricta del JSON de drand y a `vectors/wordkey.json` el vector del anexo, y trae dos fixtures, `format3_time_and_key_words` y `format3_full_chunk`. `wordkey/lists` trae la lista inglesa de la EFF y la española; la página solo publica la española.
## Licencia

@ -1,7 +1,7 @@
{
"module": "g.activething.com/go/DateKeys",
"commit": "aefc8f6dfe89037d71e22d5338a092ff21159429",
"commit": "4f7885495bd6ea666cb444519090fb5f3ea46752",
"files": {
"recovery.md": "c8c9b8815708d963ca6a3d688003bdc5046c499ab034a2d8c98a18c9811d3bd3"
"recovery.md": "c284a263fe0b94f7cf7d179bd2878c331a42e97c57620b57871646a0a0e80a58"
}
}

@ -1,6 +1,6 @@
# Cómo abrir una cápsula DateKeys sin software de DateKeys
Este texto acompaña a una cápsula del tiempo de DateKeys, un fichero `.dkc`: dice cómo abrirla, llegada su fecha, sin ningún software de DateKeys, por si ya no existe. Es el anexo informativo §79 de la especificación del protocolo DateKeys v0.15, cuyo texto tiene el SHA-256 45105e693be4187af4dd30f4d254402612587b6427c746f5d29f07a541c1e3f3. Es el mismo para toda cápsula: no lleva ningún dato de esta.
Este texto acompaña a una cápsula del tiempo de DateKeys, un fichero `.dkc`: dice cómo abrirla, llegada su fecha, sin ningún software de DateKeys, por si ya no existe. Es el anexo informativo §79 de la especificación del protocolo DateKeys v0.16, cuyo texto tiene el SHA-256 c13b598fa688f6cd74b7223f34896c30ff091e05df11dad8a02f1ea6bcbb6fa7. Es el mismo para toda cápsula: no lleva ningún dato de esta. Su licencia es CC BY-ND 4.0, Atribución-SinDerivadas 4.0 Internacional (https://creativecommons.org/licenses/by-nd/4.0/deed.es): se puede copiar y compartir sin cambios, citando su origen.
## 79. Anexo informativo: recuperación sin software DateKeys
@ -13,11 +13,12 @@ Hace falta:
- el `.dkc`;
- el release de su ronda, de cualquier fuente: un relay de drand, un archivo de releases, un servicio de caché (§50) o cualquier copia. No hace falta confiar en quien lo da: se verifica con la clave pública de 79.1 (79.3);
- en `time_and_key`, una credencial: la `.dkk`, la identity `age` de un recipient o las palabras de una llave de palabras (§38.1);
- una librería de BLS12-381 con pairing y con el hash a G1 de RFC 9380, SHA-256, HMAC-SHA256, HKDF-SHA256 (RFC 5869), ChaCha20-Poly1305 (RFC 8439), un decodificador de CBOR y la herramienta `age` (§77) o una librería compatible.
- una librería de BLS12-381 con pairing y con el hash a G1 de RFC 9380, SHA-256, HMAC-SHA256, HKDF-SHA256 (RFC 5869), ChaCha20-Poly1305 (RFC 8439), un decodificador de CBOR y la herramienta `age` (§77) o una librería compatible;
- con una llave de palabras, PBKDF2-HMAC-SHA256 (RFC 8018) y, si las palabras llevan otras letras que las de 79.7, `UnicodeData.txt` de Unicode 18.0.0.
No sirven las herramientas de drand: `tle` pide el release a la red y no acepta uno dado, y `age` no acepta una file key, que es lo que da el stanza tlock (79.4). Por eso este anexo describe esos dos pasos enteros (79.4 y 79.5).
La implementación de referencia lo sigue en `scripts/recovery`, un programa que no importa ningún paquete de DateKeys, tlock ni drand: solo la librería estándar de Go, `golang.org/x/crypto`, `filippo.io/age` y la librería BLS12-381 `drand/kyber-bls12381`. `scripts/recovery_check.sh` abre con él una cápsula `time_only` y otra `time_and_key` de los fixtures oficiales.
La implementación de referencia lo sigue en `scripts/recovery`, un programa que no importa ningún paquete de DateKeys, tlock ni drand: solo la librería estándar de Go, `golang.org/x/crypto`, `filippo.io/age` y la librería BLS12-381 `drand/kyber-bls12381`, con las interfaces de `drand/kyber`. `scripts/recovery_check.sh` abre con él una cápsula `time_only`, otra `time_and_key` con su `.dkk`, otra con una llave de palabras y otra cuyo `PAYLOAD_AGE` acaba en un bloque completo, de los fixtures oficiales. Las palabras se le dan en un fichero de texto, y `UnicodeData.txt`, si hace falta, en otro.
### 79.1 Parámetros de Quicknet
@ -108,7 +109,7 @@ Es la especificación `age` v1 de C2SP (§77), resumida. Un fichero `age` es una
```text
age-encryption.org/v1
-> <tipo> <argumentos…>
<cuerpo del stanza en Base64 sin relleno, líneas de 64 caracteres, la última más corta>
<cuerpo del stanza en Base64 sin relleno, líneas de 64 caracteres, la última más corta, quizá vacía>
--- <MAC en Base64 sin relleno, 43 caracteres>
<payload>
```
@ -117,20 +118,48 @@ Con la file key FK, de 16 bytes:
1. La cabecera: clave_mac = HKDF-SHA256(ikm = FK, salt = vacío, info = `header`), 32 bytes. El MAC es HMAC-SHA256(clave_mac, la cabecera desde `age-encryption.org/v1` hasta `---` inclusive, sin el espacio que lo sigue). Si no coincide con el de la línea `---`, la file key o la cabecera son otras.
2. El payload empieza tras el salto de línea del MAC por un nonce de 16 bytes. clave = HKDF-SHA256(ikm = FK, salt = nonce, info = `payload`), 32 bytes.
3. Lo demás son bloques de ChaCha20-Poly1305 de 65 536 bytes de texto, 65 552 cifrados, el último más corto. El nonce de 12 bytes del bloque n, desde 0, es n en 11 bytes big-endian seguido de 0x01 en el último bloque y de 0x00 en los demás. No hay datos asociados. El último bloque solo puede estar vacío si es el único, y nada sigue al último bloque.
3. Lo demás son bloques de ChaCha20-Poly1305 de 65 536 bytes de texto, 65 552 cifrados; el último puede ser más corto, o estar completo: un plaintext de 65 536 bytes es un solo bloque, completo y marcado como último. El nonce de 12 bytes del bloque n, desde 0, es n en 11 bytes big-endian seguido de 0x01 en el último bloque y de 0x00 en los demás. No hay datos asociados. El último bloque solo puede estar vacío si es el único, y nada sigue al último bloque.
### 79.6 Las capas siguientes
El plaintext de `SEALED_CONTROL` es:
- en `time_only`, `CONTROL_CBOR`;
- en `time_and_key`, otro fichero `age`, `INNER_ACCESS_AGE`, con stanzas X25519, uno por credencial y señuelos hasta 16 en los formatos 2 y 3. Se abre con `age -d -i clave.txt`, con la identity de la credencial en `clave.txt`. La de una `.dkk` es su `access_material`. La `.dkk` empieza por 12 bytes, `DKK1`, `01`, `00`, `00 00` y `BODY_LEN` en 4 bytes big-endian (§40), y le sigue un mapa CBOR cuya clave 5 es ese `access_material`, 32 bytes (§41), y cuya clave 3 es el `capsule_id` de su cápsula. Una llave de palabras da la identity con §38.1.
- en `time_and_key`, otro fichero `age`, `INNER_ACCESS_AGE`, con stanzas X25519, uno por credencial y señuelos hasta 16 en los formatos 2 y 3. Se abre con `age -d -i clave.txt`, con la identity de la credencial en `clave.txt`. La de una `.dkk` es su `access_material`. La `.dkk` empieza por 12 bytes, `DKK1`, `01`, `00`, `00 00` y `BODY_LEN` en 4 bytes big-endian (§40), y le sigue un mapa CBOR cuya clave 5 es ese `access_material`, 32 bytes (§41), y cuya clave 3 es el `capsule_id` de su cápsula. Una llave de palabras da la identity con 79.7.
`CONTROL_CBOR` es un mapa CBOR (§31). Su clave 3 es `I_PAYLOAD`, otra identity X25519 de 32 bytes, y en los formatos 2 y 3 su clave 6 es L, una cadena de 8 bytes con un entero big-endian, no un entero CBOR. Con `I_PAYLOAD`, `age -d -i payload.txt` abre `PAYLOAD_AGE`.
Una identity X25519 de 32 bytes se escribe para `age` en Bech32 (BIP 173, §77), no Bech32m: el prefijo `age-secret-key-`, los 32 bytes reagrupados de 8 en 5 bits con ceros al final, que dan 52 caracteres, y la suma de comprobación de BIP 173, calculada con el prefijo en minúsculas. Después, todo en mayúsculas: `AGE-SECRET-KEY-1…`.
### 79.7 El contenido
### 79.7 La llave de palabras
Unas palabras dan la identity X25519 de una credencial (§38.1):
```text
P = las palabras normalizadas, en UTF-8, separadas por un espacio (0x20)
S = "DateKeys llave de palabras v2|" || chain_hash || "|" || ronda || "|" || capsule_id
id = PBKDF2-HMAC-SHA256(P, S, 600000 iteraciones, 32 bytes) ; RFC 8018
```
En S, `chain_hash` es el de 79.1 y `capsule_id` el de 79.2, en hexadecimal en minúsculas, y la ronda, la de la DateKey en decimal, sin ceros a la izquierda. `id` es la identity, que se escribe para `age` como dice 79.6.
**Normalización sin tablas.** Si el texto solo lleva caracteres ASCII imprimibles (U+0021 a U+007E), los espacios U+0009 a U+000D y U+0020, las letras á, é, í, ó, ú, ü y ñ y sus mayúsculas, y marcas de U+0300 a U+036F, que es lo que da cualquier palabra de las listas de DateKeys, las palabras normalizadas salen así:
1. se quitan las marcas de U+0300 a U+036F;
2. á y Á pasan a a; é y É, a e; í e Í, a i; ó y Ó, a o; ú, ü, Ú y Ü, a u; ñ y Ñ, a n;
3. A a Z pasan a a a z;
4. se parte el texto por los espacios, sin palabras vacías.
El resto se queda: la puntuación y las cifras cuentan, y «perro,» no es «perro».
**Normalización completa.** Para cualquier otro texto, en este orden: la NFD de UAX #15, con la descomposición canónica de cada punto de código (el campo 5 de `UnicodeData.txt`, sin las que llevan una etiqueta entre `<` y `>`, aplicada hasta el final), la de las sílabas Hangul, que se calcula, y la reordenación canónica por la clase de combinación (campo 3); se quitan los puntos de código de U+0300 a U+036F; cada punto de código pasa a su minúscula simple (campo 13), si la tiene; y se parte por los espacios U+0009 a U+000D, U+0020, U+0085, U+00A0, U+1680, U+2000 a U+200A, U+2028, U+2029, U+202F, U+205F y U+3000, sin palabras vacías. Sirve cualquier copia de `UnicodeData.txt` de Unicode 18.0.0 cuyo SHA-256 sea `0736451de439ae7baf1425136617da495e09ee5afbe6e394374db7009ea08950`; unicode.org la publica en `https://www.unicode.org/Public/18.0.0/ucd/UnicodeData.txt`. Otra versión de Unicode puede dar otras palabras: una que asigne un punto de código nuevo, o que cambie una descomposición o una minúscula.
Vectores, con el chain hash de Quicknet, la ronda 1000 y `capsule_id` = `000102030405060708090a0b0c0d0e0f`:
- «perro luna casa verde tren mar» da `id` = `fceec4d8ca8de86c85a1f26ed49f82a2b38431bd0ce36db995ae7dfd49b96e41`;
- el texto «Ñandú», dos espacios, «PINGÜINO», un tabulador (U+0009) y «camión árbol Éter ola» da «nandu pinguino camion arbol eter ola» e `id` = `273295d29370126a3be50b743132718d3cd9137fb3bb4cb20aa23163d2e19bb7`, lo mismo que ese texto con las tildes, la diéresis y la tilde de la eñe escritas como marcas sueltas detrás de su letra (U+0301, U+0308 y U+0303).
### 79.8 El contenido
El plaintext de `PAYLOAD_AGE` es:
@ -153,6 +182,6 @@ La clave 5 del head es la lista de ficheros (§29.4). Cada uno es un mapa:
Sus bytes van de start a end, sin incluir end, contados desde el origen. Las claves 3 y 4 del head son el comentario y el autor declarado: textos del creador que no prueban nada (§29.7). Una ruta que saldría de la carpeta de destino no se escribe.
### 79.8 Lo que el anexo no comprueba
### 79.9 Lo que el anexo no comprueba
Este anexo comprueba lo que decide que el resultado es el correcto: la firma del release, r·G2 == U, los MAC de cada fichero `age` y el SHA-256 de cada fichero. No comprueba, entre otras cosas, la codificación canónica de cada objeto, `header_binding` (§26), los 16 stanzas de `INNER_ACCESS_AGE` (§39), los ceros del relleno (§29.1) ni las reglas de las rutas (§29.5). Una cápsula que el lector de §63 rechazaría puede abrirse siguiendo este anexo; su contenido es el que sellaron las MAC de `age`, pero no tiene la garantía de un lector conforme.

@ -17,9 +17,9 @@
// - a page has an inline style, an event handler attribute or a URL to
// another origin, or a stylesheet imports or references one;
// - the official .dkc fixtures are not shipped byte for byte, or a secret of
// the fixtures (.dkk files, plaintexts, identities, payload identities,
// access material, CONTROL_CBOR, and the heads, salts, comments and paths
// of format 3) is anywhere in the build;
// the fixtures (.dkk files, plaintexts, identities, the text of a key of
// words, payload identities, access material, CONTROL_CBOR, and the heads,
// salts, comments and paths of format 3) is anywhere in the build;
// - a page loads the Unicode tables of the paths of format 3 with its first
// load, not on demand;
// - a page loads the locator of datekeys.capsule (locator.ts, envelope.ts,
@ -236,6 +236,8 @@ for (const name of fixtureFiles) {
if (name.endsWith('.json')) {
const record = JSON.parse(bytes.toString('utf8'));
for (const id of record.identities ?? []) addSecret(`${name} identities`, id);
// The text of the words of a key of words (spec v0.16, annex 79.7), a credential.
addSecret(`${name} words_text`, record.words_text);
addSecret(`${name} payload_identity`, record.payload_identity);
addSecret(`${name} access_material`, record.access_material);
addSecret(`${name} control_cbor`, record.control_cbor);

@ -27,11 +27,12 @@
// lines.
//
// It imports internal packages, so it runs as a test in an export of
// datekeys-go at the tag spec-v0.12 made with git archive, which it does not
// change, never in the repository itself. From the root of this repository:
// datekeys-go made with git archive, which it does not change, never in the
// repository itself: at the tag spec-v0.12 when it was written, and at
// 4f78854, the draft v0.16, since. From the root of this repository:
//
// node scripts/signing-ts-samples.mjs > /tmp/ts-signing.json
// commit=$(git -C ../datekeys-go rev-parse 'spec-v0.12^{commit}')
// commit=$(git -C ../datekeys-go rev-parse '4f78854^{commit}')
// tmp=$(mktemp -d)
// git -C ../datekeys-go archive "$commit" | tar -x -C "$tmp"
// mkdir "$tmp/signingvectors"
@ -42,7 +43,10 @@
// rm -rf "$tmp"
//
// The cases are the same on every run with Go 1.26.8; the samples are
// random, and frozen with what Go gives for them.
// random, and frozen with what Go gives for them. For v0.16 the frozen
// samples were read again, with their "ts" as this library reads them now,
// and the tokens of the sealer, without accuracy, give S5 (spec v0.16,
// §29.11).
package signingvectors
import (

@ -109,6 +109,9 @@ const OID = {
sigTimeStamp: oid('1.2.840.113549.1.9.16.2.14'),
tstInfo: oid('1.2.840.113549.1.9.16.1.4'),
riOCSP: oid('1.3.6.1.5.5.7.16.2'),
// The best practices time-stamp policy of ETSI EN 319 421, whose tokens
// carry accuracy (spec v0.16, §29.11).
btsp: oid('0.4.0.2023.1.1'),
sha256: oid('2.16.840.1.101.3.4.2.1'),
sha384: oid('2.16.840.1.101.3.4.2.2'),
sha512: oid('2.16.840.1.101.3.4.2.3'),
@ -950,9 +953,16 @@ export function checkSigner(s: SignerInfo, message: Uint8Array): CheckResult {
/** A time-stamp token of RFC 3161 read with the profile of spec §29.11. */
export interface Token {
/** t, and the precision of the token, zero when it has none. */
/**
* t, and the precision of the token, zero in the fields it does not carry.
* hasAccuracy is whether it carries the field at all: without it, the token
* does not say its precision (spec v0.16, §29.11).
*/
readonly genTime: Instant;
readonly accuracy: Instant;
readonly hasAccuracy: boolean;
/** The content of the object identifier of its policy, in hexadecimal, as this module compares them. */
readonly policy: string;
/** The algorithm of the messageImprint, and the hash. */
readonly imprintAlg: AlgID;
readonly imprint: Uint8Array;
@ -974,13 +984,13 @@ export function parseToken(b: Uint8Array): Token {
const imprintAlg = parseAlgID(info.imprintAlg);
const signer = sd.signers[0]!;
if (hashOfAlg(imprintAlg) === undefined || params(signer) === undefined || publicKey(signer.cert) === undefined) throw new CmsAlgorithmError();
return { genTime: info.genTime, accuracy: info.accuracy, imprintAlg, imprint: info.hash, tsa: signer.cert, data: sd };
return { genTime: info.genTime, accuracy: info.accuracy, hasAccuracy: info.hasAccuracy, policy: info.policy, imprintAlg, imprint: info.hash, tsa: signer.cert, data: sd };
}
// The TSTInfo of RFC 3161 3.2.1 in DER: the fields in order, each once, and
// nothing after the last. It returns the messageImprint algorithm, as the DER
// of its AlgorithmIdentifier, and the hash.
function parseTSTInfo(b: Uint8Array): { genTime: Instant; accuracy: Instant; imprintAlg: Uint8Array; hash: Uint8Array } {
function parseTSTInfo(b: Uint8Array): { genTime: Instant; accuracy: Instant; hasAccuracy: boolean; policy: string; imprintAlg: Uint8Array; hash: Uint8Array } {
const bad = (what: string): never => {
throw form(`the TSTInfo: ${what}`);
};
@ -999,9 +1009,11 @@ function parseTSTInfo(b: Uint8Array): { genTime: Instant; accuracy: Instant; imp
// The check of the DER read every time of the TSTInfo, genTime among them.
const genTime = parseTime(f[4]!).time;
let accuracy: Instant = { seconds: 0, nanos: 0 };
let hasAccuracy = false;
let rest = f.slice(5);
if (rest.length > 0 && rest[0]![0] === 0x30) {
accuracy = parseAccuracy(rest[0]!, bad);
hasAccuracy = true;
rest = rest.slice(1);
}
if (rest.length > 0 && rest[0]![0] === 0x01) {
@ -1013,7 +1025,7 @@ function parseTSTInfo(b: Uint8Array): { genTime: Instant; accuracy: Instant; imp
if (rest.length > 0 && rest[0]![0] === 0xa0) rest = rest.slice(1); // tsa
if (rest.length > 0 && rest[0]![0] === 0xa1) rest = rest.slice(1); // extensions
if (rest.length !== 0) bad('a field out of its place, or one that does not exist');
return { genTime, accuracy, imprintAlg: mi[0]!, hash: derContent(mi[1]!) };
return { genTime, accuracy, hasAccuracy, policy: oidOf(f[1]!)!, imprintAlg: mi[0]!, hash: derContent(mi[1]!) };
}
// Accuracy: seconds from 0 to 2^31 - 1, and millis and micros from 1 to 999,
@ -1056,6 +1068,15 @@ export function tokenImprintIsSHA256(t: Token): boolean {
return t.imprintAlg.oid === OID.sha256;
}
/**
* Whether the policy of the token is the best practices time-stamp policy of
* ETSI EN 319 421 (0.4.0.2023.1.1), compared by the bytes of its DER, which
* requires accuracy in every token (spec v0.16, §29.11).
*/
export function tokenIsBTSP(t: Token): boolean {
return t.policy === OID.btsp;
}
/**
* Verifies the token over `subject`, the bytes that it seals: the
* message-digest is the hash of the TSTInfo, the signature of the TSA

@ -325,12 +325,13 @@ describe('the hooks', () => {
files,
options({
authorKey: { publicKey: () => key.publicKey(), sign: (m) => ((message = m), key.sign(m)) },
sealer: { seal: (s) => ((subject = s), b.token(s, signedAt, {}, tsa)) },
sealer: { seal: (s) => ((subject = s), b.token(s, signedAt, { accuracy: b.accuracyOf(1) }, tsa)) },
}),
{ payloadIdentity: payloadId },
);
const o = await openedOf(w.dkc!);
expect([o.verdicts, o.author_key, o.area_len]).toEqual([['F4', 'S4'], hx(key.publicKey()), AREA_LEN]);
expect([w.security?.signature, w.security?.seal, w.security?.detail?.sealReason]).toEqual(['F4', 'S4', undefined]);
expect((await openedOf(w.dkc!, { [key.publicString()]: 'mía' })).lines![0]).toBe('Firmado con la clave que guardaste como mía.');
// The control from the capsule: its binding, I_PAYLOAD and L, whatever L is.
const p = split(w.dkc!);
@ -345,6 +346,24 @@ describe('the hooks', () => {
expect(hx(subject)).not.toBe(hx(sealSubject(cc, headDigest(body.head), undefined)));
});
// Spec v0.16, §62.1 rule 19: a seal without accuracy is written, and the verdicts of the area that encryptFiles wrote
// say that it proves nothing before the opening date, so that the writer warns of it; the line of a signer of F6
// whose seal carries none says so too. encrypt, which writes format 2, has no area.
it('return the verdicts of the area they wrote, so that the writer warns of a seal without accuracy', async () => {
const tsa = await b.newECDSA('TSA', 'P-256', from, to);
const ana = await b.newECDSA('Ana', 'P-256', from, to);
const sealed = await encryptFiles(files, options({ sealer: { seal: (s) => b.token(s, signedAt, {}, tsa) } }));
expect([sealed.security?.signature, sealed.security?.seal, sealed.security?.detail?.sealReason]).toEqual(['F0', 'S5', 'no accuracy']);
expect((await openedOf(sealed.dkc!)).lines).toEqual(['Sin firma de autor.', 'No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión.']);
const btsp = await encryptFiles(files, options({ sealer: { seal: (s) => b.token(s, signedAt, { policy: b.BTSP_POLICY }, tsa) } }));
expect(btsp.security?.detail?.sealReason).toBe('no accuracy, BTSP');
const hash = await certHash(ana);
const cms = await encryptFiles(files, options({ cmsSigner: { signers: () => [hash], sign: (m) => b.signature(m, { token: (sig) => b.token(sig, signedAt, {}, tsa) }, ana) } }));
expect([cms.security?.signature, cms.security?.seal, cms.security?.detail?.signers.map((s) => [s.before, s.reason])]).toEqual(['F6', 'S0', [[false, 'no accuracy']]]);
expect((await encryptFiles(files, options())).security).toEqual({ signature: 'F0', seal: 'S0' });
expect((await encryptVectors(new Uint8Array(3), options())).security).toBeUndefined();
});
it('take a CMS signature with certificates, F6, and widen the area only when it is asked and needed', async () => {
const tsa = await b.newECDSA('TSA', 'P-256', from, to);
const ana = await b.newECDSA('Ana', 'P-256', from, to);

@ -91,6 +91,7 @@ interface SignerResult {
result: string;
seal_time?: string;
before_round_time: boolean;
seal_reason?: string;
}
// A signer as the record of the reference writes it.
@ -100,6 +101,7 @@ const resultOf = (s: SignerLine): SignerResult => ({
result: s.result,
...(s.sealTime === undefined ? {} : { seal_time: formatRFC3339(s.sealTime) }),
before_round_time: s.before,
...(s.reason === undefined ? {} : { seal_reason: s.reason }),
});
interface DkkFixture {

@ -29,6 +29,7 @@ import { frame, split } from './testing/capsule.ts';
import { kinds } from './testing/verdicts.ts';
import { h, hx, listTestdata, readBytes, readJSON } from './testing/testdata.ts';
import { applyEdits, edits } from './testing/vectors.ts';
import { normalizeWords, wordKey } from './wordkey.ts';
import { parseX25519Identity, unwrapX25519, x25519PublicKey } from './x25519.ts';
interface Sidecar {
@ -47,6 +48,9 @@ interface Sidecar {
access_key_stanza?: number;
identities?: string[];
identity_stanzas?: number[];
capsule_id: string;
// The text of the words of a key of words, as the person types it (spec v0.16, annex 79.7).
words_text?: string;
// Format 3: the plaintext file is BODY.
area_len?: number;
content_offset?: number;
@ -780,3 +784,37 @@ describe('the identities of open', () => {
);
});
});
// Spec v0.16, annex 79.7 and §38.1: format3_time_and_key_words opens with the
// identity that its words give, the text of the second vector of the annex
// with capitals, accents, two spaces and a tab, as TestFixtureWords of Go.
describe('format3_time_and_key_words', () => {
it('opens with the identity that the words of its record give', async () => {
const f = fixture('format3_time_and_key_words');
expect([f.side.words_text, f.side.identities?.length, f.side.access_key_file]).toEqual(['Ñandú PINGÜINO camión árbol Éter ola', 1, undefined]);
const words = await normalizeWords(f.side.words_text!);
expect(words.join(' ')).toBe('nandu pinguino camion arbol eter ola');
const id = await wordKey(words, chainHashHex(quicknet()), f.release.round, h(f.side.capsule_id));
expect(hx(id)).toBe(hx(parseX25519Identity(f.side.identities![0]!)));
const sink = new MemorySink();
const r = await open(f.dkc, options(f, { identities: [id], sink }));
expect(r.error).toBeUndefined();
expectFiles(f, r, sink);
// The same text with its marks apart gives the same words.
expect(await normalizeWords(f.side.words_text!.normalize('NFD'))).toEqual(words);
});
});
// Spec v0.16, annex 79.5: the last STREAM chunk of age may be full. In
// format3_full_chunk, BODY and P measure 65536 bytes, so PAYLOAD_AGE is one
// full chunk, marked as the last.
describe('format3_full_chunk', () => {
it('opens a PAYLOAD_AGE that ends in a full chunk', async () => {
const f = fixture('format3_full_chunk');
expect([f.side.payload_length, f.side.padded_length, f.plaintext.length]).toEqual([65536, 65536, 65536]);
const sink = new MemorySink();
const r = await open(f.dkc, options(f, { sink }));
expect(r.error).toBeUndefined();
expectFiles(f, r, sink);
});
});

@ -1,6 +1,8 @@
// Tests of releaseobject.ts, the release object, drand's JSON and the local
// archive of spec v0.15 (§47.1, §50): what vectors/release.json does not
// reach, with the texts of provider/release.go and provider/archive.go.
// archive of spec v0.15 (§47.1, §50), drand's JSON read strictly since v0.16:
// what vectors/release.json does not reach, with the texts of
// provider/release.go and provider/archive.go, and the cases of
// provider/drandjson_test.go.
import { describe, expect, it } from 'vitest';
import { concatBytes, toHex } from './bytes.ts';
@ -12,10 +14,13 @@ import {
encodeRelease,
isDrandJSON,
isReleaseArchive,
jsonRound,
newReleaseObject,
parseDrandJSON,
parseRelease,
type Release,
ReleaseArchive,
strictJSON,
verifyRelease,
} from './release.ts';
import { expectCode, h, readJSON } from './testing/testdata.ts';
@ -25,6 +30,8 @@ const sig1000 = signature('time_only');
const sig1001 = signature('empty_payload');
const rel = (round: number, sig: Uint8Array): Release => ({ round, signature: sig });
const S = toHex(sig1000);
// The randomness of round 1000, SHA-256 of its signature, as drand's API gives it.
const R = 'fe290beca10872ef2fb164d2aa4442de4566183ec51c56ff3cd603d930e54fdd';
const enc = new TextEncoder();
const MALFORMED = 'provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID';
@ -62,35 +69,155 @@ describe("parseRelease of drand's JSON", () => {
expect(isDrandJSON(enc.encode('{'))).toBe(false);
});
it('reads it as encoding/json reads it into the struct of the reference', async () => {
// Keys without case, the last one winning; other members ignored,
// nested values included; null unsets round and signature.
expect(await parsed(`{"ROUND":1,"Signature":"${S}","x":{"a":[1,"}",{"b":null}]},"y":[]}`)).toBe('ok 1 48');
expect(await parsed(`{"round":1,"round":2,"signature":"${S}"}`)).toBe('ok 2 48');
expect(await parsed(`{"round":1,"Round":null,"signature":"${S}"}`)).toBe(MALFORMED);
expect(await parsed(`{"round":1,"signature":"${S}","signature":null}`)).toBe(MALFORMED);
expect(await parsed(`{"round":1,"ſignature":"${S}"}`)).toBe('ok 1 48');
expect(await parsed(`{"round":1,"signature":"\\u0062${S.slice(1)}","randomness":null}`)).toBe('ok 1 48');
expect(await parsed(`{"round":1,"signature":"","randomness":""}`)).toBe('ok 1 0');
expect(await parsed(`{ "round" : 1 , "e" : "\\"}" , "signature" : "${S}" }`)).toBe('ok 1 48');
// Spec v0.16, §47.1: names compared exactly once their escapes are decoded, no name twice in any object, round a
// number of 1 to 2^53 - 1 without sign, fraction or exponent, and signature and randomness strings. release.json has
// the cases of Go; these are those whose escapes its generator wrote as plain text, and a few more.
it('reads it strictly', async () => {
expect(await parsed(`{"\\u0072ound":1,"signature":"${S}"}`)).toBe('ok 1 48');
expect(await parsed(`{"round":1,"\\u0072ound":1,"signature":"${S}"}`)).toBe(MALFORMED);
expect(await parsed(`{"round":1,"signature":"${S}","\\u0073ignature":"${S}"}`)).toBe(MALFORMED);
expect(await parsed(`{"ROUND":1,"Signature":"${S}"}`)).toBe(MALFORMED);
expect(await parsed(`{"round":1,"ſignature":"${S}"}`)).toBe(MALFORMED);
expect(await parsed(`{"round":1,"signature":"\\u0062${S.slice(1)}"}`)).toBe('ok 1 48');
expect(await parsed(`{"round":1,"signature":"${S.toUpperCase()}","randomness":"${R.toUpperCase()}"}`)).toBe('ok 1 48');
expect(await parsed(`{"round":1,"signature":""}`)).toBe('ok 1 0');
expect(await parsed(`{ "round" : 1 , "e" : "\\"}" , "signature" : "${S}" , "x":{"a":[1,"}",{"b":null}]},"y":[] }`)).toBe('ok 1 48');
expect(await parsed('{}')).toBe(MALFORMED);
expect(await parsed(`{"signature":"${S}"}`)).toBe(MALFORMED);
expect(await parsed('{"round":1}')).toBe(MALFORMED);
expect(await parsed('{"round":1,"signature":"abc"}')).toBe('provider: drand JSON: signature is not hex: ERR_RELEASE_INVALID');
expect(await parsed(`{"round":1,"signature":"${S}","randomness":"${R.slice(2)}"}`)).toBe(
'provider: drand JSON: randomness does not match the signature: ERR_RELEASE_INVALID',
);
// Not UTF-8, as Go's utf8.Valid: a byte that starts nothing, and a surrogate written in UTF-8.
for (const bad of [Uint8Array.of(0xff), Uint8Array.of(0xed, 0xa0, 0x80)]) {
const b = concatBytes(enc.encode(`{"round":1,"signature":"${S}","note":"`), bad, enc.encode('"}'));
await expect(parseDrandJSON(b)).rejects.toThrow(MALFORMED);
}
// A byte order mark is not a space of JSON.
await expect(parseDrandJSON(enc.encode(`{"round":1,"signature":"${S}"}`))).rejects.toThrow(MALFORMED);
});
it('fails a value of another type for a field it reads, and a round that is not a uint64', async () => {
for (const r of ['-1', '1.0', '1e3', '18446744073709551616', '"1"', 'true', '{}', '[]']) {
it('fails a value of another type for a field it reads, and a round that is not one', async () => {
for (const r of ['-1', '0', '1.0', '1e3', '01', '9007199254740992', '18446744073709551616', '"1"', 'true', 'null', '{}', '[]']) {
expect(await parsed(`{"round":${r},"signature":"${S}"}`), r).toBe(MALFORMED);
}
expect(await parsed('{"round":1,"signature":1}')).toBe(MALFORMED);
expect(await parsed('{"round":1,"signature":null}')).toBe(MALFORMED);
expect(await parsed(`{"round":1,"signature":"${S}","randomness":1}`)).toBe(MALFORMED);
expect(await parsed(`{"round":1,"signature":"${S}","randomness":null}`)).toBe(MALFORMED);
expect(await parsed(`{"round":1,"signature":"${S}"} x`)).toBe(MALFORMED);
});
it('keeps a round above 2^53-1 exact, for the text of ERR_ROUND_MISMATCH', async () => {
const big = await parseRelease(enc.encode(`{"round":18446744073709551615,"signature":"${S}"}`));
expect(big.round).toBe(18446744073709551615n);
expectCode(() => verifyRelease(quicknet(), 1000, big), 'ERR_ROUND_MISMATCH', /^provider: release for round 18446744073709551615, expected 1000: /);
expect((await parseRelease(enc.encode(`{"round":9007199254740991,"signature":"${S}"}`))).round).toBe(9007199254740991);
it('reads a round of 2^53 - 1 as a number, for the text of ERR_ROUND_MISMATCH', async () => {
const big = await parseRelease(enc.encode(`{"round":9007199254740991,"signature":"${S}"}`));
expect(big.round).toBe(9007199254740991);
expectCode(() => verifyRelease(quicknet(), 1000, big), 'ERR_ROUND_MISMATCH', /^provider: release for round 9007199254740991, expected 1000: /);
});
});
// Spec v0.16, §47.1: the strict reading of drand's JSON, at the edges of the grammar of RFC 8259 that release.json does
// not reach, as provider.TestStrictJSON of the reference, with its escapes written as escapes.
describe('strictJSON', () => {
it.each([
['{}', true],
[' {"a":1} ', true],
['\t{"a":1}\r\n', true],
['{"a":[]}', true],
['{"a":[1,2,[3,{}]]}', true],
['{"a":true,"b":false,"c":null}', true],
['{"a":-0,"b":0.5,"c":1E+2,"d":1e-2,"e":-12.25e3}', true],
['{"a":"\\"\\\\\\/\\b\\f\\n\\r\\té"}', true],
['{"\\u00e9":1,"é":2}', false], // one name, escaped and not
['{"\\u00E9":1,"\\u00e9":2}', false],
['{"a":1,"a":2}', false],
['{"a":{"b":1},"c":{"b":2}}', true], // the same name in two objects
['{"a":[{"b":1,"b":1}]}', false],
['{"a":{"b":1,"b":1}}', false],
['{"a":01}', false],
['{"a":1.}', false],
['{"a":.5}', false],
['{"a":1e}', false],
['{"a":1e+}', false],
['{"a":+1}', false],
['{"a":-}', false],
['{"a":tru}', false],
['{"a":fals}', false],
['{"a":nul}', false],
['{"a":x}', false],
['{"a":"\\x"}', false],
['{"a":"\\u12"}', false],
['{"a":"\\u12g4"}', false],
['{"a":"\\ud800"}', false],
['{"a":"\\ud800A"}', false],
['{"a":"\\ud800x"}', false],
['{"a":"\\ud800\\u0041"}', false],
['{"a":"\\ud800\\ud800"}', false],
['{"a":"\\ud800\\ue000"}', false],
['{"a":"\\ud800\\u12"}', false],
['{"a":"\\ue000\\u0041"}', true],
['{"a":"\\udfff\\ud800"}', false],
['{"a":"\\ud83d\\ude00"}', true],
['{"a":"\\uD83D\\uDE00"}', true],
['{"a":"😀"}', true],
['{"a":"\u0001"}', false],
['{"a":"a\tb"}', false],
['{"a":"\\', false],
['{"a":"b', false],
['{"a":1,}', false],
['{,"a":1}', false],
['{"a" 1}', false],
['{"a":1 "b":2}', false],
['{a:1}', false],
['{"a":[1,]}', false],
['{"a":[1 2]}', false],
['{"a":[1', false],
['{"a":"b"', false],
['{"a":', false],
['{"a"', false],
['{', false],
['{"a":1}x', false],
['[]', false],
['"a"', false],
['', false],
['{"a":1}\v', false], // not a space of JSON
['{"a":1} ', false],
])('%j: %s', (input, ok) => {
expect(strictJSON(enc.encode(input)) !== undefined).toBe(ok);
});
it('gives the members of the outer object, in their order, with their names and strings decoded', () => {
const m = strictJSON(enc.encode('{"round":1000,"no\\u0074e":"a\\ud83d\\ude00","n":-1.5,"o":{"p":[]},"t":true}'));
expect(m).toEqual([
{ name: 'round', kind: '0', raw: '1000', str: '' },
{ name: 'note', kind: '"', raw: '"a\\ud83d\\ude00"', str: 'a\u{1f600}' },
{ name: 'n', kind: '0', raw: '-1.5', str: '' },
{ name: 'o', kind: '{', raw: '{"p":[]}', str: '' },
{ name: 't', kind: 't', raw: 'true', str: '' },
]);
expect(strictJSON(Uint8Array.of(0x7b, 0x22, 0x61, 0x22, 0x3a, 0x22, 0xff, 0x22, 0x7d))).toBeUndefined();
});
});
describe('jsonRound', () => {
it.each([
['1', 1],
['1000', 1000],
['9007199254740991', 9007199254740991],
['0', undefined],
['9007199254740992', undefined],
['9999999999999999', undefined],
['99999999999999999', undefined],
['-1', undefined],
['1.0', undefined],
['1e3', undefined],
['01', undefined],
])('%s', (raw, want) => {
expect(jsonRound({ name: 'round', kind: '0', raw, str: '' })).toBe(want);
});
it('is not a string', () => {
expect(jsonRound({ name: 'round', kind: '"', raw: '"1"', str: '1' })).toBeUndefined();
});
});

@ -1,8 +1,9 @@
// The release object of spec v0.15, §47.1, as provider/release.go and
// provider/archive.go of the Go reference: the release of a round as data
// that is kept: an entry of a release cache or archive and the answer of a release cache
// or of the Release API; drand's JSON, which a reader accepts too as the
// input of the caller; the sources of a release in the caller's hand
// The release object of spec v0.15, §47.1, as provider/release.go,
// provider/drandjson.go and provider/archive.go of the Go reference: the
// release of a round as data that is kept: an entry of a release cache or
// archive and the answer of a release cache or of the Release API; drand's
// JSON, which a reader accepts too as the input of the caller, read strictly
// since spec v0.16; the sources of a release in the caller's hand
// (provider.Supplier); and a local release archive, the informative format
// of §50. The texts of the errors are those of the reference.
//
@ -10,7 +11,15 @@
// only decodes, so that the page can read what the person gives before
// loading the code that opens a capsule.
import { equalBytes, goQuote, sha256, toHex, utf8Length } from "./bytes.ts";
import {
decodeUtf8,
equalBytes,
fromHex,
goQuote,
sha256,
toHex,
utf8Length,
} from "./bytes.ts";
import {
checkSchema,
Decoder,
@ -63,16 +72,11 @@ export interface Release {
}
/**
* A release as the caller gives it, before step 10. Its round is a bigint
* only when drand's JSON names a round above 2^53-1, which no DateKey has:
* step 10 then reports ERR_ROUND_MISMATCH with its exact digits, as Go's
* uint64 does.
* A release as the caller gives it, before step 10. Since spec v0.16 the
* round of drand's JSON is at most 2^53 - 1, as that of a release object
* (§47.1), so it is a Release.
*/
export interface ParsedRelease {
readonly round: number | bigint;
readonly signature: Uint8Array;
readonly chainHash?: Uint8Array;
}
export type ParsedRelease = Release;
// ---------------------------------------------------------------------------
// The release object
@ -211,24 +215,259 @@ export function isDrandJSON(b: Uint8Array): boolean {
/**
* Reads a release that the caller supplies, as provider.ParseRelease:
* drand's JSON when isDrandJSON, or else a release object, with
* decodeRelease. drand's JSON is the answer of a relay, {"round": …,
* "signature": "…"}, with an optional "randomness" that must be SHA-256 of
* the signature; it does not name its chain, so the release has no chain
* hash, and any failure to read it is ERR_RELEASE_INVALID. It is accepted as
* input, never written.
* drand's JSON when isDrandJSON, read strictly by parseDrandJSON, or else a
* release object, with decodeRelease. drand's JSON is the answer of a relay,
* {"round": …, "signature": "…"}, with an optional "randomness" that must be
* SHA-256 of the signature; it does not name its chain, so the release has no
* chain hash, and any failure to read it is ERR_RELEASE_INVALID. It is
* accepted as input, never written.
*/
export async function parseRelease(b: Uint8Array): Promise<ParsedRelease> {
return isDrandJSON(b) ? parseDrandJSON(b) : decodeRelease(b);
}
// The JSON of a drand relay, read as Go's encoding/json reads it into
// {Round *uint64; Signature *string; Randomness string}: the members of the
// object in order, each matched to a field by its name without case, the
// last one winning; null leaves a field as it was for Randomness and unset
// for the other two; a value of another type, or a round that is not an
// integer of 0 to 2^64-1, fails the whole input.
async function parseDrandJSON(b: Uint8Array): Promise<ParsedRelease> {
// ---------------------------------------------------------------------------
// drand's JSON, read strictly
//
// The strict reading of drand's JSON (spec v0.16, §47.1), as
// provider/drandjson.go of the reference: JSON of RFC 8259, in UTF-8, whose
// value is an object; no object of the JSON repeats a name, and names are
// compared exactly, code point by code point, once their escapes are
// decoded, so that "round" is round and Round is another name; and an
// escape of a surrogate that does not pair with the next one makes the JSON
// malformed. A common JSON reader keeps the last of two repeated names, or
// does not tell upper from lower case in them, and two readers would see two
// rounds in the same input.
/**
* A member of the outer object of drand's JSON: its name, decoded; the kind
* of its value, its first character ('"', '{', '[', 't', 'f' or 'n'), or '0'
* for a number; the text of the value as written; and for a string, the
* string decoded.
*/
export interface JSONMember {
readonly name: string;
readonly kind: string;
readonly raw: string;
readonly str: string;
}
/**
* Reads `b` as a JSON object with the strict rules of spec v0.16, §47.1, and
* returns the members of the outer object in their order, or undefined when
* `b` breaks one: it is not UTF-8, its value is not an object, an object
* repeats a name, or a string escapes a surrogate without its pair.
*/
export function strictJSON(b: Uint8Array): JSONMember[] | undefined {
const text = decodeUtf8(b);
if (text === undefined) return undefined;
const r = new JSONReader(text);
r.space();
if (!r.at("{")) return undefined;
const members = r.object(true);
r.space();
return members !== undefined && r.i === text.length ? members : undefined;
}
// Reads JSON from the text s at i. The text is decoded UTF-8, so it holds no
// lone surrogate, and a byte order mark at its start stays, as a character
// that is not a space of JSON.
class JSONReader {
i = 0;
private readonly s: string;
constructor(s: string) {
this.s = s;
}
at(c: string): boolean {
return this.s[this.i] === c;
}
// Skips the four spaces of JSON.
space(): void {
while (this.i < this.s.length && " \t\n\r".includes(this.s[this.i]!))
this.i++;
}
// One value of any kind, undefined when it breaks the grammar.
value(): Omit<JSONMember, "name"> | undefined {
const start = this.i;
const c = this.s[this.i];
let kind = c;
let str = "";
let ok = false;
if (c === "{") ok = this.object(false) !== undefined;
else if (c === "[") ok = this.array();
else if (c === '"') {
const v = this.string();
ok = v !== undefined;
str = v ?? "";
} else if (c === "t") ok = this.literal("true");
else if (c === "f") ok = this.literal("false");
else if (c === "n") ok = this.literal("null");
else if (c === "-" || (c !== undefined && c >= "0" && c <= "9")) {
kind = "0";
ok = this.number();
}
return ok
? { kind: kind!, raw: this.s.slice(start, this.i), str }
: undefined;
}
// An object, with no name twice; its members when keep is set.
object(keep: boolean): JSONMember[] | undefined {
this.i++; // {
this.space();
const out: JSONMember[] = [];
if (this.at("}")) {
this.i++;
return out;
}
const seen = new Set<string>();
for (;;) {
this.space();
if (!this.at('"')) return undefined;
const name = this.string();
if (name === undefined || seen.has(name)) return undefined;
seen.add(name);
this.space();
if (!this.at(":")) return undefined;
this.i++;
this.space();
const m = this.value();
if (m === undefined) return undefined;
if (keep) out.push({ name, ...m });
this.space();
if (this.at(",")) this.i++;
else if (this.at("}")) {
this.i++;
return out;
} else return undefined;
}
}
array(): boolean {
this.i++; // [
this.space();
if (this.at("]")) {
this.i++;
return true;
}
for (;;) {
this.space();
if (this.value() === undefined) return false;
this.space();
if (this.at(",")) this.i++;
else if (this.at("]")) {
this.i++;
return true;
} else return false;
}
}
// A string, with its escapes decoded; a surrogate escaped alone, without
// its pair, breaks it.
string(): string | undefined {
this.i++; // "
let out = "";
while (this.i < this.s.length) {
const c = this.s[this.i]!;
if (c === '"') {
this.i++;
return out;
}
if (c < " ") return undefined;
if (c !== "\\") {
out += c;
this.i++;
continue;
}
const e = this.s[this.i + 1];
this.i += 2;
const k = e === undefined ? -1 : '"\\/bfnrt'.indexOf(e);
if (k >= 0) {
out += '"\\/\b\f\n\r\t'[k];
continue;
}
if (e !== "u") return undefined;
let u = this.hex4();
if (u === undefined || (u >= 0xdc00 && u <= 0xdfff)) return undefined;
if (u >= 0xd800 && u <= 0xdbff) {
if (!this.s.startsWith("\\u", this.i)) return undefined;
this.i += 2;
const low = this.hex4();
if (low === undefined || low < 0xdc00 || low > 0xdfff)
return undefined;
u = 0x10000 + ((u - 0xd800) << 10) + (low - 0xdc00);
}
out += String.fromCodePoint(u);
}
return undefined;
}
// The four hexadecimal digits of an escape \u.
hex4(): number | undefined {
const h = this.s.slice(this.i, this.i + 4);
if (!/^[0-9a-fA-F]{4}$/.test(h)) return undefined;
this.i += 4;
return parseInt(h, 16);
}
literal(word: string): boolean {
if (!this.s.startsWith(word, this.i)) return false;
this.i += word.length;
return true;
}
// A number of the grammar of RFC 8259: a minus, an integer part without
// leading zeros, and an optional fraction and exponent.
number(): boolean {
const digits = (): number => {
const from = this.i;
while (this.i < this.s.length && /[0-9]/.test(this.s[this.i]!))
this.i++;
return this.i - from;
};
if (this.at("-")) this.i++;
if (this.at("0")) this.i++;
else if (digits() === 0) return false;
if (this.at(".")) {
this.i++;
if (digits() === 0) return false;
}
if (this.at("e") || this.at("E")) {
this.i++;
if (this.at("+") || this.at("-")) this.i++;
if (digits() === 0) return false;
}
return true;
}
}
/**
* The round of drand's JSON (spec v0.16, §47.1): a number without sign,
* fraction or exponent, from 1 to 2^53 - 1, as in the release object;
* undefined for any other member.
*/
export function jsonRound(m: JSONMember): number | undefined {
if (m.kind !== "0" || !/^[1-9][0-9]{0,15}$/.test(m.raw)) return undefined;
const n = Number(m.raw);
return n <= MAX_SAFE_UINT ? n : undefined;
}
/**
* Reads the JSON of a drand relay with the strict rules of spec v0.16, §47.1,
* as provider.ParseDrandJSON: at most MAX_RELEASE_JSON_SIZE bytes of JSON
* whose value is an object, with no repeated name and names compared exactly
* once their escapes are decoded; "round" a number without sign, fraction or
* exponent, from 1 to 2^53 - 1; "signature" a string of hexadecimal, in lower
* or upper case; and "randomness", when present, a string with SHA-256 of the
* signature in hexadecimal. Other members are ignored. Any failure is
* ERR_RELEASE_INVALID, with the texts of the reference. The release names no
* chain. The page reads the answers of the relays of drand with it too.
*/
export async function parseDrandJSON(b: Uint8Array): Promise<Release> {
if (b.length > MAX_RELEASE_JSON_SIZE) {
throw new DateKeysError(
"ERR_RELEASE_INVALID",
@ -240,45 +479,31 @@ async function parseDrandJSON(b: Uint8Array): Promise<ParsedRelease> {
"ERR_RELEASE_INVALID",
"provider: drand JSON: malformed, or without round or signature",
);
const text = new TextDecoder().decode(b);
const members = jsonMembers(text);
const members = strictJSON(b);
if (members === undefined) throw malformed();
let round: bigint | undefined;
let round: number | undefined;
let signature: string | undefined;
let randomness = "";
let typeError = false;
for (const [key, raw] of members) {
const name = foldName(key);
if (name === "round") {
if (raw === "null") round = undefined;
else if (/^(0|[1-9][0-9]*)$/.test(raw) && BigInt(raw) < 2n ** 64n)
round = BigInt(raw);
else typeError = true;
} else if (name === "signature" || name === "randomness") {
if (raw === "null") {
if (name === "signature") signature = undefined;
} else if (raw.startsWith('"')) {
const v = JSON.parse(raw) as string;
if (name === "signature") signature = v;
else randomness = v;
} else {
typeError = true;
}
let randomness: string | undefined;
for (const m of members) {
if (m.name === "round") {
round = jsonRound(m);
if (round === undefined) throw malformed();
} else if (m.name === "signature" || m.name === "randomness") {
if (m.kind !== '"') throw malformed();
if (m.name === "signature") signature = m.str;
else randomness = m.str;
}
}
if (typeError || round === undefined || signature === undefined)
throw malformed();
if (round === undefined || signature === undefined) throw malformed();
if (!/^([0-9a-fA-F]{2})*$/.test(signature)) {
throw new DateKeysError(
"ERR_RELEASE_INVALID",
"provider: drand JSON: signature is not hex",
);
}
const sig = Uint8Array.from(signature.match(/../g) ?? [], (h) =>
parseInt(h, 16),
);
const sig = fromHex(signature);
if (
randomness !== "" &&
randomness !== undefined &&
randomness.toLowerCase() !== toHex(await sha256(sig))
) {
throw new DateKeysError(
@ -286,81 +511,7 @@ async function parseDrandJSON(b: Uint8Array): Promise<ParsedRelease> {
"provider: drand JSON: randomness does not match the signature",
);
}
return {
round: round <= BigInt(MAX_SAFE_UINT) ? Number(round) : round,
signature: sig,
};
}
// Go's encoding/json matches a key to a field name without case: ASCII
// letters in lower case, and any other character as unicode.ToLower of
// unicode.ToUpper, so that U+017F (long s) is an s and U+212A (Kelvin) a k.
function foldName(s: string): string {
let out = "";
for (const c of s)
out +=
c.charCodeAt(0) < 0x80 ? c.toLowerCase() : c.toUpperCase().toLowerCase();
return out;
}
// The members of the object that `text` is, as [key, raw value text] in
// their order, or undefined when `text` is not one JSON object. JSON.parse
// checks the grammar, the same as Go's; the scan then only splits valid text.
function jsonMembers(text: string): [string, string][] | undefined {
// Its first byte other than a space is "{": valid JSON is an object.
try {
JSON.parse(text);
} catch {
return undefined;
}
const out: [string, string][] = [];
let i = 0;
const ws = (): void => {
while (i < text.length && " \t\n\r".includes(text[i]!)) i++;
};
const skipString = (): void => {
i++;
while (text[i] !== '"') i += text[i] === "\\" ? 2 : 1;
i++;
};
const skipValue = (): void => {
if (text[i] === '"') return skipString();
if (text[i] === "{" || text[i] === "[") {
let depth = 0;
do {
const c = text[i]!;
if (c === '"') {
skipString();
continue;
}
if (c === "{" || c === "[") depth++;
else if (c === "}" || c === "]") depth--;
i++;
} while (depth > 0);
return;
}
while (i < text.length && !",}] \t\n\r".includes(text[i]!)) i++;
};
ws();
i++; // {
ws();
while (text[i] !== "}") {
const k0 = i;
skipString();
const key = JSON.parse(text.slice(k0, i)) as string;
ws();
i++; // :
ws();
const v0 = i;
skipValue();
out.push([key, text.slice(v0, i)]);
ws();
if (text[i] === ",") {
i++;
ws();
}
}
return out;
return { round, signature: sig };
}
/**

@ -9,7 +9,7 @@
import { describe, expect, it } from 'vitest';
import { h, hx } from './testing/testdata.ts';
import { arr, b, bn, map, t, u } from './testing/cborhex.ts';
import { encodeSecurity, evaluateSecurity, type Verdict, verdictLines, verdictText } from './security.ts';
import { encodeSecurity, evaluateSecurity, sealReasonText, type Verdict, verdictLines, verdictText } from './security.ts';
const EMPTY = 'a20071646174656b6579732d73656375726974790101';
// An author-signature of alg 1 with a key of 32 zero bytes and a signature
@ -86,21 +86,27 @@ describe('verdictLines', () => {
expect(verdictText('S0')).toBe('');
});
// F6 and S4 write the names that the reader found (spec v0.12 §29.7, §29.10), each between « and »: a signer sealed
// before the round time or not, with the authority of its seal, the warning that DateKeys does not check who issued
// the seals, and a signer who does not count, with its result in Spanish.
// F6 and S4 write the names that the reader found (spec v0.16 §29.7, §29.10), each between « and »: a signer whose seal
// proves that it came before the round time or not, and then why not, with the authority of its seal, the warning
// that DateKeys does not check who issued the seals, and a signer who does not count, with its result in Spanish.
it('writes the lines of F6 and S4 from the signers and the authorities that were found', () => {
const t = { seconds: 1_790_000_000, nanos: 0 };
const line = (holder: string, before: boolean, result = 'valid') => ({ holder, issuer: `emisor de ${holder}`, result, sealHolder: `TSA de ${holder}`, sealTime: t, before });
const lines = verdictLines({
signature: 'F6',
seal: 'S4',
detail: { signers: [line('Ana', true), line('Luis', false)], foreign: [line('Otro', true, 'invalid')], sealHolder: 'TSA', sealTime: t },
detail: {
signers: [line('Ana', true), { ...line('Luis', false), reason: 'late' }, { ...line('Eva', false), reason: 'no accuracy, BTSP' }],
foreign: [line('Otro', true, 'invalid')],
sealHolder: 'TSA',
sealTime: t,
},
});
expect(lines).toEqual([
'Firmado con un certificado a nombre de «Ana», «Luis». DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.',
'Firmado con un certificado a nombre de «Ana», «Luis», «Eva». DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.',
' «Ana» (emisor según su certificado: «emisor de Ana»), sellado por «TSA de Ana» el 2026-09-21T14:13:20Z, antes de la fecha de apertura.',
' «Luis» (emisor según su certificado: «emisor de Luis»), sellado por «TSA de Luis» el 2026-09-21T14:13:20Z, no antes de la fecha de apertura.',
' «Luis» (emisor según su certificado: «emisor de Luis»), sellado por «TSA de Luis» el 2026-09-21T14:13:20Z, sin acreditar que fuera antes de la fecha de apertura: se selló después de esa fecha o demasiado cerca de ella.',
' «Eva» (emisor según su certificado: «emisor de Eva»), sellado por «TSA de Eva» el 2026-09-21T14:13:20Z, sin acreditar que fuera antes de la fecha de apertura: el sello no dice la precisión que exige su política.',
' DateKeys no comprueba quién emitió los sellos.',
' Otro firmante, «Otro»: inválida. No cuenta.',
'Según un sello a nombre de «TSA», existía el 2026-09-21T14:13:20Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.',
@ -110,10 +116,10 @@ describe('verdictLines', () => {
// No line says "antes de la fecha de apertura": no warning. A time keeps the fraction of its seal (RFC 3339).
it('warns of who issued the seals only when a signer was sealed before the date, and writes the fraction of a time', () => {
const t = { seconds: 1_790_000_000, nanos: 250_000_000 };
const late = { holder: 'Ana', issuer: 'CA', result: 'valid', sealHolder: 'TSA', sealTime: t, before: false };
const late = { holder: 'Ana', issuer: 'CA', result: 'valid', sealHolder: 'TSA', sealTime: t, before: false, reason: 'no accuracy' } as const;
expect(verdictLines({ signature: 'F6', seal: 'S0', detail: { signers: [late], foreign: [] } })).toEqual([
'Firmado con un certificado a nombre de «Ana». DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.',
' «Ana» (emisor según su certificado: «CA»), sellado por «TSA» el 2026-09-21T14:13:20.25Z, no antes de la fecha de apertura.',
' «Ana» (emisor según su certificado: «CA»), sellado por «TSA» el 2026-09-21T14:13:20.25Z, sin acreditar que fuera antes de la fecha de apertura: el sello no dice su precisión.',
]);
expect(verdictLines({ signature: 'F4', seal: 'S4', authorKey: new Uint8Array(32), detail: { signers: [], foreign: [], sealHolder: 'TSA', sealTime: { seconds: t.seconds, nanos: 1 } } })[1]).toBe(
'Según un sello a nombre de «TSA», existía el 2026-09-21T14:13:20.000000001Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.',
@ -133,13 +139,26 @@ describe('verdictLines', () => {
expect(verdictLines({ signature: 'F5', seal: 'S0', detail: { signers: [], foreign } })).toEqual([verdictText('F5'), ` Otro firmante, «Otro»: ${text}. No cuenta.`]);
});
// The verdicts of spec v0.11 (§29.7) whose text is fixed, and those whose text names a key, a holder or a time, F3, F4, F6
// and S4, which verdictLines writes from what the reader found and verdictText leaves empty.
// The verdicts of spec v0.11 (§29.7) whose text is fixed, and those whose text names a key, a holder, a time or a
// reason, F3, F4, F6, S4 and, since v0.16, S5, which verdictLines writes from what the reader found and verdictText
// leaves empty.
it('has the text of the verdicts of v0.11 that do not name anything, and none for those that do', () => {
expect(verdictText('F2')).toBe('La firma no corresponde a este contenido.');
expect(verdictText('F5')).toBe('Faltan firmas o sellos que la propia cápsula exige: trátala como no firmada.');
expect(verdictText('S3')).toBe('El sello no corresponde a este contenido.');
expect(verdictText('S5')).toBe('Sellado después de la fecha de apertura: no prueba nada anterior.');
for (const v of ['F3', 'F4', 'F6', 'S4'] as const) expect(verdictText(v), v).toBe('');
for (const v of ['F3', 'F4', 'F6', 'S4', 'S5'] as const) expect(verdictText(v), v).toBe('');
});
// Spec v0.16, §29.7: S5 says why the seal does not prove that it came before the opening date, with the reason of the
// reader; without the detail of a valid seal it shows nothing, as in Go.
it('writes S5 with its reason', () => {
const t = { seconds: 1_790_000_000, nanos: 0 };
const s5 = (sealReason: 'late' | 'no accuracy' | 'no accuracy, BTSP'): string | undefined =>
verdictLines({ signature: 'F0', seal: 'S5', detail: { signers: [], foreign: [], sealHolder: 'TSA', sealTime: t, sealReason } })[1];
expect(s5('late')).toBe('No acredita que se sellara antes de la fecha de apertura: se selló después de esa fecha o demasiado cerca de ella.');
expect(s5('no accuracy')).toBe('No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión.');
expect(s5('no accuracy, BTSP')).toBe('No acredita que se sellara antes de la fecha de apertura: el sello no dice la precisión que exige su política.');
expect(verdictLines({ signature: 'F0', seal: 'S5' })).toEqual(['Sin firma de autor.']);
expect(sealReasonText(undefined)).toBe('');
});
});

@ -1,8 +1,8 @@
// The security area of a format 3 capsule (spec §29.3, §29.7), as
// EncodeSecurity, EvaluateSecurityIn and the verdicts of the Go package
// capsule give them at the draft v0.12 (format3.go, signature.go), the texts
// of the verdicts of a certificate included. SECURITY_CBOR is the map
// {0: "datekeys-security", 1: 1, ? 2: author-signature, ? 3: seal}, whose
// capsule give them at v0.16 (format3.go, signature.go), the texts of the
// verdicts of a certificate and the reasons of S5 included. SECURITY_CBOR is
// the map {0: "datekeys-security", 1: 1, ? 2: author-signature, ? 3: seal}, whose
// keys 2 and 3 hold CBOR encoded apart. In the context of a capsule it checks
// the signature of alg 1 with the strict profile of ed25519strict.ts, and the
// signature of alg 2 and the seal of seal_type 2 with securitycms.ts. The
@ -18,7 +18,7 @@ import { verifyStrict } from './ed25519strict.ts';
import { DateKeysError } from './errors.ts';
import { formatRFC3339Nano, type Instant } from './datekey.ts';
import { fieldOf, requireKeys } from './schema.ts';
import { type Detail, evaluateCMS, evaluateSeal } from './securitycms.ts';
import { type Detail, evaluateCMS, evaluateSeal, type SealReason } from './securitycms.ts';
export const SECURITY_TYPE_TAG = 'datekeys-security';
export const SECURITY_VERSION = 1;
@ -47,7 +47,10 @@ export const SEAL_TYPE_RFC3161 = 2;
* algorithm outside the table;
* - S2: a seal that does not decode or breaks its schema;
* - S3: a seal that does not correspond to this content;
* - S4 and S5: a valid seal, from before the time of the round or not.
* - S4: a valid seal with accuracy and t + accuracy < round_time (spec
* v0.16, §29.11);
* - S5: a valid seal that does not prove that it came before round_time;
* Detail.sealReason says why.
*/
export type Verdict = 'X' | 'F0' | 'F1' | 'F2' | 'F3' | 'F4' | 'F5' | 'F6' | 'S0' | 'S1' | 'S2' | 'S3' | 'S4' | 'S5';
@ -78,7 +81,11 @@ export interface SecurityContext {
readonly authorKeys?: ReadonlyMap<string, string>;
}
/** The text of a verdict that the official SDK shows, in Spanish (spec §29.7), and '' for S0, which shows nothing. */
/**
* The text of a verdict that the official SDK shows, in Spanish (spec §29.7),
* and '' for S0, which shows nothing, and for the verdicts whose text names a
* key, a holder, a time or a reason, which verdictLines writes.
*/
export function verdictText(v: Verdict): string {
switch (v) {
case 'X':
@ -97,18 +104,36 @@ export function verdictText(v: Verdict): string {
return 'Faltan firmas o sellos que la propia cápsula exige: trátala como no firmada.';
case 'S3':
return 'El sello no corresponde a este contenido.';
case 'S5':
return 'Sellado después de la fecha de apertura: no prueba nada anterior.';
// F3, F4, F6 and S4 name a key, a holder or a time: whoever shows them writes the text (spec §29.7).
// F3, F4, F6, S4 and S5 name a key, a holder, a time or a reason: whoever shows them writes the text (spec v0.16,
// §29.7).
case 'F3':
case 'F4':
case 'F6':
case 'S4':
case 'S5':
case 'S0':
return '';
}
}
/**
* The reason why a valid seal does not prove that it came before the opening
* date, as the texts of §29.7 write it (spec v0.16), and '' for none: that of
* S5, and of the line of a signer of F6 whose seal does not prove it.
*/
export function sealReasonText(r: SealReason | undefined): string {
switch (r) {
case 'late':
return 'se selló después de esa fecha o demasiado cerca de ella';
case 'no accuracy, BTSP':
return 'el sello no dice la precisión que exige su política';
case 'no accuracy':
return 'el sello no dice su precisión';
case undefined:
return '';
}
}
// The result of a signer in the texts of §29.7.
const RESULT_TEXT: Readonly<Record<string, string>> = {
valid: 'válida',
@ -126,12 +151,13 @@ const quoted = (name: string): string => `«${name}»`;
/**
* The verdicts as the official SDK shows them, in order: X alone, or the
* signature and then the seal, when it shows something (spec §29.7). F6 is
* followed by a line for each required signer, which names the authority of
* its seal, by the warning that DateKeys does not check who issued the seals
* when one of them says that it is before the opening date, and by the
* signers who do not count. A time is in RFC 3339 with the fraction of the
* seal.
* signature and then the seal, when it shows something (spec v0.16, §29.7).
* F6 is followed by a line for each required signer, which names the
* authority of its seal and says whether it proves that it came before the
* opening date, or why not, by the warning that DateKeys does not check who
* issued the seals when one of them says that it is before the opening date,
* and by the signers who do not count. S5 gives its reason. A time is in RFC
* 3339 with the fraction of the seal.
*/
export function verdictLines(v: Verdicts): string[] {
if (v.signature === 'X') return [verdictText('X')];
@ -144,7 +170,7 @@ export function verdictLines(v: Verdicts): string[] {
if (v.signature === 'F6' && d !== undefined) {
lines[0] = `Firmado con un certificado a nombre de ${d.signers.map((s) => quoted(s.holder)).join(', ')}. DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.`;
for (const s of d.signers) {
const when = s.before ? 'antes de la fecha de apertura' : 'no antes de la fecha de apertura';
const when = s.before ? 'antes de la fecha de apertura' : `sin acreditar que fuera antes de la fecha de apertura: ${sealReasonText(s.reason)}`;
lines.push(` ${quoted(s.holder)} (emisor según su certificado: ${quoted(s.issuer)}), sellado por ${quoted(s.sealHolder!)} el ${formatRFC3339Nano(s.sealTime!)}, ${when}.`);
}
// §29.7: whoever says that a capsule was signed before the date says that it does not check who issued the seal.
@ -153,6 +179,8 @@ export function verdictLines(v: Verdicts): string[] {
for (const s of d?.foreign ?? []) lines.push(` Otro firmante, ${quoted(s.holder)}: ${RESULT_TEXT[s.result]!}. No cuenta.`);
if (v.seal === 'S4' && d?.sealHolder !== undefined) {
lines.push(`Según un sello a nombre de ${quoted(d.sealHolder)}, existía el ${formatRFC3339Nano(d.sealTime!)}, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.`);
} else if (v.seal === 'S5' && d !== undefined) {
lines.push(`No acredita que se sellara antes de la fecha de apertura: ${sealReasonText(d.sealReason)}.`);
} else if (verdictText(v.seal) !== '') {
lines.push(verdictText(v.seal));
}
@ -335,7 +363,7 @@ function decodeAlg(d: Decoder): number {
// What the evaluation of the signature gives, and that of the seal.
type SignatureVerdicts = Pick<Verdicts, 'signature' | 'authorKey' | 'authorLabel' | 'detail'>;
type SealVerdicts = { seal: Verdict; sealHolder?: string; sealTime?: Instant };
type SealVerdicts = { seal: Verdict; sealHolder?: string; sealTime?: Instant; sealReason?: SealReason };
// Runs an evaluation whose failure is a verdict, never an error: its result,
// or `failed` when it throws, whatever it throws. The decoders throw a
@ -376,6 +404,7 @@ export function evaluateSecurity(b: Uint8Array, context?: SecurityContext): Verd
signers: sig.detail?.signers ?? [],
foreign: sig.detail?.foreign ?? [],
...(sealed.sealHolder === undefined ? {} : { sealHolder: sealed.sealHolder, sealTime: sealed.sealTime! }),
...(sealed.sealReason === undefined ? {} : { sealReason: sealed.sealReason }),
};
return { ...sig, seal: sealed.seal, ...(detail === undefined ? {} : { detail }) };
}

@ -1,8 +1,8 @@
// Tests of securitycms.ts, the verdicts of a signature of alg 2 and of a seal
// of seal_type 2 (spec v0.12 §29.7, §29.10, §29.11), through evaluateSecurity
// of seal_type 2 (spec v0.16 §29.7, §29.10, §29.11), through evaluateSecurity
// in the context of a capsule, on signatures and tokens that
// testing/cmsbuild.ts makes: the cases of signature2_test.go of the Go
// reference at the draft v0.12, what a signer line shows of a certificate, a
// reference at the draft v0.16, what a signer line shows of a certificate, a
// byte order mark at the start of a name or a time, and keys whose point is
// compressed. The hashes of the issuers below are those of the DER of their
// Names, which the reference shows in their place.
@ -13,7 +13,7 @@ import { ALG_CMS, ALG_ED25519, authorMessage, sealSubject, signersDigest } from
import { compareBytes, toHex } from './bytes.ts';
import { Encoder } from './cbor.ts';
import { ed25519 } from '@noble/curves/ed25519.js';
import { evaluateSecurity, type SecurityContext, type Verdicts, verdictLines, verdictText } from './security.ts';
import { evaluateSecurity, sealReasonText, type SecurityContext, type Verdicts, verdictLines, verdictText } from './security.ts';
import * as b from './testing/cmsbuild.ts';
const certFrom = new Date(Date.UTC(2025, 0, 1));
@ -83,11 +83,19 @@ function signersOf(...required: b.Signer[]): Uint8Array {
}
// The security area of a capsule with a signature of alg 2 by the signers, which SIGNERS requires with required, each
// signature sealed by `authority` at `when` with an accuracy of a second, and the seal of key 3 given.
async function cmsArea(required: b.Signer[], signers: b.Signer[], authority: b.Signer | undefined, when: Date, seal?: Uint8Array): Promise<Uint8Array> {
// signature sealed by `authority` at `when` with an accuracy of a second, or the options of the token given, and the
// seal of key 3 given.
async function cmsArea(
required: b.Signer[],
signers: b.Signer[],
authority: b.Signer | undefined,
when: Date,
seal?: Uint8Array,
tokenOptions: b.TokenOptions = { accuracy: b.accuracyOf(1) },
): Promise<Uint8Array> {
const key1 = signersOf(...required);
const msg = authorMessage(context.controlCommit, context.headDigest, signersDigest(ALG_CMS, key1));
const o: b.Options = authority === undefined ? {} : { token: (sig) => b.token(sig, when, { accuracy: b.accuracyOf(1) }, authority) };
const o: b.Options = authority === undefined ? {} : { token: (sig) => b.token(sig, when, tokenOptions, authority) };
return area(item(ALG_CMS, key1, await b.signature(msg, o, ...signers)), seal);
}
@ -96,9 +104,9 @@ async function signed(authority: b.Signer | undefined, ...signers: b.Signer[]):
return evaluateSecurity(await cmsArea(signers, signers, authority, signedAt), context);
}
// The verdicts of a seal of seal_type 2 by `authority`, without a signature.
// The verdicts of a seal of seal_type 2 by `authority`, without a signature, with an accuracy of a second.
async function sealed(authority: b.Signer): Promise<Verdicts> {
const token = await b.token(sealSubject(context.controlCommit, context.headDigest, undefined), signedAt, {}, authority);
const token = await b.token(sealSubject(context.controlCommit, context.headDigest, undefined), signedAt, { accuracy: b.accuracyOf(1) }, authority);
return evaluateSecurity(area(undefined, item(2, token)), context);
}
@ -125,12 +133,29 @@ describe('a signature of alg 2', () => {
});
it('warns of who issued the seals only when a line says before the opening date', async () => {
// A seal after the round time proves nothing before it, and then no line warns of who issued it (spec v0.16, §29.7).
const late = evaluateSecurity(await cmsArea([ana], [ana], tsa, new Date(roundTime.getTime() + 3_600_000)), context);
expect([late.signature, verdictLines(late).length]).toEqual(['F6', 2]);
expect(verdictLines(late)[1]).toBe(' «Ana López» (emisor según su certificado: «Ana López»), sellado por «TSA de prueba» el 2030-01-01T01:00:00Z, no antes de la fecha de apertura.');
expect([late.signature, verdictLines(late).length, late.detail!.signers[0]!.reason]).toEqual(['F6', 2, 'late']);
expect(verdictLines(late)[1]).toBe(
' «Ana López» (emisor según su certificado: «Ana López»), sellado por «TSA de prueba» el 2030-01-01T01:00:00Z, sin acreditar que fuera antes de la fecha de apertura: se selló después de esa fecha o demasiado cerca de ella.',
);
// t plus the accuracy of a second equal to the round time is not before it.
const edge = evaluateSecurity(await cmsArea([ana], [ana], tsa, new Date(roundTime.getTime() - 1000)), context);
expect([edge.signature, edge.detail!.signers[0]!.before, verdictLines(edge).length]).toEqual(['F6', false, 2]);
expect([edge.signature, edge.detail!.signers[0]!.before, edge.detail!.signers[0]!.reason, verdictLines(edge).length]).toEqual(['F6', false, 'late', 2]);
// A seal without accuracy, years before the round time, does not prove it either; under BTSP, with its own reason.
for (const [o, reason] of [
[{}, 'no accuracy'],
[{ policy: b.BTSP_POLICY }, 'no accuracy, BTSP'],
] as const) {
const v = evaluateSecurity(await cmsArea([ana], [ana], tsa, signedAt, undefined, o), context);
expect([v.signature, v.detail!.signers[0]!.before, v.detail!.signers[0]!.reason, verdictLines(v).length], reason).toEqual(['F6', false, reason, 2]);
expect(verdictLines(v)[1], reason).toBe(
` «Ana López» (emisor según su certificado: «Ana López»), sellado por «TSA de prueba» el 2026-09-30T12:00:00Z, sin acreditar que fuera antes de la fecha de apertura: ${sealReasonText(reason)}.`,
);
}
// With BTSP and its accuracy, it proves it.
const btsp = evaluateSecurity(await cmsArea([ana], [ana], tsa, signedAt, undefined, { policy: b.BTSP_POLICY, accuracy: b.accuracyOf(1) }), context);
expect([btsp.detail!.signers[0]!.before, btsp.detail!.signers[0]!.reason, verdictLines(btsp).length]).toEqual([true, undefined, 3]);
// A seal with a fraction of a second shows it.
const fraction = evaluateSecurity(await cmsArea([ana], [ana], tsa, new Date(signedAt.getTime() + 250)), context);
expect(verdictLines(fraction)[1]).toBe(' «Ana López» (emisor según su certificado: «Ana López»), sellado por «TSA de prueba» el 2026-09-30T12:00:00.25Z, antes de la fecha de apertura.');
@ -311,14 +336,15 @@ describe('a seal of seal_type 2', () => {
const sig = item(ALG_ED25519, ed25519.getPublicKey(seed), ed25519.sign(msg, seed));
const subject = sealSubject(context.controlCommit, context.headDigest, sig);
const withSeal = (token: Uint8Array): Uint8Array => area(sig, item(2, token));
const v = evaluateSecurity(withSeal(await b.token(subject, signedAt, {}, authority)), context);
expect([v.signature, v.seal, v.detail!.sealHolder, v.detail!.sealTime]).toEqual(['F4', 'S4', 'Autoridad de Sellado', at(signedAt)]);
const second = { accuracy: b.accuracyOf(1) };
const v = evaluateSecurity(withSeal(await b.token(subject, signedAt, second, authority)), context);
expect([v.signature, v.seal, v.detail!.sealHolder, v.detail!.sealTime, v.detail!.sealReason]).toEqual(['F4', 'S4', 'Autoridad de Sellado', at(signedAt), undefined]);
expect(verdictLines(v)[1]).toBe('Según un sello a nombre de «Autoridad de Sellado», existía el 2026-09-30T12:00:00Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.');
// Sealed with its own signature part: without key 2 the subject differs.
const alone = await sealed(authority);
expect([alone.signature, alone.seal]).toEqual(['F0', 'S4']);
const cases: [string, Promise<Uint8Array> | Uint8Array, string][] = [
['after the round time', b.token(subject, new Date(roundTime.getTime() + 60_000), {}, authority), 'S5'],
['after the round time', b.token(subject, new Date(roundTime.getTime() + 60_000), second, authority), 'S5'],
['the accuracy reaches it', b.token(subject, new Date(roundTime.getTime() - 1000), { accuracy: b.accuracyOf(2) }, authority), 'S5'],
['another subject', b.token(te.encode('other'), signedAt, {}, authority), 'S3'],
['an imprint of 33 bytes', b.token(subject, signedAt, { imprint: new Uint8Array(33) }, authority), 'S3'],
@ -337,4 +363,27 @@ describe('a seal of seal_type 2', () => {
// Without a context, as a reader of v0.10: S1.
expect(evaluateSecurity(withSeal(await b.token(subject, signedAt, {}, authority))).seal).toBe('S1');
});
// Spec v0.16, §29.7 and §29.11: a valid seal proves that it came before the round time only with accuracy; without
// it, S5 and its reason, the first that holds. An accuracy of 0 seconds, or an empty one, is a precision of 0.
it.each([
['no accuracy, years before', {}, signedAt, 'S5', 'no accuracy'],
['no accuracy under BTSP', { policy: b.BTSP_POLICY }, signedAt, 'S5', 'no accuracy, BTSP'],
['no accuracy, after the round time', { policy: b.BTSP_POLICY }, roundTime, 'S5', 'late'],
['an accuracy of 0 seconds', { accuracy: b.seq(b.int(0)) }, new Date(roundTime.getTime() - 1), 'S4', undefined],
['an empty accuracy', { accuracy: b.seq() }, signedAt, 'S4', undefined],
['BTSP with accuracy', { policy: b.BTSP_POLICY, accuracy: b.accuracyOf(1) }, signedAt, 'S4', undefined],
['an accuracy of 0 at the round time', { accuracy: b.seq(b.int(0)) }, roundTime, 'S5', 'late'],
] as const)('%s', async (_name, o, when, seal, reason) => {
const token = await b.token(sealSubject(context.controlCommit, context.headDigest, undefined), when, o, tsa);
const v = evaluateSecurity(area(undefined, item(2, token)), context);
expect([v.seal, v.detail?.sealReason]).toEqual([seal, reason]);
const last = verdictLines(v).at(-1);
if (seal === 'S5') expect(last).toBe(`No acredita que se sellara antes de la fecha de apertura: ${sealReasonText(reason)}.`);
else expect(last).toMatch(/^Según un sello a nombre de «TSA de prueba», existía el /);
});
it('has no text of its own for S5: verdictLines writes it with its reason', () => {
expect([verdictText('S5'), sealReasonText(undefined)]).toEqual(['', '']);
});
});

@ -1,8 +1,9 @@
// The verdicts of a signature of alg 2 (CMS with certificates) and of a seal of
// seal_type 2 (RFC 3161), as the Go package capsule gives them (signature2.go,
// spec v0.12 §29.7, §29.10, §29.11): F1, F2, F5 and F6 with the signers named,
// and S1 to S5 with the authority of a valid seal. Internal: index.ts does not
// re-export it.
// spec v0.16 §29.7, §29.10, §29.11): F1, F2, F5 and F6 with the signers named,
// and S1 to S5 with the authority of a valid seal and, for S5, the reason why
// it does not prove that it came before the opening date. Internal: index.ts
// does not re-export it.
import { ALG_CMS, authorMessage, sealSubject, signersDigest } from './author.ts';
import { compareBytes, equalBytes, toHex } from './bytes.ts';
@ -22,6 +23,7 @@ import {
type SignerInfo,
type Token,
tokenImprintIsSHA256,
tokenIsBTSP,
} from './cms.ts';
import { compareInstants, type Instant } from './datekey.ts';
import { DateKeysError } from './errors.ts';
@ -44,18 +46,40 @@ export interface SignerLine {
/** The holder of the certificate of the authority of its seal, with the same rules, and t; undefined without a seal that verifies. */
readonly sealHolder?: string;
readonly sealTime?: Instant;
/** Whether t plus the accuracy of the seal is before round_time. */
/**
* Whether the seal proves that it came before round_time: it carries
* accuracy and t plus the accuracy is before round_time (spec v0.16,
* §29.11). For a valid signer whose seal does not, reason says why.
*/
readonly before: boolean;
readonly reason?: SealReason;
}
/**
* Why a valid seal does not prove that it came before the opening date (spec
* v0.16, §29.7): the reason of S5, and of the line of a signer of F6 that does
* not say «antes de la fecha de apertura», the first that holds, as
* SealReason of Go:
* - 'late': t plus the accuracy, 0 without one, is not before round_time;
* - 'no accuracy, BTSP': the token carries no accuracy, and its policy is
* the BTSP of ETSI EN 319 421, which requires it;
* - 'no accuracy': the token carries no accuracy.
*/
export type SealReason = 'late' | 'no accuracy, BTSP' | 'no accuracy';
/** What the texts of F6, S4 and S5 name (spec §29.7, §29.10). */
export interface Detail {
/** The required signers, in the order of SIGNERS, and the SignerInfo of other certificates, which never count. */
readonly signers: readonly SignerLine[];
readonly foreign: readonly SignerLine[];
/** The holder of the certificate of the authority of a valid seal, as §29.7 writes it, and t. */
/**
* The holder of the certificate of the authority of a valid seal, as §29.7
* writes it, and t. sealReason is why it does not prove that it came before
* round_time (S5), undefined when it does (S4).
*/
readonly sealHolder?: string;
readonly sealTime?: Instant;
readonly sealReason?: SealReason;
}
// SIGNERS: a CBOR array of 1 to 16 strings of 32 bytes in strictly ascending order of bytes (spec §29.10). Throws a DateKeysError when it is not.
@ -124,9 +148,13 @@ function holderText(name: string, hash: Uint8Array): string {
return toHex(hash);
}
// The time of a seal, plus its accuracy, against round_time: whether it precedes it.
function before(tok: Token, roundTime: Instant | undefined): boolean {
return roundTime !== undefined && compareInstants(addInstants(tok.genTime, tok.accuracy), roundTime) < 0;
// The reason of a token that verifies, the first that holds (spec v0.16,
// §29.7): late, then without accuracy under BTSP, then without accuracy;
// undefined when it proves that it came before round_time.
function sealReason(tok: Token, roundTime: Instant | undefined): SealReason | undefined {
if (roundTime === undefined || compareInstants(addInstants(tok.genTime, tok.accuracy), roundTime) >= 0) return 'late';
if (!tok.hasAccuracy) return tokenIsBTSP(tok) ? 'no accuracy, BTSP' : 'no accuracy';
return undefined;
}
// One SignerInfo as §29.10 orders: not verifiable, invalid, without seal, with an invalid seal, out of validity, or valid.
@ -145,7 +173,9 @@ function signerLine(s: SignerInfo, msg: Uint8Array, roundTime: Instant | undefin
}
if (tok === undefined || !checkToken(tok, s.signature)) return { ...base, result: 'invalid seal' };
if (!certValidAt(s.cert, tok.genTime)) return { ...base, result: 'out of validity' };
return { ...base, result: 'valid', sealHolder: holderText(certHolder(tok.tsa), tok.tsa.hash), sealTime: tok.genTime, before: before(tok, roundTime) };
const reason = sealReason(tok, roundTime);
const line = { ...base, result: 'valid', sealHolder: holderText(certHolder(tok.tsa), tok.tsa.hash), sealTime: tok.genTime };
return reason === undefined ? { ...line, before: true } : { ...line, reason };
}
/**
@ -195,9 +225,11 @@ export function evaluateCMS(
}
/**
* The verdict of a seal of seal_type 2 (spec §29.11): S2 or S1 for the form and
* the algorithms, S3 when it does not verify, and S4 or S5 when it does, with
* the authority and t. `signature` is the content of key 2, undefined without it.
* The verdict of a seal of seal_type 2 (spec v0.16, §29.11): S2 or S1 for the
* form and the algorithms, S3 when it does not verify, and S4 or S5 when it
* does, with the authority and t: S4 only when the token carries accuracy and
* t plus the accuracy is before round_time, and otherwise S5 with its reason.
* `signature` is the content of key 2, undefined without it.
*/
export function evaluateSeal(
token: Uint8Array,
@ -205,7 +237,7 @@ export function evaluateSeal(
controlCommit: Uint8Array,
headDigest: Uint8Array,
roundTime: Instant | undefined,
): { seal: 'S1' | 'S2' | 'S3' | 'S4' | 'S5'; sealHolder?: string; sealTime?: Instant } {
): { seal: 'S1' | 'S2' | 'S3' | 'S4' | 'S5'; sealHolder?: string; sealTime?: Instant; sealReason?: SealReason } {
let tok;
try {
tok = parseToken(token);
@ -216,5 +248,7 @@ export function evaluateSeal(
}
if (!tokenImprintIsSHA256(tok)) return { seal: 'S1' };
if (!checkToken(tok, sealSubject(controlCommit, headDigest, signature))) return { seal: 'S3' };
return { seal: before(tok, roundTime) ? 'S4' : 'S5', sealHolder: holderText(certHolder(tok.tsa), tok.tsa.hash), sealTime: tok.genTime };
const valid = { sealHolder: holderText(certHolder(tok.tsa), tok.tsa.hash), sealTime: tok.genTime };
const reason = sealReason(tok, roundTime);
return reason === undefined ? { seal: 'S4', ...valid } : { seal: 'S5', ...valid, sealReason: reason };
}

@ -554,6 +554,8 @@ export interface TokenOptions {
readonly accuracy?: Uint8Array;
/** The version of the TSTInfo, 1 by default. */
readonly version?: number;
/** The policy of the TSTInfo, 1.2.3.4 by default; BTSP_POLICY is that of ETSI EN 319 421. */
readonly policy?: string;
/** Written as the hashed message instead of the hash of the subject, of any length. */
readonly imprint?: Uint8Array;
/** Written as genTime instead of the GeneralizedTime of the time given. */
@ -572,10 +574,19 @@ export function genTimeOf(t: Date): Uint8Array {
return generalizedTime(`${p(t.getUTCFullYear(), 4)}${p(t.getUTCMonth() + 1)}${p(t.getUTCDate())}${p(t.getUTCHours())}${p(t.getUTCMinutes())}${p(t.getUTCSeconds())}${frac}Z`);
}
/** The best practices time-stamp policy of ETSI EN 319 421, whose tokens carry accuracy (spec v0.16, §29.11). */
export const BTSP_POLICY = '0.4.0.2023.1.1';
/** The TSTInfo of a token over `subject` at `genTime`. */
export async function tstInfo(subject: Uint8Array, genTime: Date, o: TokenOptions = {}): Promise<Uint8Array> {
const hash = o.hash ?? 'SHA-256';
const fields = [int(o.version ?? 1), oid('1.2.3.4'), seq(hashAlg(hash), octets(o.imprint ?? (await digest(hash, subject)))), int(42), o.genTimeRaw ?? genTimeOf(genTime)];
const fields = [
int(o.version ?? 1),
oid(o.policy ?? '1.2.3.4'),
seq(hashAlg(hash), octets(o.imprint ?? (await digest(hash, subject)))),
int(42),
o.genTimeRaw ?? genTimeOf(genTime),
];
return seq(...fields, ...(o.accuracy === undefined ? [] : [o.accuracy]), ...(o.after ?? []));
}

@ -204,6 +204,16 @@
"r": "5ab044fad730f0470dfe574e1edd192065ff12ee04909e8cfe90ebc762cf5df7",
"file_key": "b6a845d405e6a47733b42802de903b6c"
},
{
"name": "format3_full_chunk",
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39",
"body": "a3316bc4eb855ba46266a6de32e066c9db948af603f96bedb97d0fb82251cf3f6ff6d605837ee4deda36d486256f5ea40af6d6442706b7c128c27a14207fe55632e1226e1c423a539d3854af06b456372cd9002130d1e6e9cdaa735d35251cb91c291d2e9d448d96076508758e7fb5bdca29568439c58864ee13a042569992ea",
"gt": "191a12f6ac1061ad64fc2741b81c2cd3b59ea5b23272b41cac5a55a25454c3303f6eb0e53bf35395cfc64b9ddb7e67a911050b711837947705a7ed8ac2b6541f45c8be7b93014384b771c6b936df77fd420f3043c14b20d09d520794ea2f00150e7a164d9ed593582ef15c791316317b34908384b4b39cc48a6cd0b195d980f77adf7b983e34c61d7c6e1efe2c26eb08015d91a1234bad16c5c1cc6afa91392e426b7c706d4255ff7c21c0b60afeacd1304d3695658f799f2c1d9db937f9439f0578aeefd983cdb9f7f499e54cf5e091cbec90d1acf4c32dc557327b44fcaca078535b8e75038016b89688026196ce3d088d49cb58032cfb80ebaf66fdb861d1f461b658c24d1a012dcf8d7bda33f90f90c97e72a2feffaf442ad4baac8f6685023935250b48dbaaf44d59967968d46510d909ba9e6d3afa0706c71579ea59adc4c683b3172b4777c909d60699c07baa0d27e7a603e6c0619358014ea5a52ec149dd9f5fc9ab1a5b99fae39fd717d21d084daea95794e7d06edd23571e613f5a0bb70efdd00506460eb4067a7ffa95f3fac70db226b60201f12d7e3125b22211fcc71d3eb5175520d81595644bc78f42116f37390e30d4df6e58b40ad43ad120138fec50180e2f39bda8b56bd1669cc8498a39fb838269a3eab4c00650d851530119f4a185228f9189afe6f36b02d3a32ed5cc990869811c2a38e1f84b6a50f9aa8a1810296c16eed15b6527f48133b40fb7e4416cf55c450e51ba99b8f020c353dbc698cd88b6c191b3500a1cf6e9cf6fc1339e678cfeec715ccf40c30e29c1",
"sigma": "8792444c092fbf9d716911f5af1915de",
"r": "02c90283781a9a2f73c0b49483dd17d2dc57026f7892e67aac365b978e8f98e0",
"file_key": "ffa78141f5ad27edd0bb5a7886c050f6"
},
{
"name": "format3_note",
"round": 1000,
@ -294,6 +304,16 @@
"r": "2bd166cc5a3e8664ed5b4cc2932e13f53c313180dea70ca9a10b420d4e83a202",
"file_key": "916c47b45f39c0b4df425f1a248028e4"
},
{
"name": "format3_time_and_key_words",
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39",
"body": "b451299668bf0607d4d6839784ae811b006a96ef77067ec33c7b9b8bfa9a50975dc223bafaa7a2a4b1eb7c09ae24807b13e9e5cc851e6fc7bc1f951890da76a86776e81278a30b808b08e4ed253261a5409647d785de1c9bb4202dbbab26707fea345f2c8a6048a26a9d1431174337a8b5707668bf4c81e11bfdb2895311fc2d",
"gt": "052db5b2caf451f8edeeb2eaa662ea40749f3de2b2118dfe23eafa7f30028e31180ab716dfd5b9e9c7283b762f97cad110ed15d491d3b596c0e43cbaf15fab6a39bd5d8ebc5ce62f206dcb86a7d5714e7bb1768862c6033368cd06c06f330d01103c7fd0aa2229906e03085f5563f024dbab25e3642a71ba1281fa7f0fae0258a3e47a7f4a41a2f6da2cfd4250884b7105b2993530eb40e45efe5065fd6e121fb36cf3178818aea92b8749958058fbe9449f67e1a426250aa2281de7843b94170d045409e95ae1b204ae8b37b0c869dbfc3e1ac95fc6656a6c087838044573709ee6a7753098cce4adb602e636f5d3560d17796d1cfe809a9aa8ccfaa78de2b5803fbdec6d45ba7fed0b35a5e0736ef80b9940ad5c659d2987efdc214f4fbb4c0d41c08cf52359b6b13a82b978bf9af6ca51d2b5a3bec6ea9289fc0657a95f23709b3b75ea2237d958f04a069d0b70c70326d4ea127d56c5504aa6043185a8d35356076250d861ff0bc2975dc40f51a81ff329bad1dbcc367b403d5b46bdb42c00bd2c3e6489ed849f0dbb3fccb1cf08cfd6785f66dbf306f74ac0448881bc9b348ec64b684e8613a4cb880ef574d7510d7bcc004c12d27bbefccf21459b0ac23bf239b3062f1c53e83ebb0373c95de1e6e682e2daf273db4c242c8e3b05435f171770affc2f7e784414099c76b938a1d60df53f0137681db610ea2b02dcc28abe697d5485fce2eb5f910fa702217bcc1107836f0c9ce72cdcf885925e01c8bf9508963686c515bfe03c5c2b0127ae0fe4e89ac5df779493493912a96e4e7f46",
"sigma": "e8c96c522ff4fb100bdbb290f607f654",
"r": "6916b5c1fd08176844ef3649f3a8653b2a79ca37008b14aeff9fe2eeb9599901",
"file_key": "7b1a19733d0d4eb54baf209550bbc7e0"
},
{
"name": "format3_tree",
"round": 1001,

@ -1,7 +1,7 @@
{
"description": "The text of the error of capsule.Open for every case of testdata/vectors/mutations.json, or ok for a capsule that opens; see the header of scripts/mutation-go-texts.go.",
"generator": "scripts/mutation-go-texts.go",
"spec": "0.15",
"spec": "0.16",
"cases": [
{
"name": "PUBLIC_HEADER_A + SEALED_CONTROL_B",

@ -227,12 +227,12 @@
"length": 32989,
"lines": [
"Firmado con la clave dkauthor1pdrcy0n3p9watxl83tp8r3tkauuflpakg4s6kp70nf8te5pdypqszvracp. No prueba quién la tiene.",
"Según un sello a nombre de «TSA de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"result": "ok",
"verdicts": [
"F4",
"S4"
"S5"
]
},
"opened_saved": {
@ -254,12 +254,12 @@
"length": 32989,
"lines": [
"Firmado con la clave que guardaste como mi clave de 2026.",
"Según un sello a nombre de «TSA de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"result": "ok",
"verdicts": [
"F3",
"S4"
"S5"
]
},
"recipe": {
@ -363,12 +363,12 @@
"length": 32893,
"lines": [
"Sin firma de autor.",
"Según un sello a nombre de «TSA de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"result": "ok",
"verdicts": [
"F0",
"S4"
"S5"
]
},
"recipe": {
@ -941,7 +941,7 @@
"length": 32893,
"lines": [
"Firmado con la clave dkauthor1pdrcy0n3p9watxl83tp8r3tkauuflpakg4s6kp70nf8te5pdypqszvracp. No prueba quién la tiene.",
"Sellado después de la fecha de apertura: no prueba nada anterior."
"No acredita que se sellara antes de la fecha de apertura: se selló después de esa fecha o demasiado cerca de ella."
],
"result": "ok",
"verdicts": [
@ -963,7 +963,7 @@
"length": 32893,
"lines": [
"Firmado con la clave que guardaste como mi clave de 2026.",
"Sellado después de la fecha de apertura: no prueba nada anterior."
"No acredita que se sellara antes de la fecha de apertura: se selló después de esa fecha o demasiado cerca de ella."
],
"result": "ok",
"verdicts": [
@ -1208,7 +1208,7 @@
"n": 16
}
],
"error": "capsule: self-check: the reader finds the verdicts F2 and S4 in this security area, not F4 and S4",
"error": "capsule: self-check: the reader finds the verdicts F2 and S5 in this security area, not F4 and S4",
"hooks": {
"author_message": "646174656b6579733a646b63333a617574686f722d7369676e61747572653a76310a303730376433653332303735303339653232633637333430626431643439326234343365666461663935656236633339356630313330636162363033386631630a",
"author_public": "0b47823e71095dd59be78ac271c576ef389f87b64561ab07cf9a4ebcd02d2041",
@ -1863,12 +1863,12 @@
"length": 32990,
"lines": [
"Firmado con la clave dkauthor1xes558a6zzqw4urrz8c80u33znlv2yzc9t9f5ywa8a0c8ysq9atqt2k706. No prueba quién la tiene.",
"Según un sello a nombre de «Autoridad de sellado de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"result": "ok",
"verdicts": [
"F4",
"S4"
"S5"
]
},
"opened_saved": {
@ -1890,23 +1890,23 @@
"length": 32990,
"lines": [
"Firmado con la clave que guardaste como la clave de prueba.",
"Según un sello a nombre de «Autoridad de sellado de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"result": "ok",
"verdicts": [
"F3",
"S4"
"S5"
]
},
"ts": {
"area_len": 32768,
"lines": [
"Firmado con la clave que guardaste como la clave de prueba.",
"Según un sello a nombre de «Autoridad de sellado de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"verdicts": [
"F3",
"S4"
"S5"
]
}
},
@ -1931,23 +1931,23 @@
"length": 32990,
"lines": [
"Sin firma de autor.",
"Según un sello a nombre de «Autoridad de sellado de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"result": "ok",
"verdicts": [
"F0",
"S4"
"S5"
]
},
"ts": {
"area_len": 32768,
"lines": [
"Sin firma de autor.",
"Según un sello a nombre de «Autoridad de sellado de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
"No acredita que se sellara antes de la fecha de apertura: el sello no dice su precisión."
],
"verdicts": [
"F0",
"S4"
"S5"
]
}
},
@ -2040,5 +2040,5 @@
}
}
],
"source": "fe405e2348744f50e54c72e35ae10470a01dc552"
"source": "4f7885495bd6ea666cb444519090fb5f3ea46752"
}

@ -107,6 +107,7 @@ import {
verifyRelease,
} from './release.ts';
import { evaluateSecurity, type Verdict, verdictLines } from './security.ts';
import type { SignerLine } from './securitycms.ts';
import { MemorySink } from './sink.ts';
import { readVectors as readLocatorVectors } from './testing/locator.ts';
import { kinds } from './testing/verdicts.ts';
@ -1964,8 +1965,9 @@ describe('vectors/release.json', () => {
});
if (f === undefined) return;
it('has the 36 objects, 12 JSON inputs and 7 archive lookups of README, every code of step 10 among them', () => {
expect([f.objects.length, f.json.length, f.archive.lookups.length]).toEqual([36, 12, 7]);
// The JSON inputs are the 12 of v0.15 and the 26 of the strict reading of v0.16 (§47.1).
it('has the 36 objects, 38 JSON inputs and 7 archive lookups, every code of step 10 among them', () => {
expect([f.objects.length, f.json.length, f.archive.lookups.length]).toEqual([36, 38, 7]);
expect([...new Set([...f.objects, ...f.json].map((c) => c.result))].sort()).toEqual([
'ERR_NON_CANONICAL_CBOR',
'ERR_PROFILE_MISMATCH',
@ -2097,14 +2099,20 @@ describe('testdata/', () => {
});
describe('vectors of v0.11', () => {
// The vectors of the draft v0.12 for the verification of the signature
// with certificates and of the seal (§29.7, §29.10, §29.11): every area is
// The vectors of v0.16 for the verification of the signature with
// certificates and of the seal (§29.7, §29.10, §29.11): every area is
// read in the context of its capsule and must give the verdicts of the
// reference, the result of each signer, of those who do not count and the
// authority of a valid seal, each time in RFC 3339 with the fraction of
// its token, and the lines of §29.7, byte for byte.
// its token, the reason why a valid seal does not prove that it came
// before the round time, and the lines of §29.7, byte for byte.
describe('security_cms.json', () => {
const RESULTS = ['valid', 'invalid', 'absent', 'without seal', 'invalid seal', 'out of validity', 'not verifiable'];
// The reasons of README, the first that holds (spec v0.16, §29.7).
const sealReason = (v: unknown, at: string): string => {
if (!['late', 'no accuracy', 'no accuracy, BTSP'].includes(str(v, at))) throw new FormatError(at, `"${String(v)}" is not a reason of README`);
return v as string;
};
// The results of the signers of a case, [] when the key is absent, as Go omits an empty list.
const results = (v: unknown, at: string): unknown[] =>
v === undefined
@ -2112,10 +2120,11 @@ describe('testdata/', () => {
: array(v, at).map((s, i) => {
const w = `${at}[${i}]`;
const o = object(s, w);
keys(o, w, ['holder', 'issuer', 'result', 'before_round_time'], ['seal_time']);
keys(o, w, ['holder', 'issuer', 'result', 'before_round_time'], ['seal_time', 'seal_reason']);
if (!RESULTS.includes(str(o.result, `${w}.result`))) throw new FormatError(w, `"${String(o.result)}" is not a result of README`);
const t = o.seal_time === undefined ? {} : { seal_time: str(o.seal_time, `${w}.seal_time`) };
return { holder: str(o.holder, `${w}.holder`), issuer: str(o.issuer, `${w}.issuer`), result: o.result, ...t, before_round_time: bool(o.before_round_time, w) };
const reason = o.seal_reason === undefined ? {} : { seal_reason: sealReason(o.seal_reason, `${w}.seal_reason`) };
return { holder: str(o.holder, `${w}.holder`), issuer: str(o.issuer, `${w}.issuer`), result: o.result, ...t, before_round_time: bool(o.before_round_time, w), ...reason };
});
const f = load('vectors/security_cms.json', (json) => {
const o = object(json, 'security_cms.json');
@ -2124,7 +2133,7 @@ describe('testdata/', () => {
return array(o.cases, 'cases').map((v, i) => {
const at = `cases[${i}]`;
const c = object(v, at);
keys(c, at, ['name', 'security_cbor', 'context', 'signature', 'seal', 'lines'], ['signers', 'foreign_signers', 'seal_holder', 'seal_time']);
keys(c, at, ['name', 'security_cbor', 'context', 'signature', 'seal', 'lines'], ['signers', 'foreign_signers', 'seal_holder', 'seal_time', 'seal_reason']);
const want = {
signature: verdict(c.signature, `${at}.signature`),
seal: verdict(c.seal, `${at}.seal`),
@ -2132,6 +2141,7 @@ describe('testdata/', () => {
foreign_signers: results(c.foreign_signers, `${at}.foreign_signers`),
seal_holder: c.seal_holder === undefined ? '' : str(c.seal_holder, `${at}.seal_holder`),
seal_time: c.seal_time === undefined ? '' : str(c.seal_time, `${at}.seal_time`),
seal_reason: c.seal_reason === undefined ? '' : sealReason(c.seal_reason, `${at}.seal_reason`),
lines: linesOf(c.lines, `${at}.lines`),
};
return { name: str(c.name, at), area: hexOf(c.security_cbor), context: securityContext(c.context, `${at}.context`), want };
@ -2139,20 +2149,23 @@ describe('testdata/', () => {
});
if (f === undefined) return;
it('has the 135 cases of README, which reach every verdict but X and F3', () => {
expect(f).toHaveLength(135);
it('has the 143 cases of README, which reach every verdict but X and F3, and every reason', () => {
expect(f).toHaveLength(143);
const reached = new Set(f.flatMap((c) => [c.want.signature, c.want.seal]));
expect([...reached].sort()).toEqual(['F0', 'F1', 'F2', 'F4', 'F5', 'F6', 'S0', 'S1', 'S2', 'S3', 'S4', 'S5']);
const reasons = (c: (typeof f)[number]): unknown[] => [c.want.seal_reason, ...c.want.signers.map((s) => (s as { seal_reason?: string }).seal_reason)];
expect([...new Set(f.flatMap(reasons))].filter((r) => r !== '' && r !== undefined).sort()).toEqual(['late', 'no accuracy', 'no accuracy, BTSP']);
});
it.each(f.map((c) => [c.name, c] as const))('%s', (_n, c) => {
const v = evaluateSecurity(c.area, c.context);
const result = (s: { holder: string; issuer: string; result: string; sealTime?: Instant; before: boolean }): unknown => ({
const result = (s: SignerLine): unknown => ({
holder: s.holder,
issuer: s.issuer,
result: s.result,
...(s.sealTime === undefined ? {} : { seal_time: formatRFC3339Nano(s.sealTime) }),
before_round_time: s.before,
...(s.reason === undefined ? {} : { seal_reason: s.reason }),
});
expect({
...kinds(v),
@ -2160,6 +2173,7 @@ describe('testdata/', () => {
foreign_signers: v.detail?.foreign.map(result) ?? [],
seal_holder: v.detail?.sealHolder ?? '',
seal_time: v.detail?.sealTime === undefined ? '' : formatRFC3339Nano(v.detail.sealTime),
seal_reason: v.detail?.sealReason ?? '',
lines: verdictLines(v),
}).toEqual(c.want);
});

@ -10,13 +10,13 @@
* of words, the public note and the locator of datekeys.capsule; 0.3.0
* implements spec 0.14: one drand scheme and the root of trust byte for byte;
* 0.4.0 implements spec 0.15: the release object and a release in hand;
* 0.5.0-dev is what comes after it.
* 0.5.0-dev is what comes after it, on the draft 0.16.
*/
export const VERSION = '0.5.0-dev';
/**
* The version of the DateKeys Protocol Specification that this library
* implements: v0.15 of the Go reference, whose shared vectors
* implements: the draft v0.16 of the Go reference, whose shared vectors
* and fixtures (testdata/) all name it.
*/
export const SPEC_VERSION = '0.15';
export const SPEC_VERSION = '0.16';

@ -39,7 +39,16 @@ import { checkAuthor, checkComment, checkPath, checkTree, MAX_AUTHOR_LEN, MAX_CO
import { cloneProfile, type Profile, validateProfile } from './profile.ts';
import { permute, type RandomWords } from './random.ts';
import { checkX25519Recipient, formatX25519Recipient } from './recipient.ts';
import { encodeAuthorSignatureItem, encodeSealItem, encodeSecurity, encodeSecurityWith, evaluateSecurity, SEAL_TYPE_RFC3161, type Verdict } from './security.ts';
import {
encodeAuthorSignatureItem,
encodeSealItem,
encodeSecurity,
encodeSecurityWith,
evaluateSecurity,
SEAL_TYPE_RFC3161,
type Verdict,
type Verdicts,
} from './security.ts';
import { type Detail, encodeSigners } from './securitycms.ts';
import { timeRecipient } from './tlock.ts';
import { checkWords, wordKey } from './wordkey.ts';
@ -218,6 +227,14 @@ export interface Encrypted {
* paths, with their layout and SHA-256, and the comment as written.
*/
readonly head?: Head;
/**
* The verdicts of the security area that encryptFiles wrote, as a reader of
* this capsule finds them; undefined for encrypt. A valid seal that does
* not prove that it came before the opening date, S5 or the line of a
* signer of F6 with a reason, is written all the same: the writer warns of
* it, and offers to ask another authority (spec v0.16, §62.1 rule 19).
*/
readonly security?: Verdicts;
/** The size of the .dkc. */
readonly size: number;
/** The .dkc, only without an output. */
@ -367,11 +384,13 @@ export async function writeFiles(files: readonly FileSource[], opts: EncryptOpti
// SECURITY_CBOR and the frame are final once the control is: the
// signature commits to it (spec §29.8). prepare builds them before
// anything is written, with the commitments of the control, which do not
// depend on L, and returns the final L.
// depend on L, and returns the final L, keeping the verdicts of the area
// it built last for the result (§62.1 rule 19).
let security: Uint8Array = new Uint8Array(0);
let verdicts: Verdicts | undefined;
let frame: Uint8Array = new Uint8Array(0);
const prepare = async (c: Control): Promise<number> => {
security = await securityArea(s, c, headBytes);
({ security, verdicts } = await securityArea(s, c, headBytes));
// The area is the common one, or the large one only when what was
// signed does not fit and largeArea allows it (§62.1 rule 13).
if (security.length <= common) area = common;
@ -393,7 +412,7 @@ export async function writeFiles(files: readonly FileSource[], opts: EncryptOpti
return bodyContent([frame, areaBytes, headBytes], final.files, sources, order);
};
const res = await seal(s, FORMAT_3, length, draws, state, body, prepare);
return { ...res, head: final };
return { ...res, head: final, security: verdicts! };
});
}
@ -402,9 +421,11 @@ export async function writeFiles(files: readonly FileSource[], opts: EncryptOpti
// author key or of the CMS signer, and the seal of the sealer (spec §29.3,
// §29.8 to §29.11). The signature is made first and the seal after it, which
// seals it. It decodes and evaluates what it returns with the rules of the
// reader, in the context of this capsule (§62.1 rules 17, 19 and 21). The
// caller decides the area from its length.
async function securityArea(s: SealState, c: Control, head: Uint8Array): Promise<Uint8Array> {
// reader, in the context of this capsule (§62.1 rules 17, 19 and 21), and
// returns the verdicts too, so that the writer warns of a seal that does not
// prove that it came before the opening date (rule 19). The caller decides the
// area from its length.
async function securityArea(s: SealState, c: Control, head: Uint8Array): Promise<{ security: Uint8Array; verdicts: Verdicts }> {
const { authorKey, cmsSigner, sealer } = s;
if (authorKey === undefined && cmsSigner === undefined && sealer === undefined) {
const security = encodeSecurity();
@ -413,7 +434,7 @@ async function securityArea(s: SealState, c: Control, head: Uint8Array): Promise
if (v.signature !== 'F0' || v.seal !== 'S0') {
throw new Error(`capsule: self-check: the reader finds the verdicts ${v.signature} and ${v.seal} in this security area`);
}
return security;
return { security, verdicts: v };
}
const hd = headDigest(head);
const cc = controlCommit(c, FORMAT_3);
@ -462,13 +483,14 @@ async function securityArea(s: SealState, c: Control, head: Uint8Array): Promise
const v = evaluateSecurity(security, { controlCommit: cc, headDigest: hd, roundTime: s.unlock });
// A seal that proves nothing before the round time (S5) is still a seal
// that verifies: the clock of the writer and that of the authority may
// differ.
// differ, or the token may carry no accuracy. Encrypted.security lets the
// caller warn of it (§62.1 rule 19).
const sealOK = v.seal === wantSeal || (wantSeal === 'S4' && v.seal === 'S5');
const sameKey = key === undefined ? v.authorKey === undefined : v.authorKey !== undefined && equalBytes(v.authorKey, key);
if (v.signature !== wantSig || !sealOK || !sameKey) {
throw new Error(`capsule: self-check: the reader finds the verdicts ${v.signature} and ${v.seal} in this security area, not ${wantSig} and ${wantSeal}${detailText(v.detail)}`);
}
return security;
return { security, verdicts: v };
}
// What a hook returned, which must be bytes: a copy, which the hook cannot

@ -67,11 +67,32 @@ describe('fetchRelease', () => {
'api3.drand.sh': new TypeError('blocked'),
};
await expect(fetchRelease(CHAIN, ROUND, relays(odd))).rejects.toThrow(
'Ningún relay de drand dio la firma: api.drand.sh respondió algo que no es una firma; api2.drand.sh dio la ronda undefined, no la 32668196; api3.drand.sh no se pudo conectar.',
'Ningún relay de drand dio la firma: api.drand.sh respondió algo que no es una firma; api2.drand.sh respondió algo que no es una firma; api3.drand.sh no se pudo conectar.',
);
});
// The client of the reference reads the body with json.Unmarshal, which refuses a byte order mark before the JSON.
// Spec v0.16, §47.1: the client of the reference reads the answers of the relays with the strict reader of drand's
// JSON, as a release that the caller gives: a name twice is malformed, and ROUND is another name, ignored.
it('reads an answer with the strict reader of drand JSON', async () => {
const text = (body: string): Response => new Response(body, { status: 200 });
const answers: Record<string, Answer> = {
'api.drand.sh': text(`{"round":${ROUND},"round":${ROUND},"signature":"${SIG}"}`),
'api2.drand.sh': text(`{"round":${ROUND},"signature":""}`),
'api3.drand.sh': text(`{"round":${ROUND},"signature":"${SIG}","randomness":null}`),
};
await expect(fetchRelease(CHAIN, ROUND, relays(answers))).rejects.toThrow(
'Ningún relay de drand dio la firma: api.drand.sh respondió algo que no es una firma; api2.drand.sh dio una firma que no es hexadecimal; api3.drand.sh respondió algo que no es una firma.',
);
const other = await fetchRelease(
CHAIN,
ROUND,
relays({ 'api.drand.sh': text(`{"\\u0072ound":${ROUND},"ROUND":${ROUND + 1},"signature":"${SIG}"}`), 'api2.drand.sh': 'hang', 'api3.drand.sh': 'hang' }),
);
expect(other).toEqual({ round: ROUND, signature: SIG, relay: 'https://api.drand.sh' });
});
// The client of the reference reads the body with provider.ParseDrandJSON, for which a byte order mark before the JSON
// is not a space of JSON.
it('refuses an answer that starts with a byte order mark, as the client of the reference does', async () => {
const body = new TextEncoder().encode(JSON.stringify({ round: ROUND, signature: SIG, randomness: RANDOMNESS }));
const bom = new Response(new Uint8Array([0xef, 0xbb, 0xbf, ...body]), { status: 200 });

@ -2,10 +2,15 @@
// person asks for it: the one connection the page makes to another site.
// The relays are those of the CLI of the reference, raced as its client
// races them (provider/drand/client.go): the same path, a timeout of 6 s, at
// most 8 KiB an answer, no redirects, and the randomness checked against the
// signature. The signature itself is verified in step 10 with the pinned
// public key, so a relay cannot make the page accept a false one. A relay
// sees the address of the person and the round asked for.
// most 8 KiB an answer, no redirects, and the answer read as that client
// reads it since spec v0.16, with the strict reader of drand's JSON
// (releaseobject.ts, §47.1), the randomness checked against the signature.
// The signature itself is verified in step 10 with the pinned public key, so
// a relay cannot make the page accept a false one. A relay sees the address
// of the person and the round asked for.
import { toHex } from '../dkc/bytes.ts';
import { parseDrandJSON, type Release } from '../dkc/releaseobject.ts';
/** The relays the page may reach, as the Content-Security-Policy lists them. */
export const RELAYS = ['https://api.drand.sh', 'https://api2.drand.sh', 'https://api3.drand.sh'] as const;
@ -55,20 +60,18 @@ export async function fetchRelease(chainHash: string, round: number, fetcher: ty
}
if (res.status === 404) throw new RelayError('aún no la ha publicado', true);
if (res.status !== 200) throw new RelayError(`respondió HTTP ${res.status}`);
// The answer is read as a release that the person gives, with the strict
// rules of spec v0.16, §47.1, randomness included; then its round.
const body = await bounded(res);
let wire: unknown;
let release: Release;
try {
wire = JSON.parse(body);
} catch {
throw new RelayError('respondió algo que no es una firma');
}
const { round: got, signature, randomness } = (typeof wire === 'object' && wire !== null ? wire : {}) as Record<string, unknown>;
if (got !== round) throw new RelayError(`dio la ronda ${String(got)}, no la ${round}`);
if (typeof signature !== 'string' || !/^(?:[0-9a-f]{2})+$/i.test(signature)) throw new RelayError('dio una firma que no es hexadecimal');
if (randomness !== undefined && (typeof randomness !== 'string' || randomness.toLowerCase() !== (await sha256Hex(signature)))) {
throw new RelayError('dio una aleatoriedad que no es la de su firma');
release = await parseDrandJSON(body);
} catch (err) {
throw new RelayError(problemOf(err as Error));
}
return { round, signature: signature.toLowerCase(), relay };
if (release.round !== round) throw new RelayError(`dio la ronda ${release.round}, no la ${round}`);
if (release.signature.length === 0) throw new RelayError('dio una firma que no es hexadecimal');
return { round, signature: toHex(release.signature), relay };
};
try {
return await Promise.any(RELAYS.map(one));
@ -82,14 +85,20 @@ export async function fetchRelease(chainHash: string, round: number, fetcher: ty
}
}
// The body of an answer as text, a leading U+FEFF kept: JSON.parse refuses it,
// as json.Unmarshal does in the client of the reference.
const BODY_TEXT = new TextDecoder('utf-8', { ignoreBOM: true });
// What the strict reader of drand's JSON found wrong in an answer, by the text
// of the reference that it gives, in the words of the page. A leading U+FEFF
// is not a space of JSON: the answer is malformed, as for the client of the
// reference.
function problemOf(err: Error): string {
if (err.message.includes('signature is not hex')) return 'dio una firma que no es hexadecimal';
if (err.message.includes('randomness does not match the signature')) return 'dio una aleatoriedad que no es la de su firma';
return 'respondió algo que no es una firma';
}
// The body of an answer, read up to MAX_RESPONSE bytes and not one more.
async function bounded(res: Response): Promise<string> {
async function bounded(res: Response): Promise<Uint8Array> {
const reader = res.body?.getReader();
if (reader === undefined) return '';
if (reader === undefined) return new Uint8Array(0);
const parts: Uint8Array[] = [];
let n = 0;
for (let r = await reader.read(); !r.done; r = await reader.read()) {
@ -106,11 +115,5 @@ async function bounded(res: Response): Promise<string> {
all.set(p, at);
at += p.length;
}
return BODY_TEXT.decode(all);
}
async function sha256Hex(hex: string): Promise<string> {
const bytes = Uint8Array.from(hex.match(/../g)!, (b) => Number.parseInt(b, 16));
const sum = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));
return Array.from(sum, (b) => b.toString(16).padStart(2, '0')).join('');
return all;
}

@ -20,6 +20,8 @@ interface Record {
access_policy: string;
access_key_file?: string;
identities?: string[];
capsule_id: string;
words_text?: string;
plaintext_file: string;
plaintext_sha256: string;
format: number;
@ -311,4 +313,20 @@ describe('openCapsule with words', () => {
const elsewhere = await openCapsule(withWords('perro luna casa verde tren mar', '00'.repeat(16)));
expect(elsewhere.ok && elsewhere.opened.error !== undefined).toBe(true);
});
// Spec v0.16, annex 79.7: the official fixture of a key of words opens with its text as typed, and with its marks
// apart, to the files of its record.
it('opens format3_time_and_key_words with the text of its record', async () => {
const f = fixture('format3_time_and_key_words');
const words = { chainHash: toHex(quicknet().chainHash), round: f.record.release.round, capsuleId: f.record.capsule_id };
for (const text of [f.record.words_text!, f.record.words_text!.normalize('NFD')]) {
const r = await openCapsule({ ...f.request, words: { text, ...words } });
if (!r.ok) throw new Error(r.problem);
expect(r.opened.error).toBeUndefined();
const files = r.files!;
expect(await Promise.all(files.head.files.map(async (_, i) => toHex(await sha256(new Uint8Array(await files.file(i).arrayBuffer())))))).toEqual(
(f.record.files ?? []).map((x) => x.sha256),
);
}
});
});

@ -26,6 +26,31 @@ describe('parseReleaseText', () => {
expect(r).toEqual({ ok: true, release: { round: 5, signature: new Uint8Array([0xab]) }, form: 'json' });
});
// Spec v0.16, §47.1: the strict reader of drand's JSON, so that the page reads the round that step 10 would.
it("reads drand's JSON strictly, as step 10 reads it", () => {
const round = (s: string): number | undefined => {
const r = parseReleaseText(s, 7);
return r.ok ? r.release.round : undefined;
};
expect(round('{"\\u0072ound": 1000, "signature": "ab"}')).toBe(1000);
expect(round('{"round": 1000, "ROUND": 1001, "Round": 1002, "signature": "ab"}')).toBe(1000);
expect(round('{"round": 9007199254740991, "signature": "ab"}')).toBe(9007199254740991);
const problem = (s: string): string => {
const r = parseReleaseText(s, 1000);
if (r.ok) throw new Error(`accepted ${s}`);
return r.problem;
};
expect(problem('{"round": 1000, "round": 1001, "signature": "ab"}')).toMatch(/no es JSON válido/);
expect(problem('{"round": 1000, "\\u0072ound": 1000, "signature": "ab"}')).toMatch(/no es JSON válido/);
expect(problem('{"round": 1000, "signature": "ab", "x": {"a": 1, "a": 2}}')).toMatch(/no es JSON válido/);
expect(problem('{"round": 1000, "signature": "ab", "x": "\\ud800"}')).toMatch(/no es JSON válido/);
expect(problem('{"ROUND": 1000, "signature": "ab"}')).toMatch(/round/);
expect(problem('{"round": 0, "signature": "ab"}')).toMatch(/round/);
expect(problem('{"round": 1e3, "signature": "ab"}')).toMatch(/round/);
expect(problem('{"round": 01000, "signature": "ab"}')).toMatch(/no es JSON válido/);
expect(problem('{"round": 1000, "signature": null}')).toMatch(/signature/);
});
it('keeps a round other than the capsule one, for step 10 to reject', () => {
const r = parseReleaseText(DRAND_1000, 1001);
expect(r.ok && r.release.round).toBe(1000);

@ -5,14 +5,18 @@
// at step 10 like any release the caller supplies (plan of phase 2,
// decision 4, confirmed by the author on 28-09-2026).
//
// Of what is pasted only the round and the signature are read. drand's
// answer also carries `randomness`, and other drand endpoints carry a public
// key, a period or a chain hash: none of them is read, because the root of
// trust is the pinned profile and never a remote input (spec §11, §13).
// Of what is pasted only the round and the signature are read, with the
// strict reader of drand's JSON of the library (spec v0.16, §47.1), so that
// the page never reads another round than step 10 would: no name twice, names
// exact, the round a number of 1 to 2^53 - 1. drand's answer also carries
// `randomness`, and other drand endpoints carry a public key, a period or a
// chain hash: none of them is read, because the root of trust is the pinned
// profile and never a remote input (spec §11, §13).
//
// No noble here: this module is part of the page's initial bundle.
import { fromHex } from '../dkc/index.ts';
import { jsonRound, strictJSON } from '../dkc/releaseobject.ts';
/** A release as the caller supplies it, before any verification. */
export interface SuppliedRelease {
@ -52,22 +56,23 @@ export function parseReleaseText(text: string, round: number): ReleaseInput {
}
function fromJSON(s: string): ReleaseInput {
// JSON text that starts with { and parses is an object.
let v: object;
try {
v = JSON.parse(s) as object;
} catch {
// JSON text that starts with { and that the strict reader reads is an
// object without a name twice.
const members = strictJSON(new TextEncoder().encode(s));
if (members === undefined) {
return { ok: false, problem: 'Empieza por { pero no es JSON válido. Copia la respuesta de drand entera.' };
}
const round: unknown = Object.hasOwn(v, 'round') ? (v as { round: unknown }).round : undefined;
const signature: unknown = Object.hasOwn(v, 'signature') ? (v as { signature: unknown }).signature : undefined;
if (typeof round !== 'number' || !Number.isSafeInteger(round) || round < 0) {
const member = (name: string) => members.find((m) => m.name === name);
const r = member('round');
const round = r === undefined ? undefined : jsonRound(r);
if (round === undefined) {
return { ok: false, problem: 'El campo round falta o no es un número entero de ronda.' };
}
if (typeof signature !== 'string' || !/^[0-9a-fA-F]*$/.test(signature) || signature.length % 2 !== 0 || signature === '') {
const signature = member('signature');
if (signature?.kind !== '"' || !/^(?:[0-9a-fA-F]{2})+$/.test(signature.str)) {
return { ok: false, problem: 'El campo signature falta o no es hexadecimal.' };
}
return { ok: true, release: { round, signature: fromHex(signature) }, form: 'json' };
return { ok: true, release: { round, signature: fromHex(signature.str) }, form: 'json' };
}
/**

48
testdata/README.md vendored

@ -94,7 +94,7 @@ extension and a noncritical CONTROL_CBOR extension. The release that opens each
capsule, a published Quicknet signature, is in its `<name>.json`, so they all
decrypt offline.
Fourteen are in format 3. Their plaintext file is BODY, L bytes: the frame, the
Sixteen are in format 3. Their plaintext file is BODY, L bytes: the frame, the
security area, the head and the files (spec §29.2).
| Fixture | Policy | Files | Comment | L | Padding code | P | Area | Verdicts |
@ -113,6 +113,8 @@ security area, the head and the files (spec §29.2).
| `format3_signed` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F4, S0 |
| `format3_signed_cms` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F6, S0 |
| `format3_sealed` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F4, S4 |
| `format3_time_and_key_words` | `time_and_key`, a key of words and 15 dummies | 1, `secreto.txt`, with mtime | — | 32944 | 2 | 34816 | 32768 | F0, S0 |
| `format3_full_chunk` | `time_only` | 1 of 32637 bytes, with mtime | — | 65536 | 1 | 65536 | 32768 | F0, S0 |
The first five were written by a writer of v0.10, with the area of 512 bytes.
The next four only a generator of test vectors may write (spec §62.1 rule 13):
@ -153,7 +155,20 @@ their record has a `signature` object, and `seal` in the third:
`SEAL_SUBJECT`. The record has `seal`: `seal_subject`, the `token` in
hexadecimal, the `holder` of the authority as §29.7 shows it, and the time.
In the three, the record gives the commitments `control_commit`, `head_digest`
The last two are of v0.16, for the annex of recovery (spec §79):
- `format3_time_and_key_words` opens with a key of words (spec §38.1): the
text of the second vector of the annex, 79.7, «Ñandú», two spaces,
«PINGÜINO», a tab and «camión árbol Éter ola», whose words are «nandu
pinguino camion arbol eter ola». The record gives the text in `words_text`
and the identity it derives with this capsule_id in `identities`, with its
stanza in `identity_stanzas`, so that a reader without words opens it too.
- `format3_full_chunk`: BODY and P measure 65536 bytes, so PAYLOAD_AGE ends
in a full STREAM chunk of age, the last one, which the annex of v0.16
allows (79.5). `scripts/recovery_check.sh` opens both following only the
annex.
In the three signed ones, the record gives the commitments `control_commit`, `head_digest`
and `signers_digest`, the text `author_message` and its `author_code`, and the
exact content of key 2 of `SECURITY_CBOR`. An implementation checks them from
the control, the head and the security area of the fixture, and the verdicts
@ -358,7 +373,13 @@ one of CBOR (RFC 8949).
case, and may have `randomness`, which must then be SHA-256 of the
signature; it names no chain, so its `release` has no `chain_hash`. Any
failure to read it is `ERR_RELEASE_INVALID`, and so is one of more than
8192 bytes; then the round and the signature, as for an object.
8192 bytes; then the round and the signature, as for an object. Since
v0.16 it is read strictly: no object of the JSON repeats a name, names are
compared exactly once their escapes are decoded (`"round"` is
`round`, and `ROUND` another name, which is ignored), an escape of a lone
surrogate is malformed, `round` is a number without sign, fraction or
exponent from 1 to 2^53 − 1, and `signature` and `randomness` are strings.
The cases of v0.16 follow those of v0.15 in the list.
- `archive`: the lookups of the local archive `releases/archive_1000_1004.bin`,
an informative format (spec v0.15, §50). It is the `header`, the
deterministic CBOR map `{0: "datekeys-release-archive", 1: 1, 2: chain_hash,
@ -598,9 +619,11 @@ in `security_cms.json`.
## `vectors/security_cms.json`
Security areas with an author signature of `alg` 2, a CMS signature with
certificates, or a time seal of `seal_type` 2, an RFC 3161 token: 135 cases,
certificates, or a time seal of `seal_type` 2, an RFC 3161 token: 143 cases,
each with the context of its capsule, the verdicts, the result of each signer
and the lines of v0.12, §29.7, §29.10 and §29.11. They complete
and the lines of v0.16, §29.7, §29.10 and §29.11. Made again for v0.16, when a
seal without `accuracy` stopped proving that it came before the round time:
the cases about something else carry an accuracy of a second. They complete
`security.json`, whose areas have no valid signature or seal of these kinds.
The file is frozen: the certificates and the tokens are made once, with test
keys, so a second implementation reads them and must reach the same verdicts
@ -621,10 +644,13 @@ and write the same lines. Delete the file to make it again.
count. Each has the `holder` and the `issuer` as §29.7 shows them, its
`result` (`valid`, `invalid`, `absent`, `without seal`, `invalid seal`,
`out of validity` or `not verifiable`), the `seal_time` of its CAdES-T when
it has one, and `before_round_time`, whether that time plus its accuracy
precedes the round time.
it has one, and `before_round_time`, whether its seal proves that it came
before the round time: it carries `accuracy` and that time plus its accuracy
precedes the round time (v0.16). When it does not, `seal_reason` says why:
`late`, `no accuracy`, or `no accuracy, BTSP` for a token of the ETSI
policy 0.4.0.2023.1.1, which requires it; the first that holds.
- `seal_holder` and `seal_time`: the authority and the time of a valid seal
of key 3.
of key 3, and `seal_reason` the reason of S5, as for a signer.
- `lines`: the verdicts as the official SDK shows them (§29.7), byte for byte:
the names between « and », the line of each signer with its authority, the
warning that DateKeys does not check who issued the seals, and the times in
@ -644,7 +670,11 @@ table, RSASSA-PSS with and without `trailerField`, an attribute with an arc of
each string type and against each rule, `givenName` and `surname` before a
`commonName` with its NIF included; and, over an `alg` 1 signature, the seals
S1 to S5 at the edges of the token: its accuracy, its `genTime`, `ordering`,
a field after the last, the imprint, `crls` and the authority.
a field after the last, the imprint, `crls` and the authority. For v0.16: a
token without `accuracy` years before the round time and after it, one of the
BTSP policy without it and with it, an `accuracy` of 0 seconds and an empty
one, which are a precision of 0, and a signer of `alg` 2 whose seal carries
none, also under BTSP.
## `vectors/locator.json`

116
testdata/SOURCE.json vendored

@ -1,148 +1,156 @@
{
"module": "g.activething.com/go/DateKeys",
"commit": "aefc8f6dfe89037d71e22d5338a092ff21159429",
"commit": "4f7885495bd6ea666cb444519090fb5f3ea46752",
"files": {
"README.md": "d26b3f507edf5afe89600f063cb509b9ef62a908b93ed476bff964b5a443bf4b",
"README.md": "d1b99f9d84d5d59cba341663967f4ffccd7fd14b0398d32d9c14a3b6a37c14b9",
"fixtures/empty_payload.dkc": "871e9bf05b52bbae17f3adfbbf97b46e7f0e53aa8f57bcaa506e43f36f53a9d4",
"fixtures/empty_payload.inspect.json": "373e5d012b023ad58bbb54cbdffe0bed9e50c637438a4083ddb74d5414c59f59",
"fixtures/empty_payload.json": "d1fc459ab76d4ee0231a8c6b7dfc212fcf8da90e7a392b30133f646b3a9df4db",
"fixtures/empty_payload.json": "4ef16c75b3a7fe543ab37ff2c08e9e5061dc2d22f4b2724fe572c658af41c72a",
"fixtures/empty_payload.plaintext": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"fixtures/format2_empty_payload.dkc": "7aea2b5aa48b1a46053716f733d50fab9cd0b80b1be67631bcc06c5bb765dc21",
"fixtures/format2_empty_payload.inspect.json": "d0bb7356d3970986e6b197640f0b3b38abe9fabf1740b745171b358aa28903ff",
"fixtures/format2_empty_payload.json": "699cc67dd448ba69ecc52ad97a1947175b46f9d64859a6fd018ecae8fbe12508",
"fixtures/format2_empty_payload.json": "98743e5fe3833290035bc764d05e2b69d56efb3ca6563be8c9f4a0d3575f55ae",
"fixtures/format2_empty_payload.plaintext": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"fixtures/format2_time_and_key_portable.dkc": "600892659fe4890223e895876275f656995d170fda42b07fb2bec0ca51ce4b43",
"fixtures/format2_time_and_key_portable.dkk": "095b7bc516a22bf0c2366f0af3cd48bfe857a2354d6e2a9b285278b95e450fe0",
"fixtures/format2_time_and_key_portable.dkk.json": "0f95e43881b140330415ca98a284913c22dab9af3b9e16da0f4d2b1b5229d768",
"fixtures/format2_time_and_key_portable.dkk.json": "6b0879f3710ee534bc29b448e22190272d1286d096961143be29cd0905c87a10",
"fixtures/format2_time_and_key_portable.inspect.json": "522c9a98e5911c86f5f24f278971cf7c7588f6c88aaede3dd1129ee4e042868a",
"fixtures/format2_time_and_key_portable.json": "960bd78f54c91d0f8afcb9a1fdc18a7c7a7d74363ac1b77aa4ebef183d258dc6",
"fixtures/format2_time_and_key_portable.json": "c4302561ed04a182602b0bcb686d7e40472a8b8eb9458cb9a38883b00c3ac505",
"fixtures/format2_time_and_key_portable.plaintext": "937492203d207d6fe36161b8696bf1f05b8b4cc56d855c44853f4b76aad3a05b",
"fixtures/format2_time_and_key_recipients.dkc": "1a44fd8708c92e2e0a10cfcb1d864a71331ea9af25d97e1a42e969dc898959e3",
"fixtures/format2_time_and_key_recipients.dkk": "2ad99b1556086ec311d7f0b3bd3aaba05e75f45c4fa22490b0d5e8bb0b1a222e",
"fixtures/format2_time_and_key_recipients.dkk.json": "9d7e89e390e253bc1a432fa36ca2c37abbd6e88a0ac2fc25e87c1b7bb442e7d1",
"fixtures/format2_time_and_key_recipients.dkk.json": "a765cffd2532bf794a03d2da53680d8d4aa43c2721368cc8f474d3fe10e10bd0",
"fixtures/format2_time_and_key_recipients.inspect.json": "d975a9eddd45f5d59618ea2455d574d807e57daf08585e840d07986f261bf099",
"fixtures/format2_time_and_key_recipients.json": "34abdf930940ff7ac7b28de597bda3fdcadc94a074912e5220153bd85f9e096c",
"fixtures/format2_time_and_key_recipients.json": "909efe909032db2405c458952f221496751848166f9f2d23d01e7c55c286f555",
"fixtures/format2_time_and_key_recipients.plaintext": "0e9fd50e98a85953aa9cf07a11ee3c62bb3d7622f344f1c6ce744d1ed111659f",
"fixtures/format2_time_and_key_sixteen.dkc": "7aaac5c18f216bf53df326ecc817179640a53408cf25dfd50488910a762dc381",
"fixtures/format2_time_and_key_sixteen.inspect.json": "492cd0b0dca0030df9332b098d22b4f6aa4adfb57d5540de5325e3e6d5aa3667",
"fixtures/format2_time_and_key_sixteen.json": "30b8103e730e3cea5b8b39078b61022c8e3e168dec70c27a7aa69c83046853ee",
"fixtures/format2_time_and_key_sixteen.json": "a0fcad6094b3633d0d89acdcacf30f1e8b2dfd79745157455b3a1c41a4b733a0",
"fixtures/format2_time_and_key_sixteen.plaintext": "e5abfb7b5fdbf297277b6cc4729c15d85435e890b653c2e2342b7031ecd9eab9",
"fixtures/format2_time_only.dkc": "f5a40ac6b8a08a0c12db6114c2bca23522d6a77b512b509a217fb15f367813c4",
"fixtures/format2_time_only.inspect.json": "40a8683f5204c7b6369558e4775ae6bb6fed978097e9e660de64c06c167b043e",
"fixtures/format2_time_only.json": "4a3c76f1a75fbc39399c5bd7f8c355565c2ea826ad1284dd056953af8aedb009",
"fixtures/format2_time_only.json": "1ab9b7327c87b443c411beb9abf42bc4024fa5bbca6fc94a32054754bd087ed1",
"fixtures/format2_time_only.plaintext": "53b8ee821fb7b678e89d4f93da1812339f6cc1ab83aac6ed1432db99df784be5",
"fixtures/format2_time_only_bloque256.dkc": "aae769c30d04920801d8b293d30864fbe223c9c9353ec2b4907a1ee1996e39f9",
"fixtures/format2_time_only_bloque256.inspect.json": "767766414ad547f0ba95b40059e14b62c81b5489a2afcbc683bf227334d61be7",
"fixtures/format2_time_only_bloque256.json": "7dbd8dc320dbed995be9cb5830ca9a50d4fb8e956b1d468408f6199c97fc26f4",
"fixtures/format2_time_only_bloque256.json": "777b1ed31e0232c3790b4bc856005c1d9ab04f2d537ae339fd4fcbc23a93bce0",
"fixtures/format2_time_only_bloque256.plaintext": "53b8ee821fb7b678e89d4f93da1812339f6cc1ab83aac6ed1432db99df784be5",
"fixtures/format2_time_only_extensions.dkc": "fb406100d5703a2e888983b3175ed34a09a34469cc722256e5cf535dd728fbe9",
"fixtures/format2_time_only_extensions.inspect.json": "1595d793c1d35bfdaa36576b75f53d734a9e07c2a8a12036295dbaee7f5a7f5a",
"fixtures/format2_time_only_extensions.json": "bfe30126441ea1b1b2e9b7cb0cbbce71d5f26f98b77004893b46ebf4be6b573f",
"fixtures/format2_time_only_extensions.json": "ab38f1ab6ddd03dad33414ba00c1413c355b145c12ac081616c4f1390a9ca8e0",
"fixtures/format2_time_only_extensions.plaintext": "1129768e195e2f1e50b7a6f926b6eebef120212c29b5642c8a662c503b2a9131",
"fixtures/format3_area_1024.dkc": "41ea2eed0293e4fef7f4a307b7f16aaf1339f5bf6f4ded7a6a9ae1aebeb0133c",
"fixtures/format3_area_1024.inspect.json": "06e6b347926242ae5540f16a053f6a3545743986989bc0be8c39918bce968686",
"fixtures/format3_area_1024.json": "06319a474d9e6c545185aa282be407908f98cb97a1ad5128fc0d557adc45695d",
"fixtures/format3_area_1024.json": "ef1cca654c906616fc6aa0ed94346846b25fb41c5240035752fb9b930c56ec4f",
"fixtures/format3_area_1024.plaintext": "043830350a287cba1fd50f6c063f70a74209895ff0cf03147bb4e5ccdfc206b5",
"fixtures/format3_bloque256.dkc": "ff18444f434164ba8e7b26d38c76c7855dc6b0593b2fc8b4e9a95dbf9252d55d",
"fixtures/format3_bloque256.inspect.json": "d0007080da5ce079c6ffa3a56bf8ce519d2846a31cc1082fd027f401e4f7bade",
"fixtures/format3_bloque256.json": "e551a2224454cd5a416d6e91e2ab0947249a906dac3ef028a3dd45ee441e62b9",
"fixtures/format3_bloque256.json": "68c34409d7c992f318aae9d46dd6fed0a2b971dc6c6373a1cf9790ddf31e2bd1",
"fixtures/format3_bloque256.plaintext": "9ff2843e40bc1280dbfea8dce9386a42d06e8b43742c2cc6257540770cb53c73",
"fixtures/format3_comment_only.dkc": "7f98a89413f08655bbbab28b96585dfa6173c1705dd81a900deba2100d19f2ef",
"fixtures/format3_comment_only.inspect.json": "fb56eca8bf42c8c47fde4a1d6b2580fcff386f2f58b566820731dce388542196",
"fixtures/format3_comment_only.json": "1aff9f9c3531e269fc48b25c4e224e6b547ba1d5eba187ed6afa3eaff82c26f8",
"fixtures/format3_comment_only.json": "2ce37e9c76b4d01d32563757dc1bdf8f235ba88f4d0f3eae2cca7eba9f8d00a1",
"fixtures/format3_comment_only.plaintext": "bc5b05885e608f036d8a14fde8738a8c53b395b71c3bcee99c1eab37ea23e80e",
"fixtures/format3_full_chunk.dkc": "af658967b0b2c79379e25edfe3a785095e9aa2686203e93e9f30dacf27a38684",
"fixtures/format3_full_chunk.inspect.json": "1f07e80039804157b800c8db3d5d680948a3180684792ae6ac71d84190f03923",
"fixtures/format3_full_chunk.json": "353cac92a051a22db3a6e576cfa3b88e3f125436d9a8a8b4fcefd6add8136ff2",
"fixtures/format3_full_chunk.plaintext": "ec5bfd307b2e36c1b8e232031167401a1f06d205ccade7a054d39914ebf5c9f8",
"fixtures/format3_note.dkc": "da1bee54231252a0fd98439e24588125c5521f6e5a2c6641b499e6b22192c0eb",
"fixtures/format3_note.inspect.json": "dcebb62407097c757bb62552be5d315155ba8a35dec175e95c3f6fddd6e81869",
"fixtures/format3_note.json": "70bae49ca3ee84dcd4fecc572d500ce61cb014cc1e43385ae172cf414e10fa41",
"fixtures/format3_note.json": "15ab206c7444c204bb26c94f48ae53328fca63bb4d587211526dd1664b0e8dc4",
"fixtures/format3_note.plaintext": "0468737c5141be936f59d2823122e87661d4ae155a1df036b4cad1ea6620c17b",
"fixtures/format3_seal_unsupported.dkc": "ae3219fbdbd1de4cef6fade1a3fb3f6e5d5e2e8af54d9516b05f0a48136913ad",
"fixtures/format3_seal_unsupported.inspect.json": "b3a7a1038192394c1f844fed994011646f3e78d1cf311c18cb5c936249b95f14",
"fixtures/format3_seal_unsupported.json": "fedb92f17376cb90d91bce6777e152739d63bee884809ec9458dd5610d939d66",
"fixtures/format3_seal_unsupported.json": "6506de0008f936d6c5f6eedd2e951ead0d465df1b4d91016039f26d120824345",
"fixtures/format3_seal_unsupported.plaintext": "0f865221d26545762712271faf835cb2e9980f8fb15c9d3b94fb6747cf16c1df",
"fixtures/format3_sealed.dkc": "dde5a072d8783227d28279d06d3d226a1fb967c766da626f889d1c6fd76ac9c7",
"fixtures/format3_sealed.inspect.json": "b984a0755332bad838025e47f8e917b9f18b9bb5c068c2d1ef0070db8e42849c",
"fixtures/format3_sealed.json": "b925f7daae6d21c139b529a10900b9f67adb121b18670682969da3d1faa4e382",
"fixtures/format3_sealed.json": "7aafa982a3a244322b1bbf1bce71b9251b946573de164c4477233f3db0ec7eff",
"fixtures/format3_sealed.plaintext": "aea0f5feb40acd81ca3b02dd21ea15510234da3ab52b374322f3206e7632d47b",
"fixtures/format3_security_v2.dkc": "3d02b39ace010d74604554e378d22fe5ce00cecd998c0f797d657b17620b8912",
"fixtures/format3_security_v2.inspect.json": "dba4d21f1d4e228a17c761bae9a4b8c5a91cd9c0123e3141d3782a43c139321e",
"fixtures/format3_security_v2.json": "47bf9cf26e04f1c87eaf669d3d3811846bffd188dc6a90b3ac96cc45ea1ff256",
"fixtures/format3_security_v2.json": "1dc33fe75085054921365eb023c64e57b99d30b27bbcd5ef37eb44b8126ecdca",
"fixtures/format3_security_v2.plaintext": "0c58ef40e4b1c7afde0f6e0a1f4ed7e3d45405757c5143a095f0c2b58042669f",
"fixtures/format3_signature_unsupported.dkc": "e8e3106d8d73bb7b845062e0fe42af21df7d7cd8f63c335cab8dedb3e690df31",
"fixtures/format3_signature_unsupported.inspect.json": "6db653db27604cb07e2cb2c23545fb68542c121e85002762f26c6d39e63bf00c",
"fixtures/format3_signature_unsupported.json": "1e2e173ede53f38fb368289fc616325b18c7e07fd87d5eda28ace180087f151e",
"fixtures/format3_signature_unsupported.json": "052a9f2929144eaca3ca01a1f34cbe914da41a17f82356df8b3f1a1d5b856bb7",
"fixtures/format3_signature_unsupported.plaintext": "9fe05e6b3a463371b33fc6a81b81d538e572789a8d03ace9f752a931f4ca4728",
"fixtures/format3_signed.dkc": "3c7d3c9e24c02853a0c7761b93bea1120b27fce396468d8d0f68e53aeb668c5e",
"fixtures/format3_signed.inspect.json": "7c37054d542869e766147350e2fa72695f209d39a03726757008e2c2291b0e97",
"fixtures/format3_signed.json": "725b6cdb41ad9a5d34ec5327f7b0f1b667510fea6a9b1fb714be582ed55eaaed",
"fixtures/format3_signed.json": "d018866c235ccc43e2c95683c989dc0d3873d7fc16f60951a81299cc09790206",
"fixtures/format3_signed.plaintext": "3de3ccab0ac74f95a76aa45c0f85e1749d4b4a051d87e81828eff6bf24372000",
"fixtures/format3_signed_cms.dkc": "d658f8d5ac2c5550c07b8f8fd6883b2f6dc02ceafc47d436ea02d8950b2548d2",
"fixtures/format3_signed_cms.inspect.json": "afadf530e8146687b25c03f26100ebff18e7f481e0ef09378816bad582270de5",
"fixtures/format3_signed_cms.json": "eac7b9c2d2470ef140f41cf7c94e32657e2949a40f621a07340f7d2342ee7dc1",
"fixtures/format3_signed_cms.json": "af34910ff4f1ad245fb19b190b878c3eaf03c8e0e639378df37ed3488f96c12e",
"fixtures/format3_signed_cms.plaintext": "31c35eeeee856277b605fe44203a8f4786bb8f591eda3b6ee58252df5b3cf2f0",
"fixtures/format3_single.dkc": "9f68664af8733255084be9036a100b75d27bd16106bf0acff94ce469dd1d1743",
"fixtures/format3_single.inspect.json": "7878da921c17aada50e00d5911ea97e8558633a1684fb96acbd00d6f1b117529",
"fixtures/format3_single.json": "0a31c6d416ec3e6acd891172881f4f5738c36e01c9583be48f0376324641d17b",
"fixtures/format3_single.json": "5ac7e261a24c1591afc4406d444f0f7f92af810dbcc17454f3f076d22deab333",
"fixtures/format3_single.plaintext": "74f9dd84d07e95a31e6dc063bf65ce414197acf84aac445eabc76fa4e3f24936",
"fixtures/format3_time_and_key_portable.dkc": "680d29962e575689a31543df28433dae7737abd9a793e9cae92ef40920d09636",
"fixtures/format3_time_and_key_portable.dkk": "54cc64d849395234b3e093e47f432b72781ccc13f455c9ef394e3554ab566751",
"fixtures/format3_time_and_key_portable.dkk.json": "a3aff664132ec3d6da8f016c44978733ad8b9e500dec08d5c08a1a1c39a09071",
"fixtures/format3_time_and_key_portable.dkk.json": "968c5d8cde65ae066671635c9f5679233e00e982977fcaadcedfa1483a5ef246",
"fixtures/format3_time_and_key_portable.inspect.json": "f269af86f5bf84c22a1038fd78db146af93166150755eb1ca35cf15e224035b4",
"fixtures/format3_time_and_key_portable.json": "0a545aab8299c5af039b57092276cf5db62e73ef74e6e1046834f6886a34bbf0",
"fixtures/format3_time_and_key_portable.json": "8bc86ac3455f77d2996e952f4b3430e002111a731b2d8d12217cffc07cc5256f",
"fixtures/format3_time_and_key_portable.plaintext": "e6684cf607c102bfa4d6977742d5a7520b0e09483282181bd6d8f5f4ba5f7726",
"fixtures/format3_time_and_key_words.dkc": "64a11824630b6134892087a4d4ad3ee6e27941513507ad17fc87a7a2b4421e33",
"fixtures/format3_time_and_key_words.inspect.json": "838b2fe32b73bfd2ed45b2104170ec8532909d03ed0772e9baacf2c254fa4c32",
"fixtures/format3_time_and_key_words.json": "083e84c42ea89544fcc7c39665ec8815d272727bbc0907932412a43402396c92",
"fixtures/format3_time_and_key_words.plaintext": "2ff49df00ad9a37446c626be6d0353e94bd3bf41d73a6141e10f77b586abcb67",
"fixtures/format3_tree.dkc": "217f378faaf795f6a9c416b564fb8931bb2e896918aee870120fd14f9a5da7d1",
"fixtures/format3_tree.inspect.json": "643a9dfdc2d0c44b8a1636909c66ed81bfd8c50df2a4cad6566832a8e47ba938",
"fixtures/format3_tree.json": "1d2ed3e28c5075ead9898757b971298273c4b20acee27911286dc250aa7d0143",
"fixtures/format3_tree.json": "3a97244352fffae5cc980bb278ecd3e201d6c89ac867656e1ca4956e71b10c1a",
"fixtures/format3_tree.plaintext": "f69ac5f450966f7d0e9161aa37451d4260b194a750e3e132c02e8a15ba561cfa",
"fixtures/format3_unsigned.dkc": "317ab722ae3812a25ddd78b4c98c586363e5587c8d3634c881ce7c421af19168",
"fixtures/format3_unsigned.inspect.json": "2f52f7286d6bd4c846ddd63b10b4989b25d17501a989a2e9deb25e4db0859e1a",
"fixtures/format3_unsigned.json": "bce5d7fcf60ddb553e2bbe5b501892ae7a13488b4095fdad598dc3207690dfb3",
"fixtures/format3_unsigned.json": "cac5778e7e8d5cf86bcba0c7893ae45387cba43b93a8a7877ffc60d6e469b346",
"fixtures/format3_unsigned.plaintext": "25527e5e2e1ce02056d4419fb89f7b0ce35e4920f93217f58dcf62a4377f8af5",
"fixtures/time_and_key_portable.dkc": "2e97878078bae6358037a9c264f379a3cbe839f767d69836b0343f35657b2972",
"fixtures/time_and_key_portable.dkk": "e528fa2c832c91119f0684bb9d6fb3c4c2d0d55183482890e7c4fe92f668426a",
"fixtures/time_and_key_portable.dkk.json": "4535028d6559cd368a44a6f03ebf8bcbcc2bdac4fcf13374aae541618b81c99f",
"fixtures/time_and_key_portable.dkk.json": "6f1c5c6fae50d37e7afd9731992353063e2c9ed4ef3b736c840a10f60d43ef0e",
"fixtures/time_and_key_portable.inspect.json": "238c1f8ca6a6bf69f20bf26f5676e89a0b07e83b4362628560fc2f7522a202c9",
"fixtures/time_and_key_portable.json": "389f36834ffb86a4c60950872a540caf8a8a94f65d02f56ce5f7922e06bf933c",
"fixtures/time_and_key_portable.json": "9c80f7a9ace3e5cbb180d9f6109626ec6c8ae3a8b131e26aa3feda60f35600db",
"fixtures/time_and_key_portable.plaintext": "937492203d207d6fe36161b8696bf1f05b8b4cc56d855c44853f4b76aad3a05b",
"fixtures/time_and_key_portable_extension.dkk": "0bf463a7c65627b7dda2234d728df89ec5b835816a2a37b91497d8fecc5ea548",
"fixtures/time_and_key_portable_extension.dkk.json": "e6f4015f10403926a8e2d3399f78ba99a48ce6c4760e24174c1521046c23efb2",
"fixtures/time_and_key_portable_extension.dkk.json": "9f931c77aa5e98b51d21f3b875b307fd647496390c295abd98a0c9a4e6825286",
"fixtures/time_and_key_recipients.dkc": "69ac110380f5d768b5b6afaa157a50ed17d8ceccfbd4604ffa5b6da38539b635",
"fixtures/time_and_key_recipients.dkk": "19f6c47150c3194712d454f43c7392b7344e6b4e7b074d83e9ca5f563a8e072f",
"fixtures/time_and_key_recipients.dkk.json": "f206ed1a51fc6aac7b2faabd2e3519224f68f7b9b9643653dbdf5b6139f042e0",
"fixtures/time_and_key_recipients.dkk.json": "9bd11c87bf1789872d6f0989ecebdd07958544e159ac462d799096220ce378c5",
"fixtures/time_and_key_recipients.inspect.json": "4b32c63d18febe0772837fbcd75a0c971e31378bf799201b720a9d32bdcd8c2b",
"fixtures/time_and_key_recipients.json": "2cdbc03ef027879b36c68de56e205960e774858a3ff170aba66d630de27c7303",
"fixtures/time_and_key_recipients.json": "733da9ee7d1122a254f0ad45e5c2e530884fe75fc85648ad0df093ad5dbb42b2",
"fixtures/time_and_key_recipients.plaintext": "0e9fd50e98a85953aa9cf07a11ee3c62bb3d7622f344f1c6ce744d1ed111659f",
"fixtures/time_only.dkc": "99e915810d595f1092700b728f5e5081d78efe83f5343e76325b1bcc2c33ccf2",
"fixtures/time_only.inspect.json": "a4d45f945d6ba6616c01e120ac1133785e5279fea7dcab706b5feee287be8884",
"fixtures/time_only.json": "9e66fedbfcf6ffec45648d8afdb7592eecd645751703051d1f60af1d0b7f9f01",
"fixtures/time_only.json": "a1d321bee1f31fb70affb32327cb360c7134c9dda3879519e683e5a6606d4add",
"fixtures/time_only.plaintext": "53b8ee821fb7b678e89d4f93da1812339f6cc1ab83aac6ed1432db99df784be5",
"fixtures/time_only_extensions.dkc": "0446c9b73e267adcb24e5cc89afba2544a386ec9a050016e06517a4a57aa2085",
"fixtures/time_only_extensions.inspect.json": "6f957b028da8a4a495b5e951ced0b91e0678128dac4e962b02d024b9439a0ba1",
"fixtures/time_only_extensions.json": "4bb636805cc681ba1c74aa426f5afda72580f4350929a720da49eeccee3eb2be",
"fixtures/time_only_extensions.json": "5dd015716747d6b8b3b9ab7aa6eccdb6edde4bd5023c1f15e75b1cd3961d9fd9",
"fixtures/time_only_extensions.plaintext": "1129768e195e2f1e50b7a6f926b6eebef120212c29b5642c8a662c503b2a9131",
"releases/1000.cbor": "5d2e86210d2e8d64ce36e55edf3ff6c8997fda4bc06fec7fd5feb0dd6cab8293",
"releases/1001.cbor": "2b55dcc09dfe8fa97192aa6d9a85f9fc50206142d52f66329261cfe9e03de755",
"releases/1004.cbor": "aabdffe0fb944d8796528a6682fdbafb448b1e5da164ee039b6c3ee7f354e766",
"releases/2000.cbor": "9e37be0004850faaa8541658a90ee8aba0832fccf2b695df42c89cb3f7de29ff",
"releases/archive_1000_1004.bin": "bb53d542abd704f3af9f6436c9178f65bf812a06630607b3aad3a09eaed0cce1",
"vectors/cbor.json": "715c8e7ca88d17c4e68a8764350217f108e96484e59282d384a032169d36bfb8",
"vectors/dk1.json": "e2b849b1f606e7961a8571c305dcd0c4374f03c8943a3a715b20a9a43002ea44",
"vectors/ed25519_strict.json": "eb47ac6b5e879ca3e115f6551b81e5b6fe5bf5050aa7b829804e915705c3a1ad",
"vectors/head_schema.json": "29493360d4cf97c3ad488a8ce58221804b17511523756cea1efb87e6fbc13444",
"vectors/inspect_differential.json": "bdc5210415084cde71aa84fbaa7ebbcd81e0f1800b411c6dbacc5e553b4021dd",
"vectors/locator.json": "02476c2f5e421bfc35e2f7b2498ab1d395fd7ef5971fb46dd16fe0abfb6f7a11",
"vectors/mutations.json": "e3ce6a57a57e49bfdb726fb8e9e6e36711c65a23b08e751aab00f8c4dbfcade3",
"vectors/note.json": "f02feea92b22c98227c21e725b3d3e8b303ec647c4cab3fd9b4b71261273818e",
"vectors/padding.json": "7cd6ac71fd978e21c5f93870a211035f98028b9020422f727e407c477e4514bb",
"vectors/path_fold.json": "75e4fa473da4b394d32ac107a5e9559b0d3358ce1b39e5d7403366aa35c2efff",
"vectors/paths.json": "a33ecdbd6a191d693600e18879e15a5813d77b133d46f30c5535aae72eeaeb04",
"vectors/profile_quicknet.json": "e291d5167cdce9b9e24993571a7f349e35fbbcfea793665883ec9ec8d479b498",
"vectors/quicknet_rounds.json": "bf990896dddc51a91e309143fede773adeae918ef47433e0ab6132a4456ce9a8",
"vectors/release.json": "97bd46e055024a00f6a765f840b47ebfea5dad1e08a88c70d2cf42a77df6d2ba",
"vectors/resolved_ip.json": "7c554e7c3f272a62a89c3f3e97203dc7d5dc50290bbe356f4a8efd44a19eea70",
"vectors/security.json": "6045492767cbeb02fce5b6faf6cd0e179ffe96c898c8c023793e4a138b0277f0",
"vectors/security_cms.json": "4915fa3cfe93b1ad92e91a68bba7517c3b2dc3e33fd9def7340b3a045eff99cf",
"vectors/tlock_ibe.json": "5c1def934c89c1638187058e9dcb76ac9fffd148885a9869a1dc7b19d2df76b6",
"vectors/tlock_steps.json": "661c5214c30ea3ea08e7f54f779346b6e838e68a0732ea2110ddcbf07a25cd35",
"vectors/wordkey.json": "1ef9f07d84e7d99d68433a89371c79a407c4e10f33547210bfdf1d7378956d6b"
"vectors/cbor.json": "d2aacec9423d6e52dd199dee10989f161f271cfdf016f64d8ac6e0ed0a2f6719",
"vectors/dk1.json": "68192059df37af531601a814f9f34b39f226d44df6118375dcc4a960219346aa",
"vectors/ed25519_strict.json": "0469a6516423ffe9380fa41a358b208a583f22ed632e4b6b8768a172f061ed0d",
"vectors/head_schema.json": "7e9039aee039fa8ca09e26f73c45a740161db2ced5a667e48a9a2a692d8188bf",
"vectors/inspect_differential.json": "5f2b768a6b17c059fda779f7894115c16bdec30f5645800e2796c45c1733e64f",
"vectors/locator.json": "b1cbd347664faf15dfd3974f046675a6a315a1c4dedf67c3fc9f5d42ae6d3e57",
"vectors/mutations.json": "2f86da3018e68ec0cb263c608e0f7db719fd23f2bb2450f9aa054b71f958e53d",
"vectors/note.json": "af8806904f5f7cd5b672f00043918bd86a9576fe3d1b3724186ac1de18b15c9b",
"vectors/padding.json": "77504260c4d1db0e7ab3da4b9b97b3416be774ceb7813a41e61bd236b44310ef",
"vectors/path_fold.json": "6adcdbeec02b4082e23433c2b8ad9653a1febce9f19df18ace8668bf0254e362",
"vectors/paths.json": "23f9f809e17d335a4b307ec6fee3b8af8f69e7f3ad697f3f7a94ae559d1efd33",
"vectors/profile_quicknet.json": "ac0afa6019b232055375af6e5630c2fff505ca0a88774f095f4c1f519f6c2427",
"vectors/quicknet_rounds.json": "9e0630a03f839ab2152dfd9150b462e6faff77fad9fd8fe3dfcf18f0944bdedb",
"vectors/release.json": "fb00ad416f8b616f3a615a7a9bb735fa4a6fa254fe0ff7bce922260dfbd3927a",
"vectors/resolved_ip.json": "ca3dd96afdb767f57111fc7985fc1f70aa380939df7048a5b55c81172a14807f",
"vectors/security.json": "b45492a3559cebb6fb41c809e61a64f16d23521870e7a26f8cba92c8332bff31",
"vectors/security_cms.json": "b06a252fd0e72312c0ae65be30981d0651b0d76f471f651f3aaa8cc7678ec177",
"vectors/tlock_ibe.json": "e853eebad87722995901b063404de45383a26748299e5c493b8eb6c420de53b4",
"vectors/tlock_steps.json": "7e0b77593466aa213217ea03f4a344db8181759ab26d094dfc299d4ef7ef33d2",
"vectors/wordkey.json": "89494f886361bdbee7274f21356a814b606ef3b1716ee66117e4f3feca733dff"
}
}

@ -1,6 +1,6 @@
{
"description": "time_only capsule with an empty payload",
"spec": "0.15",
"spec": "0.16",
"format": 1,
"file": "empty_payload.dkc",
"sha256": "871e9bf05b52bbae17f3adfbbf97b46e7f0e53aa8f57bcaa506e43f36f53a9d4",

@ -1,6 +1,6 @@
{
"description": "format 2 time_only capsule with an empty content: L = 0, P = 256",
"spec": "0.15",
"spec": "0.16",
"format": 2,
"file": "format2_empty_payload.dkc",
"sha256": "7aea2b5aa48b1a46053716f733d50fab9cd0b80b1be67631bcc06c5bb765dc21",

@ -1,6 +1,6 @@
{
"description": "portable X25519 .dkk of format2_time_and_key_portable.dkc",
"spec": "0.15",
"spec": "0.16",
"file": "format2_time_and_key_portable.dkk",
"sha256": "095b7bc516a22bf0c2366f0af3cd48bfe857a2354d6e2a9b285278b95e450fe0",
"credential_id": "e3c7be83cbf1fbd6b115c96411b3bd01",

@ -1,6 +1,6 @@
{
"description": "format 2 time_and_key capsule with one credential, a portable .dkk, and 15 dummies",
"spec": "0.15",
"spec": "0.16",
"format": 2,
"file": "format2_time_and_key_portable.dkc",
"sha256": "600892659fe4890223e895876275f656995d170fda42b07fb2bec0ca51ce4b43",

@ -1,6 +1,6 @@
{
"description": "portable X25519 .dkk of format2_time_and_key_recipients.dkc",
"spec": "0.15",
"spec": "0.16",
"file": "format2_time_and_key_recipients.dkk",
"sha256": "2ad99b1556086ec311d7f0b3bd3aaba05e75f45c4fa22490b0d5e8bb0b1a222e",
"credential_id": "93cedf68421710e83908ec683b104436",

@ -1,6 +1,6 @@
{
"description": "format 2 time_and_key capsule for three known X25519 recipients and a portable .dkk, and 12 dummies",
"spec": "0.15",
"spec": "0.16",
"format": 2,
"file": "format2_time_and_key_recipients.dkc",
"sha256": "1a44fd8708c92e2e0a10cfcb1d864a71331ea9af25d97e1a42e969dc898959e3",

@ -1,6 +1,6 @@
{
"description": "format 2 time_and_key capsule for sixteen known X25519 recipients, without dummies",
"spec": "0.15",
"spec": "0.16",
"format": 2,
"file": "format2_time_and_key_sixteen.dkc",
"sha256": "7aaac5c18f216bf53df326ecc817179640a53408cf25dfd50488910a762dc381",

@ -1,6 +1,6 @@
{
"description": "format 2 time_only capsule, padding code 2 (reforzado): L = 78000, P = 79872, two STREAM chunks",
"spec": "0.15",
"spec": "0.16",
"format": 2,
"file": "format2_time_only.dkc",
"sha256": "f5a40ac6b8a08a0c12db6114c2bca23522d6a77b512b509a217fb15f367813c4",

@ -1,6 +1,6 @@
{
"description": "format 2 time_only capsule with the content of format2_time_only and padding code 1 (bloque256): L = 78000, P = 78080",
"spec": "0.15",
"spec": "0.16",
"format": 2,
"file": "format2_time_only_bloque256.dkc",
"sha256": "aae769c30d04920801d8b293d30864fbe223c9c9353ec2b4907a1ee1996e39f9",

@ -1,6 +1,6 @@
{
"description": "format 2 time_only capsule with a noncritical PUBLIC_HEADER extension and a noncritical CONTROL_CBOR extension",
"spec": "0.15",
"spec": "0.16",
"format": 2,
"file": "format2_time_only_extensions.dkc",
"sha256": "fb406100d5703a2e888983b3175ed34a09a34469cc722256e5cf535dd728fbe9",

@ -1,6 +1,6 @@
{
"description": "format 3 time_only capsule with a security area of 1024 bytes, as a later version may write it, holding the empty security",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_area_1024.dkc",
"sha256": "41ea2eed0293e4fef7f4a307b7f16aaf1339f5bf6f4ded7a6a9ae1aebeb0133c",

@ -1,6 +1,6 @@
{
"description": "format 3 time_only capsule with padding code 1 (bloque256) and one file of 20000 bytes",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_bloque256.dkc",
"sha256": "ff18444f434164ba8e7b26d38c76c7855dc6b0593b2fc8b4e9a95dbf9252d55d",

@ -1,6 +1,6 @@
{
"description": "format 3 time_only capsule with a comment of two lines, the second one with a TAB, a declared author and no files",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_comment_only.dkc",
"sha256": "7f98a89413f08655bbbab28b96585dfa6173c1705dd81a900deba2100d19f2ef",

Binary file not shown.

@ -0,0 +1,61 @@
{
"file": "format3_full_chunk.dkc",
"format": 3,
"capsule_id": "9c672412223e65667407568b2ffab62d",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"profile": "datekeys:quicknet:v1",
"round": 1000,
"unlock_at": "2023-08-23T15:59:24Z",
"access_policy": "time_only",
"valid": true,
"checks": [
{
"step": 1,
"name": "parse DKC1",
"ok": true,
"detail": "magic DKC1"
},
{
"step": 2,
"name": "prelude",
"ok": true,
"detail": "DKC1 v3, PUBLIC_HEADER_LEN=121, SEALED_CONTROL_LEN=458"
},
{
"step": 3,
"name": "public header",
"ok": true,
"detail": "121 bytes"
},
{
"step": 4,
"name": "header validation",
"ok": true,
"detail": "capsule_id=9c672412223e65667407568b2ffab62d datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0 policy=time_only profile=datekeys:quicknet:v1"
},
{
"step": 5,
"name": "sealed control structure",
"ok": true,
"detail": "one tlock stanza"
},
{
"step": 6,
"name": "payload structure",
"ok": true,
"detail": "one X25519 stanza"
},
{
"step": 7,
"name": "condition",
"ok": true,
"detail": "round 1000, unlock at 2023-08-23T15:59:24Z"
},
{
"step": 8,
"name": "tlock stanza",
"ok": true,
"detail": "round 1000, chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
}
]
}

@ -0,0 +1,152 @@
{
"description": "format 3 time_only capsule with padding code 1 (bloque256) and one file, whose BODY and P are 65536 bytes: PAYLOAD_AGE ends in a full STREAM chunk, which the annex of spec v0.16 (79.5) allows",
"spec": "0.16",
"format": 3,
"file": "format3_full_chunk.dkc",
"sha256": "af658967b0b2c79379e25edfe3a785095e9aa2686203e93e9f30dacf27a38684",
"release": {
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
},
"prelude": "444b43310300000000000079000001ca",
"public_header": "a5006a646174656b6579636170010102509c672412223e65667407568b2ffab62d037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d5441774d48300400",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"capsule_id": "9c672412223e65667407568b2ffab62d",
"access_policy": "time_only",
"structure": "time_only",
"unlock_at": "2023-08-23T15:59:24Z",
"header_binding": "ea2cd41f6216135cd349fceb1891772252e3f90ed945fc71fd73852a982fbf1f",
"outer_stanzas": [
{
"type": "tlock",
"args": [
"1000",
"52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
]
}
],
"payload_stanzas": [
{
"type": "X25519",
"args": [
"QsnJmO1LaffXIjpp0ms0Rh2IXta9VzX38GP6TMYlAHA"
]
}
],
"control_cbor": "a60070646174656b6579732d636f6e74726f6c0103025820ea2cd41f6216135cd349fceb1891772252e3f90ed945fc71fd73852a982fbf1f0358205be72d0295b21d37b6c8380a91d7c875a2dcab7257fa7ea44dcee5ae6280c95a064800000000000100000701",
"payload_identity": "5be72d0295b21d37b6c8380a91d7c875a2dcab7257fa7ea44dcee5ae6280c95a",
"payload_length": 65536,
"padding": 1,
"padded_length": 65536,
"plaintext_file": "format3_full_chunk.plaintext",
"plaintext_sha256": "ec5bfd307b2e36c1b8e232031167401a1f06d205ccade7a054d39914ebf5c9f8",
"area_len": 32768,
"security_cbor": "a20071646174656b6579732d73656375726974790101",
"head_cbor": "a4006d646174656b6579732d68656164010102582029068855227bf0d314be5b3b5e16392b7157581cf62b0c8d156f74017e2f19bf0581a6006a626c6f7175652e62696e01197f7d020003197f7d045820b84764fc9aa813643c9b76145bfdc87673a4b83ccb3cdfaa3c07bbbc5dba560d051a6abcf9c0",
"salt": "29068855227bf0d314be5b3b5e16392b7157581cf62b0c8d156f74017e2f19bf",
"content_offset": 32899,
"files": [
{
"path": "bloque.bin",
"size": 32637,
"start": 0,
"end": 32637,
"sha256": "b84764fc9aa813643c9b76145bfdc87673a4b83ccb3cdfaa3c07bbbc5dba560d",
"mtime": 1790769600
}
],
"verdicts": {
"signature": "F0",
"seal": "S0",
"lines": [
"Sin firma de autor."
]
},
"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": 17,
"name": "open payload",
"ok": true
},
{
"step": 18,
"name": "commit",
"ok": true
}
]
}

Binary file not shown.

@ -1,6 +1,6 @@
{
"description": "format 3 time_only capsule with a single file, nota.txt, and the public note «Cartas del viaje a Lisboa» in the noncritical array of PUBLIC_HEADER (spec v0.11, §24.1)",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_note.dkc",
"sha256": "da1bee54231252a0fd98439e24588125c5521f6e5a2c6641b499e6b22192c0eb",

@ -1,6 +1,6 @@
{
"description": "format 3 time_only capsule with an author-signature of alg 4294967295, as in format3_signature_unsupported, and a seal of seal_type 4294967295, reserved for tests, with a random token of 32 bytes: verdicts F1 and S1",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_seal_unsupported.dkc",
"sha256": "ae3219fbdbd1de4cef6fade1a3fb3f6e5d5e2e8af54d9516b05f0a48136913ad",

@ -1,6 +1,6 @@
{
"description": "format 3 time_only capsule with a single file, nota.txt, signed with alg 1 by the test key of format3_signed and sealed with seal_type 2 by a test time-stamping authority before the round time: verdicts F4 and S4, with SEAL_SUBJECT and the token in the record",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_sealed.dkc",
"sha256": "dde5a072d8783227d28279d06d3d226a1fb967c766da626f889d1c6fd76ac9c7",

@ -1,6 +1,6 @@
{
"description": "format 3 time_only capsule whose security is of version 2: verdict X",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_security_v2.dkc",
"sha256": "3d02b39ace010d74604554e378d22fe5ce00cecd998c0f797d657b17620b8912",

@ -1,6 +1,6 @@
{
"description": "format 3 time_only capsule with an author-signature of alg 4294967295, a random key of 32 bytes and a random signature of 64: verdicts F1 and S0",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_signature_unsupported.dkc",
"sha256": "e8e3106d8d73bb7b845062e0fe42af21df7d7cd8f63c335cab8dedb3e690df31",

@ -1,6 +1,6 @@
{
"description": "format 3 time_only capsule with a single file, nota.txt, signed with alg 1 by a test key whose seed the record gives: verdict F4, and the commitments and the message of the signature",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_signed.dkc",
"sha256": "3c7d3c9e24c02853a0c7761b93bea1120b27fce396468d8d0f68e53aeb668c5e",

@ -1,6 +1,6 @@
{
"description": "format 3 time_only capsule with a single file, nota.txt, signed with alg 2 by two test certificates, an ECDSA P-256 one and an RSA 2048 one, each sealed by a test time-stamping authority before the round time: verdict F6, with the certificates, the commitments, SIGNERS and the result of each signer in the record",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_signed_cms.dkc",
"sha256": "d658f8d5ac2c5550c07b8f8fd6883b2f6dc02ceafc47d436ea02d8950b2548d2",

@ -1,6 +1,6 @@
{
"description": "format 3 time_only capsule with a single file, nota.txt, with its mtime",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_single.dkc",
"sha256": "9f68664af8733255084be9036a100b75d27bd16106bf0acff94ce469dd1d1743",

@ -1,6 +1,6 @@
{
"description": "portable X25519 .dkk of format3_time_and_key_portable.dkc",
"spec": "0.15",
"spec": "0.16",
"file": "format3_time_and_key_portable.dkk",
"sha256": "54cc64d849395234b3e093e47f432b72781ccc13f455c9ef394e3554ab566751",
"credential_id": "bdb483fba42daf0b409f44d23033f362",

@ -1,6 +1,6 @@
{
"description": "format 3 time_and_key capsule with one credential, a portable .dkk, and 15 dummies",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_time_and_key_portable.dkc",
"sha256": "680d29962e575689a31543df28433dae7737abd9a793e9cae92ef40920d09636",

@ -0,0 +1,61 @@
{
"file": "format3_time_and_key_words.dkc",
"format": 3,
"capsule_id": "30e865a5c1e148c14410817a65eecfd1",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"profile": "datekeys:quicknet:v1",
"round": 1000,
"unlock_at": "2023-08-23T15:59:24Z",
"access_policy": "time_and_key",
"valid": true,
"checks": [
{
"step": 1,
"name": "parse DKC1",
"ok": true,
"detail": "magic DKC1"
},
{
"step": 2,
"name": "prelude",
"ok": true,
"detail": "DKC1 v3, PUBLIC_HEADER_LEN=121, SEALED_CONTROL_LEN=2128"
},
{
"step": 3,
"name": "public header",
"ok": true,
"detail": "121 bytes"
},
{
"step": 4,
"name": "header validation",
"ok": true,
"detail": "capsule_id=30e865a5c1e148c14410817a65eecfd1 datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0 policy=time_and_key profile=datekeys:quicknet:v1"
},
{
"step": 5,
"name": "sealed control structure",
"ok": true,
"detail": "one tlock stanza"
},
{
"step": 6,
"name": "payload structure",
"ok": true,
"detail": "one X25519 stanza"
},
{
"step": 7,
"name": "condition",
"ok": true,
"detail": "round 1000, unlock at 2023-08-23T15:59:24Z"
},
{
"step": 8,
"name": "tlock stanza",
"ok": true,
"detail": "round 1000, chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
}
]
}

@ -0,0 +1,267 @@
{
"description": "format 3 time_and_key capsule with one credential, a key of words, and 15 dummies: the text of the vector of the annex of spec v0.16 (79.7), «Ñandú», two spaces, «PINGÜINO», a tab and «camión árbol Éter ola», whose words are «nandu pinguino camion arbol eter ola»",
"spec": "0.16",
"format": 3,
"file": "format3_time_and_key_words.dkc",
"sha256": "64a11824630b6134892087a4d4ad3ee6e27941513507ad17fc87a7a2b4421e33",
"release": {
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
},
"prelude": "444b4331030000000000007900000850",
"public_header": "a5006a646174656b65796361700101025030e865a5c1e148c14410817a65eecfd1037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d5441774d48300401",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"capsule_id": "30e865a5c1e148c14410817a65eecfd1",
"access_policy": "time_and_key",
"structure": "time_and_key",
"unlock_at": "2023-08-23T15:59:24Z",
"header_binding": "4bc6ecdcd80e37d0ed1f51ef781db6800564c309e222ba6151665ddd1fe4c99d",
"outer_stanzas": [
{
"type": "tlock",
"args": [
"1000",
"52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
]
}
],
"payload_stanzas": [
{
"type": "X25519",
"args": [
"y+DAEDO0p07P4YDE2pHXFhIpzVKiv+YUku15lbotTjM"
]
}
],
"inner_stanzas": [
{
"type": "X25519",
"args": [
"MSeAnfrz4I+Pn3yhL+/+hkNASJPwQzNQLxeiYu4YeBc"
]
},
{
"type": "X25519",
"args": [
"5VwR2r6e5MknAz//TJrRNRYOtQOeqCaTDdJ+A8QV+S8"
]
},
{
"type": "X25519",
"args": [
"X09hnKn4HOIFwf6H4GfJe3vSG5f5/EqDtV6Bx+OPgFY"
]
},
{
"type": "X25519",
"args": [
"GhhTyyFwZItGXCDKsG3sIcDYJrD9QU45ybqQxjUQUQ4"
]
},
{
"type": "X25519",
"args": [
"SS7ZyiY/U4O6PokUavgTMRMghx4lHDiJ+k+K/rsC8FU"
]
},
{
"type": "X25519",
"args": [
"zchoCmsfxz/dR7YbYGOEoMkSjrDVzOLt5al0CprXG2E"
]
},
{
"type": "X25519",
"args": [
"xg6iz12nCO/jm/rpa4k6aUM1mu2MUm7CwLJsF8qDwUA"
]
},
{
"type": "X25519",
"args": [
"NfHtp8ATUtya6UcudC1FTfiL2SMfrKCFj8V3/slMfEw"
]
},
{
"type": "X25519",
"args": [
"89gYRmkwISvkX1BNb/opcezkVOmNkj7mh89WkniGkWU"
]
},
{
"type": "X25519",
"args": [
"yBQHKKHENrGZfD9qysIoZ/2sMcSXvKClt6VUL4Sx6Sc"
]
},
{
"type": "X25519",
"args": [
"POtK0+b9IiRSRxlNBYm7DPzApiULuJaVWWOSHap4C00"
]
},
{
"type": "X25519",
"args": [
"2dGTxtsPQRUHGAros0o30jqTpEa4+6d5KRt4U86Qx2o"
]
},
{
"type": "X25519",
"args": [
"EKRbRpTDe5KZJgGgsfvnwb0iaHXkzVsqplsmCj3HzHY"
]
},
{
"type": "X25519",
"args": [
"+wHShSAq5PhGXD9ZHs+AwjXxq0TRbcpXwjf46fwW1wc"
]
},
{
"type": "X25519",
"args": [
"HRclh6xyQdsMOSV2MBP0ewe7JB+6EvgTod/4BOYXNAo"
]
},
{
"type": "X25519",
"args": [
"ZIfAt94wDxyQ4Y3WWw54v7H55b26Ye19HFn7USqSwnE"
]
}
],
"identity_stanzas": [
1
],
"identities": [
"AGE-SECRET-KEY-1CWYUF8E9RJ4SYWL8D7WN43M30XHFFFXD6LSZDRYS2WZ8CMUWWENSEXCGDP"
],
"words_text": "Ñandú PINGÜINO\tcamión árbol Éter ola",
"control_cbor": "a60070646174656b6579732d636f6e74726f6c01030258204bc6ecdcd80e37d0ed1f51ef781db6800564c309e222ba6151665ddd1fe4c99d035820a5f41e788e692ea55a3931bc5e25c7e6180ecc1d14235994198f8e00edb7420a064800000000000080b00702",
"payload_identity": "a5f41e788e692ea55a3931bc5e25c7e6180ecc1d14235994198f8e00edb7420a",
"payload_length": 32944,
"padding": 2,
"padded_length": 34816,
"plaintext_file": "format3_time_and_key_words.plaintext",
"plaintext_sha256": "2ff49df00ad9a37446c626be6d0353e94bd3bf41d73a6141e10f77b586abcb67",
"area_len": 32768,
"security_cbor": "a20071646174656b6579732d73656375726974790101",
"head_cbor": "a4006d646174656b6579732d686561640101025820702740f9850339d6907640e1de069200437bf8688288ed4cc735b37bd875eb260581a6006b7365637265746f2e74787401182e020003182e045820937492203d207d6fe36161b8696bf1f05b8b4cc56d855c44853f4b76aad3a05b051a6abcf9c0",
"salt": "702740f9850339d6907640e1de069200437bf8688288ed4cc735b37bd875eb26",
"content_offset": 32898,
"files": [
{
"path": "secreto.txt",
"size": 46,
"start": 0,
"end": 46,
"sha256": "937492203d207d6fe36161b8696bf1f05b8b4cc56d855c44853f4b76aad3a05b",
"mtime": 1790769600
}
],
"verdicts": {
"signature": "F0",
"seal": "S0",
"lines": [
"Sin firma de autor."
]
},
"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": "access credential",
"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": 13,
"name": "open access layer",
"ok": true
},
{
"step": 14,
"name": "control",
"ok": true
},
{
"step": 15,
"name": "header binding",
"ok": true
},
{
"step": 16,
"name": "payload identity",
"ok": true
},
{
"step": 17,
"name": "open payload",
"ok": true
},
{
"step": 18,
"name": "commit",
"ok": true
}
]
}

@ -1,6 +1,6 @@
{
"description": "format 3 time_only capsule with five files in three folders, one of them over two STREAM chunks and one without mtime, a comment of two lines and a declared author",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_tree.dkc",
"sha256": "217f378faaf795f6a9c416b564fb8931bb2e896918aee870120fd14f9a5da7d1",

@ -1,6 +1,6 @@
{
"description": "format 3 time_only capsule with a single file, nota.txt, as format3_signed, without a signature: the area of 32 KiB of spec v0.11 holds the empty security, and P is the one of format3_signed",
"spec": "0.15",
"spec": "0.16",
"format": 3,
"file": "format3_unsigned.dkc",
"sha256": "317ab722ae3812a25ddd78b4c98c586363e5587c8d3634c881ce7c421af19168",

@ -1,6 +1,6 @@
{
"description": "portable X25519 .dkk of time_and_key_portable.dkc",
"spec": "0.15",
"spec": "0.16",
"file": "time_and_key_portable.dkk",
"sha256": "e528fa2c832c91119f0684bb9d6fb3c4c2d0d55183482890e7c4fe92f668426a",
"credential_id": "3955e944a3c60cfa1fd6485e9693c77d",

@ -1,6 +1,6 @@
{
"description": "time_and_key capsule whose only recipient is a portable .dkk",
"spec": "0.15",
"spec": "0.16",
"format": 1,
"file": "time_and_key_portable.dkc",
"sha256": "2e97878078bae6358037a9c264f379a3cbe839f767d69836b0343f35657b2972",

@ -1,6 +1,6 @@
{
"description": "portable X25519 .dkk of time_and_key_portable.dkc with a noncritical extension: the credential of time_and_key_portable.dkk re-issued with org.example.delivery",
"spec": "0.15",
"spec": "0.16",
"file": "time_and_key_portable_extension.dkk",
"sha256": "0bf463a7c65627b7dda2234d728df89ec5b835816a2a37b91497d8fecc5ea548",
"credential_id": "3955e944a3c60cfa1fd6485e9693c77d",

@ -1,6 +1,6 @@
{
"description": "portable X25519 .dkk of time_and_key_recipients.dkc",
"spec": "0.15",
"spec": "0.16",
"file": "time_and_key_recipients.dkk",
"sha256": "19f6c47150c3194712d454f43c7392b7344e6b4e7b074d83e9ca5f563a8e072f",
"credential_id": "b89292aedf6d05d584cec9a871ce8735",

@ -1,6 +1,6 @@
{
"description": "time_and_key capsule for two known X25519 recipients and a portable .dkk",
"spec": "0.15",
"spec": "0.16",
"format": 1,
"file": "time_and_key_recipients.dkc",
"sha256": "69ac110380f5d768b5b6afaa157a50ed17d8ceccfbd4604ffa5b6da38539b635",

@ -1,6 +1,6 @@
{
"description": "time_only capsule, two STREAM chunks, no extensions",
"spec": "0.15",
"spec": "0.16",
"format": 1,
"file": "time_only.dkc",
"sha256": "99e915810d595f1092700b728f5e5081d78efe83f5343e76325b1bcc2c33ccf2",

@ -1,6 +1,6 @@
{
"description": "time_only capsule with a noncritical PUBLIC_HEADER extension and a noncritical CONTROL_CBOR extension",
"spec": "0.15",
"spec": "0.16",
"format": 1,
"file": "time_only_extensions.dkc",
"sha256": "0446c9b73e267adcb24e5cc89afba2544a386ec9a050016e06517a4a57aa2085",

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "CBOR profile of spec §58 and the schemas of spec/datekeys.cddl, generated by the reference implementation. accept and reject are walked as one data item of the profile with the limits of walk; schemas are decoded with the decoder of their schema. See testdata/README.md.",
"walk": {
"max_depth": 3,

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "Canonical dk1_ strings and rejected encodings (spec §18, §19, §66), generated by the reference implementation.",
"vectors": [
{

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "Ed25519 signatures and the result of the strict profile of the author signature (spec v0.11, §29.9), after the cases of «Taming the many EdDSAs»; stdlib is the result of crypto/ed25519 of Go, for the record. Generated by the reference implementation. See testdata/README.md.",
"vectors": [
{

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "HEAD_CBOR of format 3 (spec §29.4 to §29.6) and the result of decoding it with no extension known, generated by the reference implementation: layer 2 (type tag and version), layer 3 (the CDDL with R1 and R8), then layer 4 in key order (spec §69.1). See testdata/README.md.",
"heads": [
{

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "Differential corpus of the pre-unlock checks (spec §63 steps 1 to 8): deterministic mutations of the official .dkc fixtures with the verdict of the reference implementation. See testdata/README.md.",
"format": "Each mutation is bases[base].file (in testdata/fixtures) with its edits applied. An edit is [at, delete, insert]: the delete bytes at offset at of the base are replaced by the bytes of the hex string insert. The edits of one mutation refer to offsets of the unmodified base, are sorted by offset and do not overlap. result is the verdict of steps 1 to 8 of spec §63 (capsule.Inspect, the Quicknet profile pinned, no extension known, no network, no secret): ok, or the normative error code, with step the step that failed. kind names the generator of the mutation and is informative.",
"seed": 20260925,

@ -1,6 +1,6 @@
{
"description": "The extension datekeys.capsule of a .dkk and what it points to (spec v0.12, 44.1): an envelope of age with its header apart from its rest, the rest hidden in a host file, the locator sealed with tlock for round 1000, and the data of the extension. On the same envelope, what a reader rejects and what it uses (64): addresses, a locator with rejected and usable addresses, resources of the rest, data of the extension and plaintexts of the locator. Frozen. See testdata/README.md.",
"spec": "0.15",
"spec": "0.16",
"round": 1000,
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"note": "Cartas del viaje a Lisboa",

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "Mutation corpus of spec §64 and further cases of capsule.TestMutationCorpus, generated by the reference implementation: each case is a .dkc and what the reader is given, with the normative error and the step of spec §63 at which capsule.Open fails. See testdata/README.md.",
"cases": [
{

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "The data of the public note, datekeys.note version 1 in the noncritical array of PUBLIC_HEADER (spec §24.1): the text in UTF-8, from 1 to 1024 bytes, that meets the rules of the declared author of §29.6. See testdata/README.md.",
"notes": [
{

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "Padding rules of the payload of a format 2 capsule (spec §29.1): for each content length L, P with code 1 (bloque256) and code 2 (reforzado), and the length of PAYLOAD_AGE for each. e, s and last_bits are informative. Generated by the reference implementation. See testdata/README.md.",
"l_max": 8936830510563328,
"vectors": [

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "The key of R7 (spec §29.5) of segments, with the Unicode 18.0.0 tables of §29.5.1, generated by the reference implementation: nfd is NFD(segment) and key is NFD(fold(NFD(s'))), s' the segment without ZWNJ, ZWJ, VS15 and VS16. See testdata/README.md.",
"unicode_version": "18.0.0",
"tables_digest": "07cf5d54aea1cd13a3ecef14a06976cc49a3cdad755cf9bc10395178b93aeb07",

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "Paths of a format 3 head (spec §29.5) with the Unicode 18.0.0 and best-fit tables of §29.5.1, generated by the reference implementation. paths: one path and the rules of one entry, R2 to R6c and R10; trees: the paths of a head, of 0 bytes each, and the result of decoding it. See testdata/README.md.",
"unicode_version": "18.0.0",
"tables_digest": "07cf5d54aea1cd13a3ecef14a06976cc49a3cdad755cf9bc10395178b93aeb07",

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"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",

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"profile": "datekeys:quicknet:v1",
"description": "Quicknet date to round resolution (spec §15, §16, §65), generated by the reference implementation.",
"vectors": [

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "The release object (spec v0.15, §47.1), and drand's JSON as the input of the caller, each checked against the pinned Quicknet profile and the round of a DateKey as step 10 of spec §63 checks a release that the caller supplies; and the lookups of a local release archive (spec v0.15, §50). See testdata/README.md.",
"profile": "datekeys:quicknet:v1",
"objects": [
@ -432,6 +432,204 @@
},
"result": "ERR_RELEASE_INVALID",
"text": "provider: the signature is not a canonical point encoding, or does not verify as the BLS signature of round 1000 under datekeys:quicknet:v1: ERR_RELEASE_INVALID"
},
{
"name": "round twice",
"input": "{\"round\":1000,\"round\":1001,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "round twice, once escaped as round",
"input": "{\"round\":1000,\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "round twice, the second null",
"input": "{\"round\":1000,\"round\":null,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "round escaped as round",
"input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"release": {
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
},
"result": "ok"
},
{
"name": "Round instead of round",
"input": "{\"Round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "ROUND beside round: another name, ignored",
"input": "{\"round\":1000,\"ROUND\":1001,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"release": {
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
},
"result": "ok"
},
{
"name": "round null",
"input": "{\"round\":null,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "round 1000.0",
"input": "{\"round\":1000.0,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "round 1e3",
"input": "{\"round\":1e3,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "round -1000",
"input": "{\"round\":-1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "round 0",
"input": "{\"round\":0,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "round 2^53",
"input": "{\"round\":9007199254740992,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "round 2^53 - 1 for a DateKey of round 1000",
"input": "{\"round\":9007199254740991,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"release": {
"round": 9007199254740991,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
},
"result": "ERR_ROUND_MISMATCH",
"text": "provider: release for round 9007199254740991, expected 1000: ERR_ROUND_MISMATCH"
},
{
"name": "round with a leading zero",
"input": "{\"round\":01000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "signature twice",
"input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "signature null",
"input": "{\"round\":1000,\"signature\":null}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "randomness empty",
"input": "{\"round\":1000,\"randomness\":\"\",\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: randomness does not match the signature: ERR_RELEASE_INVALID"
},
{
"name": "randomness null",
"input": "{\"round\":1000,\"randomness\":null,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "another name twice",
"input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"note\":1,\"note\":2}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "a name twice in a nested object",
"input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"meta\":{\"a\":1,\"a\":2}}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "nested objects and arrays, ignored",
"input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"meta\":{\"a\":[1,{\"a\":2}],\"b\":{},\"c\":[]}}",
"round": 1000,
"release": {
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
},
"result": "ok"
},
{
"name": "a lone surrogate in another name",
"input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"\\ud800\":1}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "a lone low surrogate in a value",
"input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"note\":\"\\udc00\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "a surrogate pair in a value",
"input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"note\":\"😀\"}",
"round": 1000,
"release": {
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
},
"result": "ok"
},
{
"name": "a tab inside a string",
"input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"note\":\"a\tb\"}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
},
{
"name": "something after the object",
"input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}{}",
"round": 1000,
"result": "ERR_RELEASE_INVALID",
"text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID"
}
],
"archive": {

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "The IP address that the name of an https address of a locator resolves to, and whether a reader may connect (spec v0.13, 44.1): a public address, or an address of NAT64 (RFC 6052) of 64:ff9b::/96 or of the NAT64 prefix of the network, whose IPv4 address inside is public. See testdata/README.md.",
"cases": [
{

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "SECURITY_CBOR of format 3, exactly its SECURITY_LEN bytes, the verdicts of the signature and of the seal in the context of the file, and their lines (spec §29.3, §29.7, §29.9). See testdata/README.md.",
"context": {
"control_commit": "0101010101010101010101010101010101010101010101010101010101010101",

File diff suppressed because one or more lines are too long

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "H2 of the IBE-CCA of tlock (spec §63 step 11): SHA-256 of \"IBE-H2\" and the 576 bytes of an element of GT, c1 before c0 at every level of the tower and each coordinate of Fp in 48 bytes big-endian (the order of kilic/bls12-381), truncated to 16 bytes. Generated by the reference implementation with drand/kyber-bls12381, the pairing of tlock.",
"vectors": [
{

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "Steps 10 and 11 of spec §63 for Quicknet, value by value, over published releases. Step 10: M = SHA-256(uint64_be(round)), H(M) the hash to G1 of RFC 9380 with the suite BLS12381G1_XMD:SHA-256_SSWU_RO_ and the DST dst, and e(H(M), public_key) = e(signature, G2). Step 11: a stanza body U || V || W built with the sigma and the file key of the vector; H2 = SHA-256(\"IBE-H2\" || e(signature, U))[:16], sigma = V XOR H2, H4 = SHA-256(\"IBE-H4\" || sigma)[:16], file_key = W XOR H4, and r = H3(sigma, file_key): h3_base = SHA-256(\"IBE-H3\" || sigma || file_key), then for i = 1, 2, ... d = SHA-256(uint16_le(i) || h3_base), its first byte shifted one bit to the right, until it is below the order of the group; r·G2 = U. Generated by the reference implementation and checked against drand, kyber, tlock and agewrap. See testdata/README.md.",
"profile": "datekeys:quicknet:v1",
"scheme": "bls-unchained-g1-rfc9380",

@ -1,5 +1,5 @@
{
"spec": "0.15",
"spec": "0.16",
"description": "The key of words (spec §38.1): the words of a text, after NFD, without U+0300 to U+036F, in simple lower case of Unicode 18.0.0 and split by the spaces of the list; what a writer refuses; and the identity, PBKDF2-HMAC-SHA256 of 600000 rounds, for a chain hash, a round and a capsule_id. See testdata/README.md.",
"normalize": [
{
@ -152,6 +152,30 @@
"ab"
]
},
{
"name": "the text of the annex of v0.16",
"text": "Ñandú PINGÜINO\tcamión árbol Éter ola",
"words": [
"nandu",
"pinguino",
"camion",
"arbol",
"eter",
"ola"
]
},
{
"name": "the text of the annex of v0.16, with its marks apart",
"text": "Ñandú PINGÜINO\tcamión árbol Éter ola",
"words": [
"nandu",
"pinguino",
"camion",
"arbol",
"eter",
"ola"
]
},
{
"name": "U+0009 is a space",
"text": "uno\tdos",
@ -648,6 +672,22 @@
"key": "fceec4d8ca8de86c85a1f26ed49f82a2b38431bd0ce36db995ae7dfd49b96e41",
"recipient": "age1fqk6hflp8q5um2qrl5ckd8lu4x75rhtuncfus7f7up3v3v0zss0stdpqkr"
},
{
"name": "the second vector of the annex of v0.16, 79.7",
"words": [
"nandu",
"pinguino",
"camion",
"arbol",
"eter",
"ola"
],
"chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971",
"round": 1000,
"capsule_id": "000102030405060708090a0b0c0d0e0f",
"key": "273295d29370126a3be50b743132718d3cd9137fb3bb4cb20aa23163d2e19bb7",
"recipient": "age1zje929pk9ej2kqp4pjra5rs4cly7k7deaeclz0jrp5843u6qcy5qwsvjdh"
},
{
"name": "the next round",
"words": [

@ -1,6 +1,6 @@
{
"module": "g.activething.com/go/DateKeys",
"commit": "aefc8f6dfe89037d71e22d5338a092ff21159429",
"commit": "4f7885495bd6ea666cb444519090fb5f3ea46752",
"files": {
"README.md": "a29122e04cd8dac3e44d6f271da2ca99ac98e28b27e88c16b58162fe3eebedad",
"en.txt": "6d557f0693958fb5e650b68b5bee585eb82cf4da32965505c789e924743bc522",

Loading…
Cancel
Save

Powered by TurnKey Linux.