The author approved the draft v0.16 on 7 October 2026 with the
recommendation of each of its decisions: the review of v0.15 by Astra, a
seal without accuracy, the key of words in the recovery annex, a full last
chunk of age, the evidence of a signature with certificates and drand's
JSON read strictly. Only the date of the header changes in the text;
spec/README.md records its SHA-256, and annex/recovery.md is written again
with it. The READMEs, SECURITY.md, the traceability, the CDDL header, the
README of testdata and the CHANGELOG name v0.16 and its tag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editor had written the escapes of the sources as their characters:
the cases "round escaped as round" and "round twice, once escaped as
round" of release.json had a plain round, and the surrogate pair of
"a surrogate pair in a value" a plain emoji, so the shared vectors tested
no escaped name. TestStrictJSON had lost its escaped é, its pair and the
escape after a lone high surrogate. They are escapes again, and the texts
of words_test.go too, so that no mark or invisible character hides in the
source. release.json gained 26 cases of drand's JSON, not 25 as b570338
says; the draft and the CHANGELOG now say 26.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SpecVersion is 0.16, and so is the spec field of every file of testdata,
the frozen security_cms.json and locator.json included. annex/recovery.md
is §79 of the draft v0.16, with the key of words in 79.7. The draft lists
the two new fixtures in 67, says in 79 what recovery_check.sh opens, and
names in 76 the tests that now exist. The CHANGELOG has its section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/recovery opens a time_and_key capsule with the words of a key of
words (-words FILE), as the annex says in 79.7: the normalization without
tables for the text of the DateKeys lists (printable ASCII, the ASCII
spaces, á é í ó ú ü ñ and their capitals, and the marks U+0300 to U+036F),
and for any other text the full one, NFD, without the marks, simple lower
case and the spaces of 38.1, from UnicodeData.txt of Unicode 18.0.0
(-unicodedata FILE), checked by its SHA-256; then PBKDF2-HMAC-SHA256 of the
standard library. Its tests check both normalizations against every case
of wordkey.json and the two vectors of the annex.
Two fixtures of v0.16: format3_time_and_key_words, opened with the text of
the second vector of the annex, recorded in words_text with the identity it
gives; and format3_full_chunk, whose BODY and P are 65536 bytes, so that
PAYLOAD_AGE ends in a full STREAM chunk (79.5). recovery_check.sh opens
both. wordkey.json gains the text of the annex, also with its marks apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JSON of RFC 8259 in UTF-8 whose value is an object; no object repeats a
name, and names are compared exactly once their escapes are decoded, so
"round" is round and ROUND another name; an escape of a lone surrogate
is malformed; round is a number without sign, fraction or exponent from 1
to 2^53 - 1; signature and randomness are strings (spec v0.16, 47.1). Go's
encoding/json kept the last of two repeated names and matched ROUND to
round. ParseDrandJSON, exported, is the one reader of drand's JSON:
provider/drand reads the answers of the relays with it too. The error
texts do not change.
release.json gains 25 cases of drand's JSON for v0.16.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A valid seal is S4 only when its token carries accuracy and t plus the
accuracy is before round_time; otherwise S5, whose text gives the reason,
the first that holds: sealed after or too close, no accuracy under the BTSP
policy of ETSI EN 319 421 (0.4.0.2023.1.1), or no accuracy (spec v0.16,
29.7, 29.11). The line of a signer of F6 whose seal does not prove it says
so with the same reason. cms.Token gains HasAccuracy, Policy and BTSP;
Verdicts gain SealReason and SignerLine.Reason; EncryptFiles returns the
verdicts of the area it wrote in Result.Security, so that a writer warns of
a seal without accuracy (rule 19).
security_cms.json is made again: 143 cases, the seals about something else
with an accuracy of a second, and the new cases of 64 with seal_reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The author chose CC-BY-ND-4.0 over CC-BY-4.0: the specification may be
copied and shared unchanged, with credit; a modified version or a
translation needs the written permission of its author. The recovery
annex, which travels alone next to every capsule, says so in its title.
The code stays Apache-2.0 and the word lists keep their own licenses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A valid seal without accuracy no longer proves that it came before the
unlock date: S5, with its reason, and a reason of its own for a token of
the ETSI BTSP policy (29.7, 29.11). The recovery annex derives a key of
words without DateKeys software (79.7, new): a recipe without tables for
the letters of the DateKeys lists, and UnicodeData.txt of Unicode 18.0.0,
named by its SHA-256, for any other text, with two vectors. The last chunk
of an age file may be full (79.5). A signature with certificates keeps the
chains without their roots and the OCSP responses that fit, and the writer
says what it leaves out (29.10, rules 21 and 22). drand's JSON is read
strictly: no repeated names, exact names after their escapes, an integer
round from 1 to 2^53 - 1 (47.1). Errata in 1, 74, 77 and 79.
No format changes. SpecVersion stays 0.15 until the author approves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec §38.1: the official SDK SHOULD warn not to reuse a password, since
once the date has come the capsule lets whoever holds it test guesses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SHOULDs of the official SDK that the CLI did not follow yet. encrypt
writes next to the .dkc the recovery annex, FILE.dkc.recuperacion.txt
(spec §62.1, rule 27): datekeys.RecoveryAnnex, annex/recovery.md, which is
§79 of the specification under a title with its version and SHA-256, the
same for every capsule; TestRecoveryAnnex checks it against the text of
SpecVersion. -no-recovery leaves it out. encrypt also says what opening the
capsule years later will take (rule 26): the .dkc, a credential of a
time_and_key capsule, and the release of its round, which an archive of
releases or a cache service must keep if drand no longer serves it; and
beyond one year it recommends time_and_key to a time_only capsule (§7.6).
profile.Status and StatusOf give the state of a pinned profile in the
registry of §71, which DateKeys does not publish yet: Quicknet is active.
encrypt writes no capsule with a profile that is not active, and decrypt
and inspect warn when the profile of a capsule is compromised.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For whoever does not trust the random numbers of a computer, as the author
decided: five dice for each word, read in a fixed order, give a number
from 11111 to 66666, the position of the word in a list of 7776 words, the
first die the most significant. wordkey.DiceNumber and DiceWord number the
words and give the word of a number; DiceWords takes several numbers, at
least 6 and never the same word twice, which is rolled again; DiceList is
the list numbered for dice, a line for each word with its dice and a tab,
as the EFF publishes its own: for en it is the file of the EFF, byte for
byte.
encrypt -dice TEXT and -dice-file FILE take the words from the dice of the
list -dic and show them: the words open the capsule, the numbers give them
only with that list. datekeys wordlist [-dic LIST] writes the numbered
list, to print it, and its SHA-256, which wordkey/lists/README.md records.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wordkey/lists/en.txt is the large wordlist for passphrases of the EFF,
7776 words, CC BY 4.0 under its copyright policy, without the dice number
of each line and in its order, so that the position of a word still gives
its dice, 11111 for the first. encrypt -new-words takes it by default, as
the author decided; -dic es takes the Spanish one. The alphabet of en is a
to z and the ASCII hyphen of its four compound words, such as t-shirt,
kept so that no word loses its dice. lists/README.md records its source,
the SHA-256 of the download, the change and the license.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The seed of TestGenerate, bytes.Repeat([]byte{7, 1, 200, 33}, 64), only
ever gives indices 1793 and 2081: both calls of Generate ended in EOF, the
test ignored the errors and compared two empty results. It now draws from
a SHA-256 counter stream and checks the errors and the count, draws an
index twice to see that it is drawn again, and keeps the old seed as the
case of a source that runs out. Found by the review of the Dart port.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wordkey.CheckList takes the language of the list and refuses a word with a
character that is not a letter of its alphabet, which only the code gives:
for es, a to z, the five vowels with an acute accent, u with diaeresis and
n with tilde, in lower case and NFC. A Cyrillic letter that looks like a
Latin one, a capital, a digit or the carriage return of a file with CRLF
lines would be written down and typed again with another character, and
the capsule would not open. The Spanish list passes as it is; its SHA-256
does not change.
wordkey.Bits is the strength of the words that Generate draws, log2 of the
number of draws in order, and encrypt -new-words prints it: 90 bits for 7
words of 7776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wordkey.Generate draws words uniformly with crypto/rand from a built-in
list, and encrypt -new-words FILE writes them to a new file (-dic, default
es; -word-count, default 7). wordkey.List and CheckList refuse a list of
fewer than 2048 words or with two words that are one once normalized
(spec 38.1). The Spanish list, 7776 words, is a draft not yet reviewed,
licensed CC BY-SA 4.0 as an adaptation of FrequencyWords; its source,
method and SHA-256 are in wordkey/lists/README.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
20 hours ago
113 changed files with 24520 additions and 522 deletions
`spec-v0.16`: un sello sin precisión no acredita nada antes de la fecha de
apertura, el JSON de drand se lee de forma estricta y el anexo para abrir una
cápsula sin software de DateKeys deriva también una llave de palabras. La
v0.15 trajo la recuperación de cápsulas a largo plazo: el objeto release, un
release en la mano que el reloj no detiene, y archivos y servicios de caché
que guardan los releases de todas las rondas.
[English version](README.md).
DateKeys cifra datos de forma que solo puedan abrirse a partir de un instante
@ -66,13 +67,13 @@ Hay tres números de versión, cada uno con su significado:
| Versión | Dónde | Cambia cuando |
|---|---|---|
| Formato | Dentro de los objetos: el formato de la cápsula, el `VERSION` del prelude de DKC1, 3 al escribir y de 1 a 3 al leer, que es también la versión de schema de CONTROL_CBOR; y 1 en la trama de DKK1 y en el schema de los demás objetos, incluidos el head y `security` del formato 3 | Cambia el formato. Un lector rechaza una versión que no conoce (spec §22, §70) |
| Especificación | `datekeys.SpecVersion`, hoy `0.15`, y el tag `spec-v0.15`. Un borrador, como lo fue la v0.15, no tiene tag ni la cambia | Cambia el texto normativo. §76 del spec recoge cada cambio con su caso |
| Especificación | `datekeys.SpecVersion`, hoy `0.16`, y el tag `spec-v0.16`. Un borrador, como lo fue la v0.16, no tiene tag ni la cambia | Cambia el texto normativo. §76 del spec recoge cada cambio con su caso |
| Módulo | Los tags de este módulo Go, `vX.Y.Z`, y `datekeys.Version()` | Cambia la API o el comportamiento. Versionado semántico, sin promesa de estabilidad antes de v1.0.0 |
`datekeys version` imprime la versión del módulo, la del spec y la del toolchain de Go. Un binario compilado en un checkout muestra la pseudo-versión de su commit, por ejemplo `v0.0.0-20260928105528-9ac9cd952f04`.
Cada release dice qué cubre, aquí y en el [CHANGELOG](CHANGELOG.md). El código de esta rama, aún sin publicar, cubre:
- la especificación 0.15: escribe el formato 3 de cápsula y lee los formatos 1, 2 y 3;
- la especificación 0.16: escribe el formato 3 de cápsula y lee los formatos 1, 2 y 3;
- el perfil Quicknet pinneado, y cualquier perfil del scheme `bls-unchained-g1-rfc9380`, el único que admite la v0.14 del spec;
- cifrado, inspección y apertura, firmas de autor y sellos, la nota pública, la llave de palabras y el localizador, y la CLI;
- todos los vectores y fixtures compartidos de [`testdata/`](testdata).
@ -137,6 +138,8 @@ datekeys profile hash
datekeys version
```
`encrypt` escribe junto a la cápsula `FICHERO.dkc.recuperacion.txt`, el anexo de la especificación sobre cómo abrir una cápsula sin software de DateKeys (spec §79), salvo con `-no-recovery`; y dice qué hará falta para abrirla años después: el `.dkc`, una credencial si es `time_and_key` y el release de su ronda, que tendrá que conservar un archivo de releases o un servicio de caché si drand ya no lo sirve (spec §50). A más de un año, recomienda `time_and_key` a una cápsula `time_only` (spec §7.6). Con un perfil que no esté activo en el registro de §71 no escribe ninguna cápsula, y `decrypt` e `inspect` avisan si el perfil de una cápsula está comprometido.
`encrypt` nunca usa la red. Cada `-in` es un fichero o una carpeta; una
carpeta da su nombre como primer segmento de sus rutas, como hace un
navegador, y se recorre sin seguir enlaces, tomando solo ficheros regulares y
@ -180,7 +183,17 @@ creador que nadie ha comprobado; una nota que incumple las reglas de texto no
se muestra, y los dos lo dicen (`public_note_unusable` en `inspect -json`).
`-words` y `-words-file` dan a una cápsula `time_and_key` una llave de
palabras: al menos seis palabras distintas de tres letras o más, que la abren
con `decrypt -words-file` en lugar de una `.dkk` (spec §38.1).
con `decrypt -words-file` en lugar de una `.dkk` (spec §38.1). `-new-words
FICHERO` las sortea en su lugar, 7 por defecto, de una lista incluida
(`-dic en`, la de la EFF, por defecto, o `-dic es`; `-word-count N`), las
escribe en un fichero nuevo y dice su fuerza en bits: las palabras que elige
una persona son más débiles. `-dice` y `-dice-file` las sacan de unos dados,
para quien no se fíe del azar de un ordenador: cinco dados por palabra dan un
número del 11111 al 66666, su posición en la lista, y `datekeys wordlist`
escribe la lista numerada para los dados, para imprimirla, con su SHA-256;
la de `en` es el fichero de la EFF, byte a byte. `encrypt` muestra las
palabras de los dados: la cápsula la abren ellas, no los números. Una lista solo se usa si cada palabra es del alfabeto de su idioma.
Las listas y su licencia están en [`wordkey/lists`](wordkey/lists/README.md).
## Librería
@ -315,6 +328,6 @@ go test -tags integration ./capsule ./provider/drand # Quicknet en vivo
`spec-v0.16`: a seal without accuracy proves nothing before the opening date,
drand's JSON is read strictly, and the annex to open a capsule without
DateKeys software derives a key of words too. v0.15 brought the long-term
recovery of capsules: the release object, a release in hand that the clock
does not stop, and archives and cache services that keep the releases of all
rounds.
[Versión en español](README.es.md).
DateKeys encrypts data so that it can only be opened after a chosen instant.
@ -66,13 +67,13 @@ Three version numbers, each with its own meaning:
| Version | Where | Changes when |
|---|---|---|
| Format | Inside the objects: the capsule format, the `VERSION` of the DKC1 prelude, 3 when written and 1 to 3 when read, which is also the schema version of CONTROL_CBOR; and 1 for the framing of DKK1 and the schema of the other objects, the head and security of format 3 included | The format changes. A reader rejects a version it does not know (spec §22, §70) |
| Specification | `datekeys.SpecVersion`, today `0.15`, and the tag `spec-v0.15`. A draft, such as v0.15 was, has no tag and does not change it | The normative text changes. Spec §76 records each change with its case |
| Specification | `datekeys.SpecVersion`, today `0.16`, and the tag `spec-v0.16`. A draft, such as v0.16 was, has no tag and does not change it | The normative text changes. Spec §76 records each change with its case |
| Module | The tags of this Go module, `vX.Y.Z`, and `datekeys.Version()` | The API or the behaviour changes. Semantic versioning, with no stability promise before v1.0.0 |
`datekeys version` prints the module version, the specification and the Go toolchain. A binary built in a checkout shows the pseudo-version of its commit, for example `v0.0.0-20260928105528-9ac9cd952f04`.
Each release states what it covers, here and in the [CHANGELOG](CHANGELOG.md). The code of this branch, not yet released, covers:
- specification 0.15: it writes capsule format 3 and reads formats 1, 2 and 3;
- specification 0.16: it writes capsule format 3 and reads formats 1, 2 and 3;
- the pinned Quicknet profile, and any profile of the scheme `bls-unchained-g1-rfc9380`, the only one spec v0.14 admits;
- encryption, inspection and opening, author signatures and seals, the public note, the key of words and the locator, and the CLI;
- every shared vector and fixture of [`testdata/`](testdata).
`encrypt` writes next to the capsule `FILE.dkc.recuperacion.txt`, the annex of the specification, in Spanish, on how to open a capsule without DateKeys software (spec §79), unless `-no-recovery`; and says what opening the capsule years later will take: the `.dkc`, a credential of a `time_and_key` capsule and the release of its round, which an archive of releases or a cache service must keep if drand no longer serves it (spec §50). Beyond one year, it recommends `time_and_key` to a `time_only` capsule (spec §7.6). A profile that is not active in the registry of §71 writes no capsule, and `decrypt` and `inspect` warn when the profile of a capsule is compromised.
`encrypt` never touches the network. Each `-in` is a file or a folder; a
folder gives its name as the first segment of its paths, as a browser does,
and is walked without following links, taking regular files only and leaving
@ -179,7 +184,18 @@ that nobody has checked; a note that breaks the rules of text is not shown,
and both say so (`public_note_unusable` in `inspect -json`).
`-words` and `-words-file` give a `time_and_key` capsule a key of words: at
least six different words of three or more letters, which open it with
`decrypt -words-file` instead of a `.dkk` (spec §38.1).
`decrypt -words-file` instead of a `.dkk` (spec §38.1). `-new-words FILE`
draws them at random instead, 7 by default, from a built-in list (`-dic
en`, the list of the EFF, by default, or `-dic es`; `-word-count N`), writes
them to a new file and says their strength in bits: words a person chooses
are weaker. `-dice` and `-dice-file` take them from dice instead, for
whoever does not trust the random numbers of a computer: five dice for each
word give a number from 11111 to 66666, its position in the list, and
`datekeys wordlist` writes the list numbered for dice, to print it, with its
SHA-256; for `en` it is the file of the EFF, byte for byte. `encrypt` shows
the words of the dice: they open the capsule, not the numbers. A
list is used only if each word is of the alphabet of its language. The lists
and their license are in [`wordkey/lists`](wordkey/lists/README.md).
## Library
@ -310,6 +326,6 @@ go test -tags integration ./capsule ./provider/drand # live Quicknet
# 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.16, cuyo texto tiene el SHA-256 807d4fe85ac09ad6f97abc75ab3e2156bb2f3fb0dc589777f4420627fad545e1. 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
Este anexo no es normativo. Dice cómo abrir una cápsula de Quicknet sin ningún software de DateKeys, por si dentro de décadas no existe. Repite lo que fijan las secciones que cita, que deciden en caso de duda.
La regla 27 de §62.1 recomienda al SDK oficial guardar este anexo junto al `.dkc`. No contiene ningún dato de una cápsula.
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;
- 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`, 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
Son los de §12, y pueden no estar ya en ningún otro sitio:
Los puntos se codifican comprimidos, en el formato de ZCash (§12.2): 48 bytes en G1, la firma, y 96 en G2, la clave pública y U, con la coordenada c1 antes que c0.
### 79.2 La cápsula
El `.dkc` empieza por 16 bytes (§22):
```text
0 4 "DKC1"
4 1 VERSION: el formato, 1, 2 o 3
5 1 0
6 2 0
8 4 PUBLIC_HEADER_LEN, entero big-endian
12 4 SEALED_CONTROL_LEN, entero big-endian
```
Le siguen `PUBLIC_HEADER`, de `PUBLIC_HEADER_LEN` bytes; `SEALED_CONTROL`, de `SEALED_CONTROL_LEN` bytes; y `PAYLOAD_AGE`, desde el byte 16 + `PUBLIC_HEADER_LEN` + `SEALED_CONTROL_LEN` hasta el final del fichero.
`PUBLIC_HEADER` es un mapa CBOR (§24). Su clave 2 es `capsule_id`, 16 bytes; su clave 4, la política, 0 para `time_only` y 1 para `time_and_key`; y su clave 3, la DateKey, un texto `dk1_` seguido del Base64URL sin relleno de un JSON (§18):
Un objeto release es un mapa CBOR (§47.1): la clave 0 es `"datekeys-release"`; la 2, el `chain_hash`, que ha de ser el de 79.1; la 3, la ronda, que ha de ser la de la DateKey; y la 4, la firma, de 48 bytes. Así lo sirven la Release API y un servicio de caché. En un archivo de releases (§50), la firma de la ronda r son los 48 bytes que empiezan en |cabecera| + (r − primera ronda)·48, y 48 ceros si el archivo no la tiene. Un relay de drand la entrega como JSON, `{"round": …, "signature": "…"}`, con la firma en hexadecimal. Hoy se pide así, aunque las direcciones pueden cambiar:
```text
GET https://api.drand.sh/v2/chains/<chain_hash>/rounds/<ronda>
```
La firma no necesita confianza: se verifica (§63, paso 10). Con M el SHA-256 de la ronda en 8 bytes big-endian, y H el hash a G1 de RFC 9380 con la suite `BLS12381G1_XMD:SHA-256_SSWU_RO_` y el DST de 79.1:
```text
e(H(M), clave_pública) == e(firma, G2)
```
con e el pairing de G1 × G2, el primer argumento en G1 y el segundo en G2, y G2 el generador de G2. La firma y la clave pública se decodifican como puntos comprimidos válidos del subgrupo, distintos del punto en el infinito. Para una ronda solo hay una firma válida: cualquier copia que verifique es el release.
### 79.4 El stanza tlock y FK_TIME
`SEALED_CONTROL` es un fichero `age` (79.5) cuya cabecera tiene un solo stanza:
El cuerpo mide 128 bytes, U ‖ V ‖ W: U, de 96 bytes, un punto de G2, y V y W, de 16 bytes. Con la firma del release (§63, paso 11):
```text
sigma = V XOR H2(e(firma, U))
FK_TIME = W XOR H4(sigma)
r = H3(sigma, FK_TIME)
comprobar r·G2 == U
```
- H2(x) son los 16 primeros bytes de SHA-256(`IBE-H2` ‖ x), con x los 576 bytes del elemento de GT en el orden de §63, «Serialización de GT en H2»: c1 antes que c0 en cada nivel de la torre, y c2, c1, c0 en Fp6, cada elemento de Fp en 48 bytes big-endian. Una librería que serializa con c0 primero da otro H2. El vector de `testdata/vectors/tlock_ibe.json` lo comprueba: H2(e(G1, G2)) = `cb87319f24560b5231579a09ad79f12e`, con G1 y G2 los generadores.
- H4(sigma) son los 16 primeros bytes de SHA-256(`IBE-H4` ‖ sigma).
- H3(sigma, FK_TIME): base = SHA-256(`IBE-H3` ‖ sigma ‖ FK_TIME); para i = 1, 2, … hasta 65534, d = SHA-256(uint16_le(i) ‖ base), con el contador en 2 bytes little-endian delante de base; se desplaza un bit a la derecha el primer byte de d, solo ese byte; y si d, como entero big-endian de 32 bytes, es menor que q, r = d.
Las etiquetas son los bytes ASCII, sin longitud ni terminador. FK_TIME, de 16 bytes, es la file key del fichero `age` de `SEALED_CONTROL`. `testdata/vectors/tlock_steps.json` da cada valor intermedio de cinco stanzas.
### 79.5 Abrir un fichero `age` con su file key
Es la especificación `age` v1 de C2SP (§77), resumida. Un fichero `age` es una cabecera de texto y un payload binario:
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 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 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 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
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:
- en formato 1, el contenido entero;
- en formato 2, el contenido en sus L primeros bytes, seguido de ceros;
- en formato 3, `BODY` en sus L primeros bytes, seguido de ceros (§29.2).
`BODY` empieza por tres enteros de 4 bytes big-endian: `AREA_LEN`, `SECURITY_LEN` y `HEAD_LEN`. Siguen el área de `security`, de `AREA_LEN` bytes, que se puede saltar: solo da los veredictos de la firma y del sello (§29.7); el head, un mapa CBOR de `HEAD_LEN` bytes que empieza en 12 + `AREA_LEN`; y los ficheros, desde 12 + `AREA_LEN` + `HEAD_LEN`, el origen de sus desplazamientos.
La clave 5 del head es la lista de ficheros (§29.4). Cada uno es un mapa:
```text
0 → ruta, con "/" entre carpetas
1 → tamaño
2 → start
3 → end
4 → SHA-256 de sus bytes
5 → mtime, en segundos Unix, opcional
```
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.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.
t.Skip("wrote annex/recovery.md; build the package again to embed it")
}
ifRecoveryAnnex!=want{
t.Fatalf("annex/recovery.md is not §79 of the specification %s: write it again with DATEKEYS_WRITE_ANNEX=1 go test -run TestRecoveryAnnex .",SpecVersion)
}
for_,s:=range[]string{"## 79. Anexo informativo: recuperación sin software DateKeys","### 79.7 La llave de palabras","### 79.9 Lo que el anexo no comprueba","DateKeys v"+SpecVersion+","}{
returnnil,fmt.Errorf("capsule: self-check: the reader finds the verdicts %s and %s in this security area, not %s and %s%s",v.Signature,v.Seal,wantSig,wantSeal,detailText(v.Detail))
!strings.HasSuffix(late.Lines()[1],", sin acreditar que fuera antes de la fecha de apertura: se selló después de esa fecha o demasiado cerca de ella.")||
// 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.
forname,tc:=rangemap[string]struct{
ocmstest.TokenOptions
whentime.Time
sealcapsule.Verdict
reasoncapsule.SealReason
}{
"no accuracy, years before":{cmstest.TokenOptions{},signedAt,capsule.VerdictSealedLate,capsule.ReasonNoAccuracy},
"no accuracy under BTSP":{cmstest.TokenOptions{Policy:cmstest.BTSPPolicy},signedAt,capsule.VerdictSealedLate,capsule.ReasonNoAccuracyBTSP},
"no accuracy, after the round time":{cmstest.TokenOptions{Policy:cmstest.BTSPPolicy},roundTime,capsule.VerdictSealedLate,capsule.ReasonLate},
"an accuracy of 0 seconds":{cmstest.TokenOptions{AccuracyRaw:cmstest.Seq(cmstest.Int(0))},roundTime.Add(-time.Microsecond),capsule.VerdictSealed,capsule.ReasonNone},
"BTSP with accuracy":{cmstest.TokenOptions{Policy:cmstest.BTSPPolicy,Accuracy:time.Second},signedAt,capsule.VerdictSealed,capsule.ReasonNone},
"an accuracy of 0 at the round time":{cmstest.TokenOptions{AccuracyRaw:cmstest.Seq(cmstest.Int(0))},roundTime,capsule.VerdictSealedLate,capsule.ReasonLate},
words:=fs.String("words","","time_and_key: at least 6 words that open the capsule; they stay in the shell history")
wordsFile:=fs.String("words-file","","time_and_key: file with the words that open the capsule")
newWords:=fs.String("new-words","","time_and_key: new file with words drawn at random from a list, that open the capsule")
dice:=fs.String("dice","","time_and_key: numbers of five dice, one for each word of the list -dic; they stay in the shell history")
diceFile:=fs.String("dice-file","","time_and_key: file with the numbers of five dice of -dice")
dic:=fs.String("dic","en","list of -new-words and -dice: "+strings.Join(wordkey.Languages(),", "))
wordCount:=fs.Int("word-count",wordkey.DefaultCount,"number of words of -new-words, 6 or more")
note:=fs.String("note","","public note of the capsule: one line that anyone with the .dkc reads before the date, and that can identify someone with it")
sign:=fs.String("sign","","file with the author key that signs the capsule")
signPass:=fs.String("sign-pass-file","","file with the passphrase of the author key, or - for the standard input")
noRecovery:=fs.Bool("no-recovery",false,"do not write the recovery annex of the specification next to the .dkc (spec §79)")
largeArea:=fs.Bool("large-area",false,"let the security area grow to 64 KiB if a signature does not fit in 32 KiB (an author key always fits)")
returnfmt.Errorf("the capsule was written to %s but its recovery annex could not be: %w",*out,err)
}
}
fmt.Fprintf(stderr,"Encrypted locally for %s (round %d)\n datekey %s\n capsule_id %x\n format %d: %d files, a payload of %d bytes, padded to %d (%s)\n",
fmt.Fprintf(stderr," words %s: the %d words of the dice in the list %s, %d bits; keep these words, which open the capsule: the numbers give them only with this list\n",
{[]string{"-policy","time_and_key","-new-words",filepath.Join(dir,"n.txt"),"-dic","xx"},`no word list for "xx"`},
{[]string{"-policy","time_and_key","-new-words",filepath.Join(dir,"n.txt"),"-word-count","5"},"at least 6 words, not 5"},
{[]string{"-new-words",filepath.Join(dir,"n.txt")},"-words, -words-file, -new-words and -dice need -policy time_and_key"},
{[]string{"-dice","11111 11112 11113 11114 11115 11116"},"-words, -words-file, -new-words and -dice need -policy time_and_key"},
{[]string{"-policy","time_and_key","-dice","11111","-dice-file",words},"-dice and -dice-file are exclusive"},
{[]string{"-policy","time_and_key","-dice","11111","-words","a"},"-dice and -dice-file exclude -words, -words-file and -new-words"},
{[]string{"-policy","time_and_key","-dice-file",words,"-new-words",filepath.Join(dir,"n.txt")},"-dice and -dice-file exclude -words, -words-file and -new-words"},
{[]string{"-policy","time_and_key","-dice","11111 11112 11113 11114 11115"},"at least 6 words, not 5"},
{[]string{"-policy","time_and_key","-dice","11111 11112 11113 11114 11115 11171"},`"11171" is not five dice: five digits from 1 to 6`},
{[]string{"-policy","time_and_key","-dice","11111 11112 11113 11114 11115 11111"},`the dice 11111 give "abacus" a second time; roll them again`},
{[]string{"-policy","time_and_key","-dice","11111 11112 11113 11114 11115 11116","-dic","xx"},`no word list for "xx"`},
iferr!=nil||!strings.Contains(stderr,"(spec §53)")||!strings.Contains(stderr,"note: for a horizon this long, or a valuable content, -policy time_and_key adds a credential"){
t.Errorf("a long horizon, time_only: %v\n%s",err,stderr)
| 4 | Security goals, among them goal 8, the privacy of metadata in format 2 | whole module | whole suite; goal 8: the tests of rows 29.1, 39 and 55.2 |
| 7 | Threat model | creator model in `internal/testkit.Build`, `RewriteAge`; third-party edits in the mutation corpus | `agewrap.TestTimeIdentityStrictness`, `TestPayloadIdentityStrictness`, `TestAccessIdentityStrictness`, `capsule.TestMutationCorpus` |
| 7 | Threat model; §7.6, SHOULD: recommend `time_and_key` for long horizons or valuable content | creator model in `internal/testkit.Build`, `RewriteAge`; third-party edits in the mutation corpus; `cmd/datekeys` recommends `-policy time_and_key` beyond one year with `time_only` (`TestRecoveryAndNotices`) | `agewrap.TestTimeIdentityStrictness`, `TestPayloadIdentityStrictness`, `TestAccessIdentityStrictness`, `capsule.TestMutationCorpus` |
| 11 | Canonical profile encoding, `profile_hash`; `period` in 1..2^53−1, `genesis_time` in 0..2^53−1 | `Profile.CanonicalCBOR`, `Profile.Hash`, `profile.Decode` (hand-written `wire` encode and decode: keys 0 to 10, all required, in order; unsigned `genesis_time`, `codec.MaxSafeUint`; `period` limited to 1..86400 s, an implementation limit marked in `spec/datekeys.cddl`, `ERR_NON_CANONICAL_CBOR`) | `profile.TestQuicknetMatchesGoldenVector`, `TestQuicknetCBORLayout`, `TestDecodeRoundTrip`, `TestDecodeStructure`, `TestIntegerRanges`, `FuzzDecode`; `testdata/vectors/profile_quicknet.json`; the `provider_profile` block of `testdata/vectors/cbor.json` (*period of one day, the implementation limit*, *… above the implementation limit*) |
@ -51,11 +52,11 @@ v0.14, and §47.1 and §79, new in v0.15. A case of §64 that is not in the repo
| 29.5 | Paths: R1 and R8 in layer 3; R2 to R6c, R4b and R10 for each entry, then R7 and R9 over the tree, in layer 4; the key of R7; errors that name the rule and the character, never the text | `internal/pathrule` (`CheckPath`, `CheckTree`, `Key`, `NFD`, `Fold`, `Error`) | `pathrule.TestCheckPath`, `TestCheckTree`, `TestKey`, `TestNFD`; `testdata/vectors/paths.json` and `path_fold.json` (`internal/testkit.PathVectors`, `PathFoldVectors`, `TestFormat3VectorFiles`); the path mutations of §64 |
| 29.5.1 | Tables: Unicode 18.0.0 and the 15 WindowsBestFit tables, pinned by their SHA-256, never the Unicode functions of the platform | `internal/pathrule/gen`, which checks the 19 pinned files in `.cache` and writes `tables.go`, and with `-ts` and `-dart` the same tables for `datekeys-ts` and `datekeys-dart`; `pathrule.UnicodeVersion`, `TablesDigest` | `pathrule.TestTablesDigest`, `TestProperties`; NFD and folding compared with `golang.org/x/text` outside this module |
| 29.6 | Text of the comment and of the declared author: no control but TAB and LF in the comment, no bidirectional control, separator, byte order mark or noncharacter, the invisibles rule with R4b for each line, no space at the ends of the author; the writer turns CR LF and a lone CR into LF | `pathrule.CheckComment`, `CheckAuthor`; `capsule.EncryptFiles` (`newHead`) | `pathrule.TestTexts`; `testdata/vectors/head_schema.json`; `capsule.TestEncryptFilesRoundTrip`, `TestEncryptFilesRejects` |
| 29.7 | Verdicts X, F0 to F6 and S0 to S5, for each part the first row of the table that holds, with the texts of the table; the name of a certificate between « and », shown when it meets the rules of the declared author, has at most 64 code points and no two spaces in a row, and its SHA-256 otherwise; the holder by `givenName` and `surname` before `commonName` when both have text that is not empty, the issuer by `commonName` or, without one that has text, `organizationName`; after F6, a line for each required signer with the authority of its seal, and «DateKeys no comprueba quién emitió los sellos.» when one says before the date; a foreign signer apart, with its result in Spanish; before the date only by a valid seal with t + accuracy <round_time,withitswarning; anmtimelaterthanavalidsealshownasaninconsistency(SHOULD);thepresentation:theverdictsfirstandlast,thelabelsofthetextofthecreator,TABsexpanded,piecesofatmostW−3columnsbehind`│ `withthewidthcountedbyexcess,thelinesoftheverdictsinrowsofatmostW−3columns,brokenatthelastspacethatfits,eachrowafterthefirstbehind` ↳ `,andwarningsofriskynames|`capsule.Verdict`,`Verdict.Text`,`Verdicts.Lines`,`Verdicts.SealedAt`,`Detail`,`SignerLine` (`quoted`,`instant`,`resultText`),`holderText`,`MaxNameLen`;`internal/cms``Cert.Holder`,`Cert.IssuerName`;`capsule.Open`step17.6(`newSecurityContext`,`openBody`),`OpenOptions.AuthorKeys`(F3),`OpenOptions.Accept`(theverdictsbeforestep18);`cmd/datekeys.present`(`writeVerdicts`,`rows`,`contMark`,`writeCreator`,`pieces`,`expandTabs`,`outputWidth`,`termWidth`,`risks`)|`capsule.TestSecurityVerdicts`,`TestEvaluateSecurityIn`(thelinesofF3andF4),`TestEvaluateCMS`(thelinesofF6withtheauthorityofeachsealandthewarning,alatesealwithoutit,aforeignsigner),`TestEvaluateSeal`(thelineofS4),`TestIssuerTextFiltered`,`TestCMSVectors`;thelinesoftherecordsofthefixtures(`capsule.TestConformanceFixtures`)andof`testdata/vectors/security.json`(`internal/testkit.TestFormat3VectorFiles`);`cmd/datekeys.TestRows`,`TestPresent`,`TestMTimeAfterSeal`,`TestAuthorSignRoundTrip`,`TestEncryptDecryptRoundTrip`,`TestDecryptFormat3Fixtures`;thenamesof§64ofv0.12(twospaces,morethan64codepoints,ESC,U+202E,abyteordermark,`givenName`and`surname`withaNIFin`commonName`,anissuerwithouttext):`security_cms.json`|
| 29.7 | Verdicts X, F0 to F6 and S0 to S5, for each part the first row of the table that holds, with the texts of the table; the name of a certificate between « and », shown when it meets the rules of the declared author, has at most 64 code points and no two spaces in a row, and its SHA-256 otherwise; the holder by `givenName` and `surname` before `commonName` when both have text that is not empty, the issuer by `commonName` or, without one that has text, `organizationName`; after F6, a line for each required signer with the authority of its seal, and «DateKeys no comprueba quién emitió los sellos.» when one says before the date; a foreign signer apart, with its result in Spanish; before the date only by a valid seal that carries accuracy, with t + accuracy <round_time,withitswarning, andS5otherwisewithitsreason,whichthelineofasignerofF6givestoo(v0.16); anmtimelaterthanavalidsealshownasaninconsistency(SHOULD);thepresentation:theverdictsfirstandlast,thelabelsofthetextofthecreator,TABsexpanded,piecesofatmostW−3columnsbehind`│ `withthewidthcountedbyexcess,thelinesoftheverdictsinrowsofatmostW−3columns,brokenatthelastspacethatfits,eachrowafterthefirstbehind` ↳ `,andwarningsofriskynames|`capsule.Verdict`,`Verdict.Text`,`Verdicts.Lines`,`Verdicts.SealedAt`,`Detail`,`SignerLine`,`SealReason` (`quoted`,`instant`,`resultText`),`holderText`,`MaxNameLen`;`internal/cms``Cert.Holder`,`Cert.IssuerName`;`capsule.Open`step17.6(`newSecurityContext`,`openBody`),`OpenOptions.AuthorKeys`(F3),`OpenOptions.Accept`(theverdictsbeforestep18);`cmd/datekeys.present`(`writeVerdicts`,`rows`,`contMark`,`writeCreator`,`pieces`,`expandTabs`,`outputWidth`,`termWidth`,`risks`)|`capsule.TestSecurityVerdicts`,`TestEvaluateSecurityIn`(thelinesofF3andF4),`TestEvaluateCMS`(thelinesofF6withtheauthorityofeachsealandthewarning,alatesealwithoutit,aforeignsigner),`TestEvaluateSeal`(thelineofS4,andS5witheachreason),`TestIssuerTextFiltered`,`TestCMSVectors`;thelinesoftherecordsofthefixtures(`capsule.TestConformanceFixtures`)andof`testdata/vectors/security.json`(`internal/testkit.TestFormat3VectorFiles`);`cmd/datekeys.TestRows`,`TestPresent`,`TestMTimeAfterSeal`,`TestAuthorSignRoundTrip`,`TestEncryptDecryptRoundTrip`,`TestDecryptFormat3Fixtures`;thenamesof§64ofv0.12(twospaces,morethan64codepoints,ESC,U+202E,abyteordermark,`givenName`and`surname`withaNIFin`commonName`,anissuerwithouttext):`security_cms.json`|
| 29.8 | Author signature, what is signed: `payload_commit`; `CONTROL_SIG`, the control with `payload_commit` in place of `I_PAYLOAD` and L at zero; `control_commit`, `head_digest`, `signers_digest`, and `AUTHOR_MESSAGE`, ASCII of 99 bytes, with its code of 8 characters; never the area or the padding, so the area can grow after signing; every value recomputed from the opened capsule | `capsule.PayloadCommit`, `ControlCommit`, `HeadDigest`, `SignersDigest`, `AuthorMessage`, `AuthorMessagePrefix`, `AuthorMessageSize`, `AuthorCode`; `capsule.Open` step 17.6 (`newSecurityContext`); the writer signs once the control is final, in the `prepare` that `EncryptFiles` gives `sealer.write` (`sealer.security`) | `capsule.TestAuthorMessage` (99 bytes, the code, `control_commit` without L and with `I_PAYLOAD`), `TestSignedFixtureVerdicts` (another control or head: F2), `TestAreaChosenAfterSigning` (signed once); `TestConformanceFixtures` (`checkSignature3`: the commitments, `AUTHOR_MESSAGE` and its code in the records of `format3_signed`, `format3_signed_cms` and `format3_sealed`); mutations *the signature of alg 1 transplanted to another capsule …*, *the area widened to 64 KiB after signing …* |
| 29.9 | Signature with a key of one's own, `alg` 1: Ed25519 of `AUTHOR_MESSAGE`; a key or a signature of another length is F1; valid only with A canonical and not of small order, `sig[63] & 0xE0` = 0, S <ℓ and the equation without the cofactor, F2 otherwise; F4, or F3 with a key the person saved | `internal/ed25519strict` (`Verify`, `Canonical`, `SmallOrder`, `SmallOrderPoints`, `OnCurve`), around `crypto/ed25519`; `capsule.evaluateSignature`; `EncryptOptions.AuthorKey`, the interface `capsule.AuthorKey` | `ed25519strict.TestVectors`, `TestVerifyRejectsWhatStdlibAccepts`, `TestCanonical`, `TestSmallOrderTable`, `TestOnCurve`; `testdata/vectors/ed25519_strict.json`, the cases of «Taming the many EdDSAs» (`internal/testkit.Ed25519StrictVectors`); `capsule.TestEvaluateSecurityIn`, `TestEncryptFilesSigned`, `TestEncryptFilesSignatureChecked`, `TestSignedFixtureVerdicts`; fixture `format3_signed`, whose signature `TestConformanceFixtures` makes again from its seed; mutations *a signature of alg 1 that does not verify …* and those of `alg` 1 of the list of v0.11: altered, removed, made again with another key, transplanted, a key of 31 bytes and a signature of 65 |
| 29.10 | Signature with certificates, `alg` 2: a detached CMS signature with the CAdES profile; `SIGNERS`, 1 to 16 SHA-256 of certificates in strictly ascending order; the form in its order (F1), with the version of a `SignerInfo` by its `sid`, attributes counted by attribute, a `signing-certificate` beside the v2 that decides nothing, an `ESSCertIDv2` with SHA-256 written, the parameters of PSS, object identifiers compared by the bytes of their DER and a SET OF that repeats an element; the closed table of algorithms; the profile of the certificate field by field, its key RSA with NULL parameters and an odd modulus, or EC uncompressed on P-256, P-384 or P-521; the result of each required signer in its order: absent, not verifiable, invalid (a key of another scheme than the algorithm included), without seal, invalid seal, out of validity, valid; F2, F5 or F6; no key 3; never who issued a certificate or whether it was revoked | `internal/der` (`Check`, `Split`, `Content`, `SetOfSorted`, `ParseTime`); `internal/cms` (`ParseSignature`, `SignedData`, `SignerInfo`, `SignerInfo.Check`, `ParseCert`, `Cert`, `Cert.ValidAt`, `ErrForm`, `ErrAlgorithm`), with the standard library only; `capsule.EncodeSigners`, `decodeSigners`, `MaxSigners`, `evaluateCMS`, `signerLine`; `EncryptOptions.CMSSigner`, the interface `capsule.CMSSigner`; `internal/cms/cmstest`, which makes certificates, signatures and tokens for the tests | `cms.TestSignatureAlgorithms` (RSA PKCS #1 v1.5 and PSS, SHA-256 to SHA-512, ECDSA on P-256, P-384 and P-521, a `sid` by `subjectKeyIdentifier`), `TestCoSignature`, `TestSignatureNotVerifiable`, `TestSignatureForm`, `TestSignatureStrictness`; `der.TestCheck`, `TestSetOfSorted`, `TestDepth`, `TestParseTime`, `TestSplit`; `capsule.TestEvaluateCMS` (a required signer absent, without seal, a key 3 beside it, another head, `SIGNERS` out of order or empty), `TestEncodeSigners`, `TestEncryptFilesCMSAndSeal`, `TestCMSVectors` (`testdata/vectors/security_cms.json`); fixture `format3_signed_cms`; the cases of `alg` 2 of §64 of v0.12 (a certificate out of validity with a valid TSA, the profile of the certificate, identifiers, repetitions, keys of another scheme or compressed): `security_cms.json` |
| 29.11 | Time seal, RFC 3161: the CAdES-T of each signer of `alg` 2, over its signature value, or `seal_type` 2 in key 3, over `SEAL_SUBJECT`, without a signature or with `alg` 1; `SIG_PART`; the profile of the token in its order: the form (S2), with the `TSTInfo` in DER field by field, `genTime` in UTC with Z, `accuracy` of 0 to 2³¹ − 1 seconds and minimal millis and micros, `ordering` only TRUE, nothing after the last field, and `crls` deciding nothing; the algorithms (S1); the verification (S3), a `messageImprint` of another length included; S4 when t + accuracy <round_time,S5otherwise|`internal/cms`(`ParseToken`,`Token`,`Token.Check`,`Token.ImprintIsSHA256`,`parseTSTInfo`,`parseAccuracy`);`capsule.SigPart`,`SealSubject`,`SealTypeRFC3161`,`evaluateSeal`,`signerLine`;`EncryptOptions.Sealer`,theinterface`capsule.Sealer`|`cms.TestTokenOverSignature`,`TestTokenFailures`,`TestTSTInfoStrict`(anegativeaccuracy,millisof0,a`genTime`withanoffsetoratrailingzero,`ordering`FALSEwritten,afieldafterthelast);`capsule.TestEvaluateSeal`,`TestEncryptFilesCMSAndSeal`,`TestCMSVectors`;fixture`format3_sealed`;thecasesof`seal_type`2of§64ofv0.12:`security_cms.json`|
| 29.11 | Time seal, RFC 3161: the CAdES-T of each signer of `alg` 2, over its signature value, or `seal_type` 2 in key 3, over `SEAL_SUBJECT`, without a signature or with `alg` 1; `SIG_PART`; the profile of the token in its order: the form (S2), with the `TSTInfo` in DER field by field, `genTime` in UTC with Z, `accuracy` of 0 to 2³¹ − 1 seconds and minimal millis and micros, `ordering` only TRUE, nothing after the last field, and `crls` deciding nothing; the algorithms (S1); the verification (S3), a `messageImprint` of another length included; S4 when the token carries accuracy and t + accuracy <round_time,S5otherwise,withitsreason,thefirstthatholds:late,noaccuracyundertheBTSPpolicyofETSIEN319421,noaccuracy(v0.16)|`internal/cms`(`ParseToken`,`Token`,`Token.Check`,`Token.ImprintIsSHA256`,`Token.HasAccuracy`,`Token.Policy`,`Token.BTSP`,`parseTSTInfo`,`parseAccuracy`);`capsule.SigPart`,`SealSubject`,`SealTypeRFC3161`,`evaluateSeal`,`signerLine`,`sealReason`;`EncryptOptions.Sealer`,theinterface`capsule.Sealer`|`cms.TestTokenOverSignature`,`TestTokenFailures`,`TestTSTInfoStrict`(anegativeaccuracy,millisof0,a`genTime`withanoffsetoratrailingzero,`ordering`FALSEwritten,afieldafterthelast);`capsule.TestEvaluateSeal`,`TestEncryptFilesCMSAndSeal`,`TestCMSVectors`;fixture`format3_sealed`;thecasesof`seal_type`2of§64ofv0.12:`security_cms.json`|
| 29.12 | Author keys of `alg` 1: a seed of Ed25519; the public key `dkauthor1…`, 67 characters in lower case, and the secret key `DKAUTHOR-SECRET-KEY-1…`, 79 in upper case, refused in another case, length or prefix, or with padding bits that are not zero; a public key canonical, a point of the curve and not of small order; a file of a secret key encrypted with age and a passphrase, scrypt of logN 16; a key that the person saved gives F3 with her label | `authorkey` (`Generate`, `NewFromSeed`, `Key.Public`, `Key.Sign`, `Key.Secret`, `Key.String` and `Key.GoString`, which hide the secret key, `Key.Clear`, `PublicString`, `ParsePublic`, `ParseSecret`, `Marshal`, `Encrypt`, `Read`, `WorkFactor`); `capsule.OpenOptions.AuthorKeys`; `cmd/datekeys`: `author keygen` and `author public` (`authorKeygen`, `authorPublic`, `loadAuthorKey`, `readPass`: the passphrase from a file or the standard input), `encrypt -sign` (`announced`), `decrypt -expect-author` | `authorkey.TestStrings` (a printed key never shows its secret), `TestParseRejects` (y = 2, off the curve), `TestFiles`; `ed25519strict.TestOnCurve`; `capsule.TestEncryptFilesSigned` (F3 with the key saved); `cmd/datekeys.TestAuthorSignRoundTrip` |
| 30 | PAYLOAD_AGE is a complete age file | `filippo.io/age` public API only | `capsule.TestInteropAgeOpensPayload` (`-tags interop`, official `age` CLI) |
| 30.1 | CONTROL_CBOR ↔ PAYLOAD_AGE binding; in format 2, L and the code fix the length and the padding of the plaintext, which adds determinism, not authenticity | `agewrap.PayloadIdentity` | mutations *SEALED_CONTROL_A + PAYLOAD_AGE_B*, *padding code 2 changed to 1, with L = 78000*, *payload_length L - 1, the last byte of the content not zero*; `capsule.TestTrustModel` (another L of the same P); `agewrap.TestPayloadIdentityStrictness` |
@ -68,7 +69,7 @@ v0.14, and §47.1 and §79, new in v0.15. A case of §64 that is not in the repo
| 36.1 | Authenticity semantics | documented in `README.md`, `SECURITY.md` | — (a property the protocol does not provide) |
| 37 | X25519 recipient V1; the writer rejects a recipient that is not canonical (bit 255 set, or u ≥ p) or of low order, and MAY reject a point of the twist | `age.X25519Recipient`; `agewrap.X25519IdentityFromRaw`, `agewrap.CheckX25519Recipient` (run by `capsule.Encrypt`); the twist check is not implemented | `agewrap.TestRawKeys`, `TestNonCanonicalRecipients`; `capsule.TestEncryptRejectsInvalidOptions` |
| 38 | Portable Access Key | `EncryptOptions.NewPortableKey` (fresh `I_ACCESS` per capsule; no API accepts an existing one); `accesskey.AccessKey` | `capsule.TestPortableKeysAreNeverReused` |
| 38.1 | Key of words: one more X25519 credential of `time_and_key`, among the 16; the normalization: NFD with the tables of Unicode 18.0.0, without U+0300 to U+036F, the simple lower case of each code point, split by the spaces of the list; PBKDF2-HMAC-SHA256 of 600 000 rounds, salted with the chain hash, the round and `capsule_id`, into a raw X25519 identity; the writer requires at least 6 words, counting only different words of 3 or more letters, and refuses controls, ignorables and unassigned code points; a reader may ask for the words instead of a `.dkk` | `wordkey` (`Normalize`, `Check`, `Key`, `Identity`, `Rounds`, `MinWords`, `MinLetters`), with `pathrule.NFD`, `Lower`, `DefaultIgnorable` and `Assigned`; `capsule.EncryptOptions.Words` (`accessRecipients`; `sealer.write` derives the identity once `capsule_id` is drawn); `cmd/datekeys`: `-words` and `-words-file` of `encrypt` and `decrypt` (`wordsText`), the words of `decrypt` salted with what `capsule.Inspect` gives | `wordkey.TestNormalize`, `TestCheck`, `TestKeyVector` (the vector of §38.1); `testdata/vectors/wordkey.json` (`internal/testkit.WordKeyVectors`, `TestVectorFilesAreCurrent`): the words of a text, what a writer refuses and the identities, the cases of §64 of v0.11; `capsule.TestEncryptFilesWords` (the words of another `capsule_id` do not open); `cmd/datekeys.TestKeyOfWords` |
| 38.1 | Key of words: one more X25519 credential of `time_and_key`, among the 16; the normalization: NFD with the tables of Unicode 18.0.0, without U+0300 to U+036F, the simple lower case of each code point, split by the spaces of the list; PBKDF2-HMAC-SHA256 of 600 000 rounds, salted with the chain hash, the round and `capsule_id`, into a raw X25519 identity; the writer requires at least 6 words, counting only different words of 3 or more letters, and refuses controls, ignorables and unassigned code points; a reader may ask for the words instead of a `.dkk`; SHOULD: random words of a public list by default, at least 6 of 2048 or more | `wordkey` (`Normalize`, `Check`, `Key`, `Identity`, `Rounds`, `MinWords`, `MinLetters`; `Generate`, `List`, `CheckList` with the alphabet of each language, `Bits`, `DefaultCount`, `MinListSize`, the lists `lists/en.txt` and `lists/es.txt`; the dice: `DiceListSize`, `DiceNumber`, `DiceWord`, `DiceWords`, `DiceList`), with `pathrule.NFD`, `Lower`, `DefaultIgnorable` and `Assigned`; `capsule.EncryptOptions.Words` (`accessRecipients`; `sealer.write` derives the identity once `capsule_id` is drawn); `cmd/datekeys`: `-words` and `-words-file` of `encrypt` and `decrypt` (`wordsText`), `-new-words`, `-dic`, `-word-count`, `-dice` and `-dice-file` of `encrypt`, the command `wordlist`, the words of `decrypt` salted with what `capsule.Inspect` gives | `wordkey.TestNormalize`, `TestCheck`, `TestKeyVector` (the vector of §38.1), `TestBuiltInLists`, `TestCheckList`, `TestGenerate`, `TestBits`, `TestGenerateUniform`, `TestDiceNumber`, `TestDiceWord`, `TestDiceWords`, `TestDiceList`; `testdata/vectors/wordkey.json` (`internal/testkit.WordKeyVectors`, `TestVectorFilesAreCurrent`): the words of a text, what a writer refuses and the identities, the cases of §64 of v0.11; `capsule.TestEncryptFilesWords` (the words of another `capsule_id` do not open); `cmd/datekeys.TestKeyOfWords`, `TestNewWords`, `TestDice`, `TestWordList` |
| 39 | Recipients of INNER_ACCESS_AGE: in formats 2 and 3 from 1 to 16 credentials, a dummy in each slot left (a fresh public key whose private key is dropped at once), the 16 in a uniformly random order; which slots are dummies is recorded only in the official vectors | `capsule/encrypt.go` (`accessRecipients`, `fillSlots`, `permute`); `agewrap.AccessIdentity` | `capsule.TestInnerHasSixteenStanzas`, `TestDummyRecipients`, `TestStanzaOrderIsUniform`, `TestCredentialBounds`, `TestFixtureRecipients`, `TestEncryptRoundTripBothPolicies`; `TestConformanceFixtures` (the stanza each credential opens, `access_key_stanza` and `identity_stanzas` in the records) |
| 40 | `.dkk` framing; `BODY_LEN` in 1..16 MiB (0 is `ERR_INTEGRITY`); order of the frame checks | `accesskey.Encode`, `accesskey.Decode` (the body buffer grows with the data read; every buffer holding the body is wiped) | `accesskey.TestDecodeRejects`, `TestDecodePrecedence`, `TestDecodeShortBodyAllocatesLittle`, `TestEncodeAndDecodeLeaveNoStaleMaterial`, `FuzzDecode` |
@ -79,13 +80,13 @@ v0.14, and §47.1 and §79, new in v0.15. A case of §64 that is not in the repo
| 45 | Release API: its answer is the release object of §47.1 (v0.15), and its user a network source; the HTTP form is informative | the object: `provider.EncodeRelease`, `provider.DecodeRelease`; the API itself is out of scope (server, plan §2) | `provider.TestReleaseVectors` |
| 46 | Release Queue | out of scope (server) | — |
| 47 | Release Cache | every release is verified again: `capsule.Open` step 10 and `agewrap.TimeIdentity`; `release_material` is the release object (v0.15): `provider.EncodeRelease` | mutations *release of another round* |
| 47.1 | Release object (v0.15): deterministic CBOR without a frame, `{0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}`; size from 1 to 1024 bytes, then type and version, then schema (`ERR_NON_CANONICAL_CBOR`, a version other than 1 `ERR_UNSUPPORTED_VERSION`); the chain hash, the round and the signature at step 10; drand's JSON accepted as the input of the caller, `ERR_RELEASE_INVALID` when unreadable; a Release Cache and the Release API keep and serve the object | `provider/release.go`: `EncodeRelease`, `DecodeRelease` (`releaseWire` with `codec.CheckSchema` and `codec.Unmarshal`), `ParseRelease`, `NewReleaseObject`, `MaxReleaseObjectSize`, `MaxReleaseJSONSize`; `cmd/datekeys` `releaseInHand` | `provider.TestReleaseVectors`, `TestEncodeRelease`, `FuzzDecodeRelease`; `testdata/vectors/release.json`, `testdata/releases/<round>.cbor`; `internal/testkit.TestVectorFilesAreCurrent` |
| 47.1 | Release object (v0.15): deterministic CBOR without a frame, `{0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}`; size from 1 to 1024 bytes, then type and version, then schema (`ERR_NON_CANONICAL_CBOR`, a version other than 1 `ERR_UNSUPPORTED_VERSION`); the chain hash, the round and the signature at step 10; drand's JSON accepted as the input of the caller, `ERR_RELEASE_INVALID` when unreadable, and since v0.16 read strictly: no repeated name, names compared exactly once their escapes are decoded, no lone surrogate, round an integer from 1 to 2^53 − 1; a Release Cache and the Release API keep and serve the object | `provider/release.go`: `EncodeRelease`, `DecodeRelease` (`releaseWire` with `codec.CheckSchema` and `codec.Unmarshal`), `ParseRelease`, `ParseDrandJSON`, `NewReleaseObject`, `MaxReleaseObjectSize`, `MaxReleaseJSONSize`; `provider/drandjson.go` (`strictJSON`, `jsonRound`), which `provider/drand` uses for the answers of the relays too; `cmd/datekeys` `releaseInHand` | `provider.TestReleaseVectors`, `TestEncodeRelease`, `FuzzDecodeRelease`, `TestStrictJSON`, `TestJSONRound`; `testdata/vectors/release.json`, `testdata/releases/<round>.cbor`; `internal/testkit.TestVectorFilesAreCurrent` |
| 48 | Multi-relay; a network source verifies every response with the rules of §63 step 10 and discards the invalid ones: none valid is `ERR_RELEASE_UNAVAILABLE` at step 9, and so is any other failure of a source, with no other code | `provider/drand.Client` (race, first *verified* release wins; the failure of each relay kept as text only, a context that ended detectable with `errors.Is`), the `provider.ReleaseSource` contract, `capsule.Open` (step 9 keeps only the text of a source error with another code or none) | `drand.TestRaceWaitsForAValidSignature`, `TestRejectMalformedRelayResponses`, `TestFetchErrorHasOneCode`, `TestUnavailabilityAndCancellation`; `capsule.TestReleaseFromANetworkSource`, `TestReleaseSourceErrorsAtStep9` |
| 49 | Direct recovery from the provider | `provider/drand`; v0.15: a release in hand, `provider.Supplier` (`provider.Encoded`, `provider.Archive`) in `capsule.OpenOptions.Release`; `datekeys decrypt -release` | `drand.TestLiveRelays`, `capsule.TestLiveLifecycle` (`-tags integration`); `capsule.TestReleaseInHand`, `TestReleaseInHandErrors`; `cmd/datekeys.TestDecryptWithReleaseInHand` |
| 50 | Historical release dependency; v0.15: long-term recovery on archives of all rounds and on cache services that serve them, with no hosting promise, the release archive as an informative format | documented in `README.md`; `provider.Archive`, `provider.EncodeArchiveHeader`; `datekeys decrypt -release` with a local archive | `provider.TestArchive`; `cmd/datekeys.TestDecryptWithReleaseInHand` (*a local archive*); the `archive` block of `testdata/vectors/release.json`, `testdata/releases/archive_1000_1004.bin` |
| 51 | Quicknet release verification; order and codes of §63 step 10: the round (`ERR_ROUND_MISMATCH`), then the signature, the canonical encoding of a point of G1 other than the identity (§12.2) that verifies as the BLS signature of the round (`ERR_RELEASE_INVALID`); those codes for a release supplied directly, a network source discarding an invalid one at step 9 (`ERR_RELEASE_UNAVAILABLE`) | `provider.Verify`; v0.15: first the chain hash a release object names, `ERR_PROFILE_MISMATCH` (`provider.Verify`) | `provider.TestVerifyPublishedReleases`, `TestVerifyRejects` (x + p, the point at infinity alone, with a payload or with the sort flag, no compression flag, the negated signature), `TestVerifyUsesThePinnedKeyOnly`; `capsule.TestReleaseFromANetworkSource`; mutations *DateKey A + release of round B*, *release of another round*, *release signature …*; the `objects` of `testdata/vectors/release.json`; mutations *release object of another chain* |
| 52 | DNS / MITM | `provider/drand` (no redirects, bounded responses, BLS) | `drand.TestRedirectsAreNotFollowed`, `TestRejectMalformedRelayResponses`, `TestRandomnessMustMatchWhenPresent` |
| 53 | Harvest now, decrypt later | `cmd/datekeys` warning beyond one year; v0.15: the release among what a long horizon needs (text) | `cmd/datekeys.TestLongHorizonWarning` |
| 53 | Harvest now, decrypt later | `cmd/datekeys` warning beyond one year; v0.15: the release among what a long horizon needs (text) | `cmd/datekeys.TestLongHorizonWarning`, `TestRecoveryAndNotices` |
| 54 | Extensions: data absent or a non-empty opaque byte string, never decoded; 1 to 64 per array; `extension_id` of at least 1 byte; `extension_version` ≤ 2^32−1; elements in strictly ascending unsigned bytewise order of the UTF-8 bytes of `extension_id` (a proper prefix first, never UTF-16 code units or a collation), so one `extension_id` per array, and none in both arrays; a known extension in an object or array it is not registered for is unknown there, its data never interpreted | `extension.New`, `Canonical`, `EncodeArray` (refuses, through `codec.Encoder.Fail`, an array that `DecodeArray` rejects), `DecodeArray` (64 entries checked on the array head, explicit key 2 check), `CheckDisjoint` (linear merge), `CheckCritical`, `CheckNoncritical`, `Unusable`; `Object`, `Array`, the optional `Placement` of a `Registry`, `KnownIn`, and `CheckCriticalIn` and `CheckNoncriticalIn`, which `capsule` runs at steps 4, 9.a and 14 | `extension.TestNew`, `TestData`, `TestCanonicalSorts`, `TestOrderIsUnsignedBytewise`, `TestCanonicalRejects`, `TestEncodeArrayRejects`, `TestDecodeArrayRejects`, `TestCheckDisjoint`, `TestCheckDisjointIsLinear`, `TestCheckCritical`, `TestCheckNoncritical`, `TestPlacement`, `FuzzDecodeArray`; `capsule.TestKnownCriticalExtensions`, `TestUnusableNoncriticalExtensions`, `TestExtensionPlacement`; mutations *unknown critical … extension*, *known critical … extension with invalid data*, *extension_version above 2^32-1*, *null extension data* |
| 55 | Auxiliary integrity | `capsule_digest` treated as UX only | — |
| 55.1 | Trust model: who writes each section, from which step and by what it is bound, what it never proves | no code of its own: `header_binding` (step 15), the age header MACs (steps 11, 13 and 17), `capsule_id` and `capsule_digest` (step 9.a) | `capsule.TestTrustModel` (a capsule forged from the public bytes of `time_only.dkc` or `format2_time_only.dkc` opens; a control that declares L + 1 over a zero padding byte opens to another content; edited PUBLIC_HEADER data passes steps 1 to 8 and fails step 15; other `.dkk` extension data opens the capsule) |
@ -98,22 +99,22 @@ v0.14, and §47.1 and §79, new in v0.15. A case of §64 that is not in the repo
| 61 | `time_only` encryption flow, format 3: the files measured, hashed, sealed and read again | `capsule.EncryptFiles` | `capsule.TestEncryptFilesRoundTrip`, `TestEncryptFilesLengths`, `TestEncryptFilesChangedFile`, `TestSealedControlLength`; the live test (`-tags integration`) |
| 62 | `time_and_key` encryption flow, format 3: 16 recipients, and the SEALED_CONTROL_LEN of an INNER_ACCESS_AGE of 16 stanzas | `capsule.EncryptFiles` | `capsule.TestEncryptFilesTimeAndKey`, `TestEncryptRoundTripBothPolicies`, `TestPortableKeysAreNeverReused`, `TestSealedControlLength` |
| 62.1 | Writer rules: format 3 only, formats 1 and 2 being written only by a generator of test vectors; an instant after the clock of the writer; from 1 to 16 credentials, none twice, canonical and not of low order; dummies and a random order; `capsule_id`, `I_PAYLOAD`, `I_ACCESS`, `credential_id`, dummies and order from a CSPRNG, `I_PAYLOAD` and dummies never reused or derived; L known before sealing, at most L_MAX, code 1 or 2; SEALED_CONTROL_LEN exact, measured by a provisional seal and checked; limits; on error, the output is discarded; in format 3, the area of 32768 bytes, signed or not, and 65536 only when the creator widens it once the signatures are made, without signing again for it, a capsule refused when they do not fit, `SECURITY_CBOR` always and empty without a signature or a seal, another area or `SECURITY_CBOR` only from a generator of test vectors (rule 13), a head with a fresh salt, a file or a comment, the order of R8, the layout and the SHA-256 of what is written, at most 16 MiB, paths and texts refused with the rule and the character, the mtime taken at load and omitted out of range, the three CBOR objects decoded with the rules of the reader (MUST), and files that must not change between the two readings; with a signature or a seal, each verified with the rules of the reader before writing, never one that gives F1, F2, F5, S1, S2 or S3 (rule 19), `AUTHOR_MESSAGE` given as text and its code shown before each signature, `SIGNERS` closed before the first (rule 20), a CAdES-T for each signer of `alg` 2 (rule 21), the seal of `seal_type` 2 over `SEAL_SUBJECT` after the signature (rule 22), and no secret on disk while waiting for them (rule 25); the public note only when asked for, with the rules of the declared author and a warning (rule 23); the rules of §38.1 for a key of words, and for a `.dkk` with a locator the rest stored before the `.dkk` is written (rule 24). SHOULD: code 2 by default, self-checks, wiping | `capsule.EncryptFiles` (`newHead`, `readSource`, `selfCheckHead`) and `capsule.Encrypt`, through their sealer (`EncryptOptions.Padding`, `accessRecipients`, `fillSlots`, `copyExactly`, `selfCheckHeader`, `selfCheckControl`, `selfCheckInner`, `selfCheckPayload`); `agewrap.CheckX25519Recipient`; `EncryptOptions.TestVectors` for `Encrypt` of format 2, and `internal/testkit.Build`, generators of test vectors (§70); in format 3, `capsule.AreaLen`, `LargeAreaLen` and `EncryptOptions.LargeArea`, the area decided by `EncryptFiles`, in the `prepare` that it gives `sealer.write`, once `sealer.security` has signed and sealed and checked both with `EvaluateSecurityIn` (rules 13, 19, 21 and 22), `EncryptOptions.AuthorKey`, `CMSSigner` and `Sealer`, a typed nil in one of them an error (`newSealer`, `isNil`), nothing written to `dst` before they return and the control and `I_PAYLOAD` kept in memory (rule 25); `EncryptOptions.TestAreaLen`, with `TestVectors`, the area of 512 bytes of the fixtures of v0.10 (rule 13); `EncryptOptions.PublicNote` and `extension.CheckWrite` (rule 23, §72); `EncryptOptions.Words` and `wordkey.Check`, and `locator.NewEnvelope`, `Locator.Marshal` and `Info.Extension` (rule 24); `cmd/datekeys`: `announced` (rule 20) and the warning of `-note` (rule 23); v0.15, rules 26 and 27 (SHOULD of the SDK): the warning and the annex next to the `.dkc` are not implemented by the CLI | `capsule.TestEncryptFilesRejects`, `TestEncryptFilesChangedFile`, `TestEncryptIsForTestVectors`, `TestEncryptFilesHeadCritical`, `TestEncryptRejectsInvalidOptions`, `TestCredentialBounds`, `TestEncryptSourceLength`, `TestEncryptSelfCheck`, `TestSealedControlLength`, `TestPayloadIdentityReuse`, `TestStanzaOrderIsUniform`, `TestDummyRecipients`, `TestPortableKeysAreNeverReused`; `cmd/datekeys.TestEncryptRefusesPaths`; rules 13 and 19 to 25: `capsule.TestEncryptFilesSigned`, `TestEncryptFilesSignatureChecked` (nothing written), `TestEncryptFilesCMSAndSeal` (a signature without a required signer, or without seals, is not written), `TestAreaChosenAfterSigning` (the area widened once signed, signing once; without a signature, 32 KiB with `LargeArea`), `TestWriterOptionsChecked`, `TestPublicNoteRules`, `TestRegisteredExtensionsWhereRegistered`, `TestEncryptFilesWords`; `wordkey.TestCheck`; `locator.TestUsableAddresses`, `TestInfo`; `cmd/datekeys.TestAuthorSignRoundTrip` (the key and the code before the signature), `TestPublicNoteCLI`, `TestKeyOfWords`; rule 25 has no test of its own |
| 62.1 | Writer rules: format 3 only, formats 1 and 2 being written only by a generator of test vectors; an instant after the clock of the writer; from 1 to 16 credentials, none twice, canonical and not of low order; dummies and a random order; `capsule_id`, `I_PAYLOAD`, `I_ACCESS`, `credential_id`, dummies and order from a CSPRNG, `I_PAYLOAD` and dummies never reused or derived; L known before sealing, at most L_MAX, code 1 or 2; SEALED_CONTROL_LEN exact, measured by a provisional seal and checked; limits; on error, the output is discarded; in format 3, the area of 32768 bytes, signed or not, and 65536 only when the creator widens it once the signatures are made, without signing again for it, a capsule refused when they do not fit, `SECURITY_CBOR` always and empty without a signature or a seal, another area or `SECURITY_CBOR` only from a generator of test vectors (rule 13), a head with a fresh salt, a file or a comment, the order of R8, the layout and the SHA-256 of what is written, at most 16 MiB, paths and texts refused with the rule and the character, the mtime taken at load and omitted out of range, the three CBOR objects decoded with the rules of the reader (MUST), and files that must not change between the two readings; with a signature or a seal, each verified with the rules of the reader before writing, never one that gives F1, F2, F5, S1, S2 or S3, and the verdicts of the area returned in `Result.Security`, so that a seal without accuracy is warned of (rule 19, v0.16), `AUTHOR_MESSAGE` given as text and its code shown before each signature, `SIGNERS` closed before the first (rule 20), a CAdES-T for each signer of `alg` 2 (rule 21; the chains without roots and the OCSP responses of v0.16 are what the `CMSSigner` returns: this module adds and removes no certificate, *pending* until a writer asks an authority), the seal of `seal_type` 2 over `SEAL_SUBJECT` after the signature (rule 22), and no secret on disk while waiting for them (rule 25); the public note only when asked for, with the rules of the declared author and a warning (rule 23); the rules of §38.1 for a key of words, and for a `.dkk` with a locator the rest stored before the `.dkk` is written (rule 24). SHOULD: code 2 by default, self-checks, wiping | `capsule.EncryptFiles` (`newHead`, `readSource`, `selfCheckHead`) and `capsule.Encrypt`, through their sealer (`EncryptOptions.Padding`, `accessRecipients`, `fillSlots`, `copyExactly`, `selfCheckHeader`, `selfCheckControl`, `selfCheckInner`, `selfCheckPayload`); `agewrap.CheckX25519Recipient`; `EncryptOptions.TestVectors` for `Encrypt` of format 2, and `internal/testkit.Build`, generators of test vectors (§70); in format 3, `capsule.AreaLen`, `LargeAreaLen` and `EncryptOptions.LargeArea`, the area decided by `EncryptFiles`, in the `prepare` that it gives `sealer.write`, once `sealer.security` has signed and sealed and checked both with `EvaluateSecurityIn` (rules 13, 19, 21 and 22), `EncryptOptions.AuthorKey`, `CMSSigner` and `Sealer`, a typed nil in one of them an error (`newSealer`, `isNil`), nothing written to `dst` before they return and the control and `I_PAYLOAD` kept in memory (rule 25); `EncryptOptions.TestAreaLen`, with `TestVectors`, the area of 512 bytes of the fixtures of v0.10 (rule 13); `EncryptOptions.PublicNote` and `extension.CheckWrite` (rule 23, §72); `EncryptOptions.Words` and `wordkey.Check`, and `locator.NewEnvelope`, `Locator.Marshal` and `Info.Extension` (rule 24); `cmd/datekeys`: `announced` (rule 20) and the warning of `-note` (rule 23); v0.15, rules 26 and 27 (SHOULD of the SDK): the warning and the annex next to the `.dkc` are not implemented by the CLI | `capsule.TestEncryptFilesRejects`, `TestEncryptFilesChangedFile`, `TestEncryptIsForTestVectors`, `TestEncryptFilesHeadCritical`, `TestEncryptRejectsInvalidOptions`, `TestCredentialBounds`, `TestEncryptSourceLength`, `TestEncryptSelfCheck`, `TestSealedControlLength`, `TestPayloadIdentityReuse`, `TestStanzaOrderIsUniform`, `TestDummyRecipients`, `TestPortableKeysAreNeverReused`; `cmd/datekeys.TestEncryptRefusesPaths`; rules 13 and 19 to 25: `capsule.TestEncryptFilesSigned`, `TestEncryptFilesSignatureChecked` (nothing written), `TestEncryptFilesCMSAndSeal` (a signature without a required signer, or without seals, is not written), `TestAreaChosenAfterSigning` (the area widened once signed, signing once; without a signature, 32 KiB with `LargeArea`), `TestWriterOptionsChecked`, `TestPublicNoteRules`, `TestRegisteredExtensionsWhereRegistered`, `TestEncryptFilesWords`; `wordkey.TestCheck`; `locator.TestUsableAddresses`, `TestInfo`; `cmd/datekeys.TestAuthorSignRoundTrip` (the key and the code before the signature), `TestPublicNoteCLI`, `TestKeyOfWords`; rule 25 has no test of its own |
| 63 | Decryption flow; step 2 accepts the formats 1, 2 and 3, and the steps after it apply the rules of the format: in formats 2 and 3, 16 stanzas at step 12, a control of schema version 2 at step 14, L, the code and P at step 16, a plaintext of P bytes with a zero padding at step 17 (`ERR_INTEGRITY` whenever it is found), the first L bytes at step 18; in format 3, step 17 in its substeps, a failure of age or a plaintext whose length is not P prevailing and a code other than `ERR_INTEGRITY` reported only after reading to EOF, and a caller without a `Sink` stopped right after step 2; steps 4 and 14 validate critical extensions (unknown, then invalid data); step 5 reads SEALED_CONTROL, a MUST (`ERR_INTEGRITY`), and SHOULD inspect its age header; step 8 argument rules; step 9 order: the `.dkk` as an object (decoded there when still encoded), its `capsule_id` and `capsule_digest`, credentials (nil identities are none) before the clock, round time, request, and nothing of the credentials under `time_only`; a network source verifies each response with the rules of step 10 and discards the invalid ones (none valid: `ERR_RELEASE_UNAVAILABLE`, step 9), and any failure of a source is `ERR_RELEASE_UNAVAILABLE` alone, whatever code its error carries; step 10: round, then signature, a canonical point other than the identity (§12.2), the codes of a release supplied directly; step 11: the tlock stanza body `U \|\| V \|\| W` of \|U\| + 32 bytes (128 in Quicknet), U canonical and not the identity, the IBE check r·G == U, every failure `ERR_INTEGRITY`, H2, H3 and H4 those of drand/kyber `encrypt/ibe`, H2 over the element of GT serialized in the order of kilic/bls12-381 (c1 before c0 at every level of the tower), with the frozen vector H2(e(G1, G2)) = `cb87319f24560b5231579a09ad79f12e`; the codes of the identities at steps 11, 13 (malformed X25519 stanza `ERR_INTEGRITY`, an identity that unwraps two stanzas `ERR_POLICY_STRUCTURE_MISMATCH` whatever the order, none `ERR_ACCESS_INVALID`) and 17; step 15 `ERR_HEADER_BINDING` | `capsule.Inspect` (steps 1–8), `capsule.Open` (steps 9–18; `openBody`, `drain`, `ErrSinkRequired`; `OpenOptions.AccessKeyFile`, `checkAccessKey`, `checkCapsuleDigest`), the `provider.ReleaseSource` contract, `provider/drand.Client` and `capsule.sourceFailure` (step 9), `tlock.TimeUnlock` with the kyber-bls12381 pairing (step 11), MUST rules inside `agewrap` identities (`AccessIdentity` tries every identity on every stanza; `TimeIdentity` checks the length of the tlock stanza body and U before `tlock.TimeUnlock`); no error copies the text of an error of age, tlock, kyber or drand (`agewrap`, `capsule.classify`), since kyber's IBE error carries the candidate plaintext and r; `cmd/datekeys` hands the `.dkk` over encoded; `datekeys inspect -json` rendered by `internal/inspectview`; v0.15: step 9.c only for `OpenOptions.Source`, a release in hand (`OpenOptions.Release`) not compared with the clock and `Opened.ClockBehind`; step 10 starts with `provider.ParseRelease`, then `provider.Verify` with the chain hash | `capsule.TestConformanceFixtures` (stage by stage), `TestOpen3`, `TestOpen3Substeps`, `TestFormatDispatch`, `TestFormatRelabel`, `TestPaddingChecksAtStep17`, `TestTlockFailureDiagnosticsCarryNoSecrets`, `TestPlaintextWriterFailureKeepsItsText`, `TestAccessKeyCheckOrder`, `TestAccessKeyFileAtStep9`, `TestPrecedenceAcrossSteps`, `TestControlCriticalBeforeHeaderBinding`, `TestReleaseFromANetworkSource`, `TestReleaseSourceErrorsAtStep9`, `agewrap.TestAccessIdentityStrictness`, `TestMalformedX25519Stanzas`, `TestTlockH2Vector` (`testdata/vectors/tlock_ibe.json`, generated by `internal/testkit.IBEVectors`, and step 11 recomputed with H2 and H4 against the file key tlock unwraps), `cmd/datekeys.TestDecryptAccessKeyOrder`, `TestMutationCorpus`, `TestInspectDifferentialCorpus` (`testdata/vectors/inspect_differential.json`: 5110 deterministic mutations of 14 fixtures, two of them of format 3, the 1825 of the format 1 ones first with the verdict of steps 1–8, generated by `internal/testkit.InspectDifferential`); `cmd/datekeys.TestInspectJSONGoldens` (`testdata/fixtures/*.inspect.json`); `capsule.TestReleaseInHand`, `TestReleaseInHandErrors`; mutations *round not reached yet* (opens) and *round not reached yet, from a network source* |
| 64 | Mandatory mutation tests: the first two lists in the three formats, the list of format 2 in format 2, and that of format 3, four of whose cases open with their verdicts, F2 for a signature of `alg` 1 that does not verify among them; the lists of v0.11 and v0.12: the signature of `alg` 1 and of `alg` 2, the area widened after signing, the seal, `alg` and `seal_type` 4294967295, the same P with a signature and without, the public note, the key of words and `datekeys.capsule` | `internal/testkit.Mutations` (the corpus: `specMutations` for each format, `furtherMutations`, `format2Mutations`, `format3Mutations` with `LoadedFixture.WithBody`, among them those of the list of v0.11 from `format3_signed`, `format3_unsigned` and `format3_note`, `signed1`), `internal/testkit.MutationCorpus` (its export); the cases of formats 2 and 3 derived without randomness, by sealing the fixtures again with their known file keys and nonces (`internal/testkit/reseal.go`, `mutations3.go`), exported as edits of their fixture (`internal/testkit.Splice`); the cases of `alg` 2, `seal_type` 2 and `datekeys.capsule`, vectors of `security_cms.json` and `locator.json`, frozen once written (`internal/testkit/genfixtures`, `frozenVectors`) | `capsule.TestMutationCorpus`: the 178 listed mutations, 33 in each format, the 23 of the list of format 2, the 48 of that of format 3 and 8 of that of v0.11 (`internal/testkit.SpecMutationsPerFormat`, `Format2SpecMutations`, `Format3SpecMutations`, `V011SpecMutations`), plus 44 more, built afresh; `capsule.TestExportedMutationCorpus`: `testdata/vectors/mutations.json`, the same 222 cases as frozen data (capsule, `.dkk`, identities, recorded release and its source, clock, registry, known extensions), replayed with the recorded error and step, or the recorded verdicts; `capsule.TestPointMutationsChangeOnlyTheEncoding`: the ten point mutations keep a valid header MAC, and a decoder that reduces coordinates modulo p opens the c0 + p and x + p cases; `internal/testkit.TestResealReproducesFixtures`, `TestFixedX25519Stanza`; the cases of the lists of v0.11 and v0.12 outside the corpus: `ed25519strict.TestVectors` (`ed25519_strict.json`), `capsule.TestCMSVectors` (`security_cms.json`), `locator.TestLocatorVectors` (`locator.json`), `wordkey.TestKeyVector`, `TestNormalize` and `TestCheck`, `testdata/vectors/note.json`, and the same P of the fixtures `format3_unsigned` and `format3_signed` (`capsule.TestConformanceFixtures`); those of `alg` 2, `seal_type` 2 and `datekeys.capsule` of the lists of v0.11 and v0.12, in `security_cms.json` and `locator.json` |
| 67 | `.dkc` vectors: the format 1 fixtures of v0.8.2, kept for compatibility, and format 2 fixtures for both policies, both codes, L = 0, one, several and 16 credentials, and extensions; the format 3 fixtures: one file, a tree, a comment alone, both codes, `time_and_key` with a portable key, an area of 1024 bytes, security of version 2, a signature of `alg` 4294967295 and that with a seal of `seal_type` 4294967295, the area of 32 KiB without a signature, a signature of `alg` 1, that with a seal of `seal_type` 2 and a file whose mtime is later than the seal, and a signature of `alg` 2 of two signers, ECDSA P-256 and RSA 2048, each with its CAdES-T; the records give the format, L, the code, P and the stanza each credential opens, and in format 3 the head, security, each file and the verdicts with their lines, and for a signature or a seal the commitments, `AUTHOR_MESSAGE` and its code, the key or `SIGNERS` and the certificates, the result of each signer, `SEAL_SUBJECT` and the token; the padding vectors, and those of paths, keys of R7, heads, security, `security_cms.json`, `ed25519_strict.json`, `note.json` and `locator.json` | `testdata/fixtures/*.dkc` + `*.json`, `internal/testkit/genfixtures`, which never regenerates a format 1 fixture, gives the five fixtures that `EncryptFiles` wrote in v0.10 their area of 512 bytes (`EncryptOptions.TestAreaLen`) and recomputes the derived fields of every record; `format3_note`, with a public note, for the mutations of §64; the frozen `datekeys inspect -json` output of each, `*.inspect.json`; `testdata/vectors/padding.json`; `security_cms.json` and `locator.json`, frozen once written (`frozenVectors`); formats in `testdata/README.md` | `capsule.TestConformanceFixtures` (`checkBody3`, `checkSignature3`), `TestPaddingAcrossChunks` (a capsule generated at run time), `TestCMSVectors`; `locator.TestLocatorVectors`; `ed25519strict.TestVectors`; `internal/testkit.TestFormat3VectorFiles`, `TestVectorFilesAreCurrent`; `cmd/datekeys.TestInspectJSONGoldens`, `TestDecryptFixtures`, `TestDecryptFormat3Fixtures`, `TestMTimeAfterSeal` |
| 67 | `.dkc` vectors: the format 1 fixtures of v0.8.2, kept for compatibility, and format 2 fixtures for both policies, both codes, L = 0, one, several and 16 credentials, and extensions; the format 3 fixtures: one file, a tree, a comment alone, both codes, `time_and_key` with a portable key, an area of 1024 bytes, security of version 2, a signature of `alg` 4294967295 and that with a seal of `seal_type` 4294967295, the area of 32 KiB without a signature, a signature of `alg` 1, that with a seal of `seal_type` 2 and a file whose mtime is later than the seal, and a signature of `alg` 2 of two signers, ECDSA P-256 and RSA 2048, each with its CAdES-T; and since v0.16 a key of words, `format3_time_and_key_words`, with the text in `words_text`, and a `PAYLOAD_AGE` that ends in a full chunk, `format3_full_chunk`; the records give the format, L, the code, P and the stanza each credential opens, and in format 3 the head, security, each file and the verdicts with their lines, and for a signature or a seal the commitments, `AUTHOR_MESSAGE` and its code, the key or `SIGNERS` and the certificates, the result of each signer, `SEAL_SUBJECT` and the token; the padding vectors, and those of paths, keys of R7, heads, security, `security_cms.json`, `ed25519_strict.json`, `note.json` and `locator.json` | `testdata/fixtures/*.dkc` + `*.json`, `internal/testkit/genfixtures`, which never regenerates a format 1 fixture, gives the five fixtures that `EncryptFiles` wrote in v0.10 their area of 512 bytes (`EncryptOptions.TestAreaLen`) and recomputes the derived fields of every record; `format3_note`, with a public note, for the mutations of §64; the frozen `datekeys inspect -json` output of each, `*.inspect.json`; `testdata/vectors/padding.json`; `security_cms.json` and `locator.json`, frozen once written (`frozenVectors`); formats in `testdata/README.md` | `capsule.TestConformanceFixtures` (`checkBody3`, `checkSignature3`), `TestPaddingAcrossChunks` (a capsule generated at run time), `TestCMSVectors`; `locator.TestLocatorVectors`; `ed25519strict.TestVectors`; `internal/testkit.TestFormat3VectorFiles`, `TestVectorFilesAreCurrent`; `cmd/datekeys.TestInspectJSONGoldens`, `TestDecryptFixtures`, `TestDecryptFormat3Fixtures`, `TestMTimeAfterSeal` |
| 68 | `.dkk` vectors, with the exact extension data; one carries an extension with data, two accompany a format 2 capsule and one a format 3 capsule | `testdata/fixtures/*.dkk` + `*.dkk.json`; `time_and_key_portable_extension.dkk` derived by `genfixtures`; `format2_time_and_key_portable.dkk`, `format2_time_and_key_recipients.dkk`, `format3_time_and_key_portable.dkk` | `accesskey.TestFixtures`, `TestFixtureWithExtension`; `capsule.TestAccessKeyFixtureWithExtension` |
| 69 | Normative errors, including `ERR_EXTENSION_DATA_INVALID` and `ERR_HEAD_INVALID`; every error of the module wraps exactly one | `errors.go`; `provider/drand.Client` and step 9 of `capsule.Open` keep another code of a failure as text only | `datekeys.TestCatalogueMatchesSpec`, `TestCode`; `drand.TestFetchErrorHasOneCode`; `capsule.TestReleaseSourceErrorsAtStep9` |
| 69.1 | Error precedence: the first failing layer of each object (frame, a truncated prelude before the version; type tag and schema version; CBOR profile and CDDL, except the rules with codes of their own; fields with codes of their own in ascending key order, an extension unknown in its object or array before invalid data), the step order of §63 across objects and steps; only the optional inspection of steps 5, 6 and 8 and the `capsule_digest` check can change the code; the codes of step 10 are those of a release supplied directly, one from a network source being discarded at step 9; in format 2, the 16 stanzas belong to step 12, the version of CONTROL_CBOR against the format to layer 2 of step 14, the rules of keys 6 and 7 to its layer 3, and the length and padding of the plaintext to step 17 | `capsule.ParsePrelude`, `capsule.DecodeHeader`, `capsule.DecodeControl`, `accesskey.Decode`, `accesskey.DecodeBody`, `profile.Decode`, `codec.CheckSchema`, `codec.Unmarshal`, `extension.CheckCriticalIn`, `capsule.checkAccessKey`, `OpenOptions.AccessKeyFile`, `agewrap.AccessIdentity`, `provider/drand.Client` | `capsule.TestPrecedenceWithinPublicHeader`, `TestPrecedenceAcrossSteps` (with the examples of format 2), `TestDecodeHeaderReportsTheCDDLFirst`, `TestAccessKeyCheckOrder`, `TestAccessKeyFileAtStep9`, `TestControlCriticalBeforeHeaderBinding`, `TestReleaseFromANetworkSource`, `TestExtensionPlacement`; `accesskey.TestDecodePrecedence`; `agewrap.TestAccessIdentityStrictness`; `profile.TestDecodePrecedence`, `TestPinPathMatchesDecode`; `cmd/datekeys.TestDecryptAccessKeyOrder`; `extension.TestCheckCritical`, `TestPlacement`; `codec.TestCheckSchemaVersionForms`; `testdata/vectors/cbor.json`, `inspect_differential.json` |
| 70 | Compatibility: a reader accepts the three formats and opens formats 1 and 2 with the semantics of v0.8.2 and v0.9; an implementation that writes capsules writes format 3, and only a generator of test vectors writes formats 1 and 2; a reader should report the format; the format is neither the version of the specification (`datekeys.SpecVersion`) nor that of the module (`datekeys.Version`, `datekeys version`); a reader of v0.10 opens the capsules of v0.11, with an area of 32 or 64 KiB, F1 for `alg` 1 and 2, S1 for `seal_type` 2, and the public note and `datekeys.capsule` ignored, and a reader of v0.11 those of v0.10, with their area of 512 bytes; v0.12 changes no format, only texts of the verdicts and how a certificate and a token are read | magic and version checks; `capsule.Format`; `codec.Peek` and `codec.CheckSchema` read keys 0 and 1 only, before strict decoding, with a type tag of at most `codec.MaxTypeTagLen` bytes; `Inspection.Prelude.Format`, `Opened.Format`; `internal/testkit.Build`; `version.go`, where `SpecVersion` stays 0.11 until v0.12 is approved; `capsule.EvaluateSecurity`, which reads security without a context, as a reader of v0.10; `ParseBodyFrame`, which accepts any area of the frame; v0.15: a valid release in hand opens a capsule with a clock behind its round time | mutations; `capsule.TestFormatDispatch`, `TestFormatRelabel`, `TestFormat1Compatibility`; `codec.TestPeek`, `TestCheckSchema`, `TestCheckSchemaVersionForms`, `FuzzPeek`; `capsule.TestDecodeSchemaVersion`; `datekeys.TestSpecVersionNamesTheSpecification`, `TestVersion`; `cmd/datekeys.TestVersion`; `capsule.TestEvaluateSecurityIn` and `TestEvaluateCMS` (no context: F1), `TestEvaluateSeal` (no context: S1), `TestCMSVectors` (the cases without a context); the fixtures of v0.10, with their area of 512 bytes, among those of v0.11 (`TestConformanceFixtures`); mutation *round not reached yet* |
| 71 | Profile registry; the states `activo`, `solo lectura` and `comprometido`; SHOULD: warn of a compromised profile | `profile.Decode` + `profile.NewRegistry` with pinned hashes; `profile.Status`, `StatusOf` (the states that this release of the module knows, by profile_hash: Quicknet active); `cmd/datekeys`: `encrypt` writes no capsule with a profile that is not active, and `decrypt` and `inspect` warn when the profile of a capsule is compromised | `profile.TestRegistry`, `TestStatus`; `cmd/datekeys.TestProfileStatus` |
| 72 | Extension registry and registration rules, among them the objects and arrays where each extension may appear, and an encoder never writes one elsewhere; the encoder decodes its own output before sealing; security-relevant claims in CONTROL_CBOR or under a signature extension, `.dkk` extension data advisory; the registered extensions, `datekeys.note` in the noncritical array of PUBLIC_HEADER and `datekeys.capsule` in the noncritical array of a `.dkk`, both informative | `extension.Registry`, `extension.Set`, `extension.DataValidator`, `extension.Placement` (optional: a `Registry` without it knows its extensions in every object and array); `extension.Standard`, the registry of the extensions of the specification (`NoteID`, `CapsuleID`), and `locator.Standard`, which validates the data of `datekeys.capsule`; `extension.CheckWrite`, which the writers of capsules and `.dkk` files apply with `extension.Standard` (`capsule.newSealer`, `accesskey.AccessKey.MarshalBody`); self-checks in `capsule.Encrypt`, `capsule.EncryptFiles`, `accesskey.MarshalBody` and `locator.Info.Extension`; these writers take no `Registry`: the application writes each extension of its own only where it is registered | `capsule.TestKnownCriticalExtensions`, `TestUnusableNoncriticalExtensions`, `TestExtensionPlacement`, `TestNestedDataSealsAndOpens`, `TestRegisteredExtensionsWhereRegistered`, `TestPublicNoteRules`, `FuzzEncodeImpliesDecode`; `extension.TestPlacement`, `TestCheckWrite`; `locator.TestInfo` |
| 74 | Provisional aspects; the implementation limits of the reference (name lengths, `public_key`, `period`, maximum `extension_id` length, `dk1_` length, age parser limits, `ERR_POLICY_STRUCTURE_MISMATCH` for INNER_ACCESS_AGE) | `profile.ValidID`, `validName`, `maxPublicKeyLen`, `maxPeriod`; `extension.MaxIDLen`; `datekey.MaxEncodedLen`; `filippo.io/age` | `profile.TestValidateRejectsTamperedProfiles`, `TestIntegerRanges`; `extension.TestNew`; the vectors of `cbor.json` named after the implementation limit |
| 75 | Blocking requirements before v1.0 | items 1–9 and 11 above, with fixtures and mutations in the three formats; item 10 (external review) pending | — |
| 76 | Change policy; the normative changes of v0.8.2: the extension change and its reproducible cases; the refinements and theirs; the amendment on point canonicality and its case (a second implementation on `tlock-js` and `@noble/curves` 1.9.7 accepted U with c0 + p and a signature with x + p); the corrections of the formal review (an invalid release from a network source, the objects and arrays of each extension, the serialization of GT in H2) and their cases, and those of its second round (the encoder rule of §72, the codes of step 10 in §17 and §51 for a release supplied directly, one code for any failure of a source at step 9); the normative changes of v0.9, capsule format 2, and their cases; those of v0.10, capsule format 3, and theirs; those of v0.11, the area of 32 KiB, what is signed, the signatures of `alg` 1 and 2, the seal of `seal_type` 2, the key of words, the public note and `datekeys.capsule`, and theirs; and those of the draft v0.12, which change no format: the names of certificates and the seal of each signer of F6 in the verdicts, the holder without its identifier, the profile of the certificate, identifiers, repetitions and edge cases, the addresses and the padding of the locator, errata, and the vectors of v0.11 | `extension`, `codec`, fixture `time_only_extensions` regenerated; refinements: the order of `capsule.checkAccessKey`, `BODY_LEN` 0 in `accesskey.Decode`, CR and LF and invalid UTF-8 in `datekey.Parse`, the `.dkk` decoded at step 9.a (`OpenOptions.AccessKeyFile`, the CLI), nil identities in `capsule.Open`, every identity tried in `agewrap.AccessIdentity`, `profile.NewRegistry` through `Decode`, `Profile.Validate` rule 1 first; four new `dk1.json` vectors; corrections: `extension.Placement` and the object-aware checks, the `provider.ReleaseSource` contract, `testdata/vectors/tlock_ibe.json`; second round: the error of `provider/drand.Client` and of step 9 in `capsule.Open`; v0.9: rows 22, 29, 29.1, 31, 33, 36, 37, 39, 55.2, 56, 57, 61, 62, 62.1, 63 and 70; v0.10: rows 22, 23, 29 to 29.7, 31, 56, 57, 61 to 64 and 67 to 70; v0.11: rows 24.1, 29.2, 29.3, 29.7 to 29.12, 38.1, 44.1, 62.1, 64, 67, 70 and 72; v0.12: rows 29.3, 29.7, 29.10, 29.11, 44.1, 64, 67 and 70, and the sizes of the locator in `spec/datekeys.cddl` | case 2: `extension.TestNew`; case 3: `capsule.TestNaNKeyedDataHasOneVerdict`; case 4: `capsule.TestExtensionFixtureData`; case 5: `capsule.TestNestedDataSealsAndOpens`; case 6: `capsule.TestHugeExtensionArraysAreRejected`, `extension.TestCheckDisjointIsLinear`; refinements: the tests of rows 12.1, 15, 17, 19, 22, 28.1, 35, 36, 40, 51, 55.1, 63 and 69.1, and `extension.TestOrderIsUnsignedBytewise`; amendment: the tests of rows 12.2 and 64; corrections: `capsule.TestReleaseFromANetworkSource`, `TestExtensionPlacement`, `extension.TestPlacement`, `agewrap.TestTlockH2Vector`; second round: `capsule.TestReleaseSourceErrorsAtStep9`, `TestExtensionPlacement` (the noncritical array of a `.dkk`), `drand.TestFetchErrorHasOneCode`, `TestUnavailabilityAndCancellation`, `datekeys.TestCode`; v0.9: the tests that §76 names for each change, in rows 22, 29.1, 31, 37, 39, 55.2, 57, 62.1, 64 and 70; v0.10: those of the rows it changed; v0.11: those of the rows it added and changed; v0.12: changes 1 and 2, `capsule.TestEvaluateCMS`, `TestIssuerTextFiltered`, `cmd/datekeys.TestRows` and the record of `format3_signed_cms`; change 5, `der.TestSetOfSorted`, `TestCheck` and `cms.TestTSTInfoStrict`; changes 6 and 7, `locator.TestAddresses`, `TestUsableAddresses`, `TestLeastMultiple` and `TestPaddingBoundaries`; change 9, `testdata/vectors/security.json` and the fixture `format3_seal_unsupported`; the cases of changes 1 and 3 to 5 in `security_cms.json`, and those of changes 6 and 7 in `locator.json` (`TestLocatorVectors`); the changes of v0.15, each with its case: the release object (`release.json`, `testdata/releases`), its chain hash at step 10 and step 9.c (`mutations.json`, field `source`), the recovery (`scripts/recovery_check.sh`) |
| 79 | Informative annex: recovery without DateKeys software (v0.15) | `scripts/recovery` (no package of this module, tlock or drand: Go, `golang.org/x/crypto`, `filippo.io/age`, `drand/kyber-bls12381`), `scripts/recovery_check.sh`, run by `scripts/check.sh` | `scripts/recovery.TestImports`, `TestRecoverFixtures` and the other tests of the package, over eight fixtures of the three formats; `scripts/recovery_check.sh` on `format3_single` and `format3_time_and_key_portable` |
| 79 | Informative annex: recovery without DateKeys software (v0.15), with the key of words in 79.7 and a last chunk that may be full (v0.16); §62.1 rule 27, the annex next to each `.dkc` | `datekeys.RecoveryAnnex` (`annex/recovery.md`, §79 under a title with the version and the SHA-256 of the specification) and `RecoveryAnnexSuffix`; `encrypt` writes it as `FILE.dkc.recuperacion.txt` unless `-no-recovery`; `scripts/recovery` (no package of this module, tlock or drand: Go, `golang.org/x/crypto`, `filippo.io/age`, `drand/kyber-bls12381` and the interfaces of `drand/kyber`), which opens with `-words`, by the normalization without tables or, with `-unicodedata`, the full one, and PBKDF2 of the standard library, `scripts/recovery_check.sh`, run by `scripts/check.sh` | `scripts/recovery.TestImports`, `TestRecoverFixtures` and the other tests of the package, over ten fixtures of the three formats, `TestAnnexWordVectors`, `TestNormalizeVectors` (both normalizations against every case of `wordkey.json`), `TestRecoverWithWords`; `datekeys.TestRecoveryAnnex` (the annex is §79 of the specification of `SpecVersion`), `cmd/datekeys.TestRecoveryAndNotices`; `scripts/recovery_check.sh` on `format3_single`, `format3_time_and_key_portable`, `format3_time_and_key_words` and `format3_full_chunk` |
lines=append(lines,"Según un sello a nombre de "+q(c.sealTSA.holder)+", existía el "+at(c.sealTime)+", antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.")
g.add(vcase{name:"alg 2: t plus the accuracy of the seal equals the round time: F6, not before the opening date",area:g.area(g.signed(only(ana),edge,ana),nil),
g.add(vcase{name:"alg 2: a seal of the BTSP policy without accuracy: F6, not proven before the opening date, by its policy",area:g.area(g.signed(only(ana),btsp,ana),nil),
g.add(vcase{name:"alg 2: a key 3 of seal_type 4294967295 beside it: F5 and S1",area:g.area(key2,g.must(capsule.EncodeSeal(capsule.SealTypeTest,[]byte{1}))),
g.add(vcase{name:"alg 2: a valid seal of seal_type 2 in key 3 beside it: F5 and S4",area:g.area(key2,g.must(capsule.EncodeSeal(capsule.SealTypeRFC3161,cmstest.Token(subject[:],vecSigned,cmstest.TokenOptions{},tsa.Signer)))),
g.add(vcase{name:"alg 2: a valid seal of seal_type 2 in key 3 beside it: F5 and S4",area:g.area(key2,g.must(capsule.EncodeSeal(capsule.SealTypeRFC3161,cmstest.Token(subject[:],vecSigned,cmstest.TokenOptions{Accuracy:time.Second},tsa.Signer)))),
sealedAt("seal: before the round time: S4",tok(vecSigned,cmstest.TokenOptions{},tsa),tsa,vecSigned,capsule.VerdictSealed)
sealedAt("seal: after the round time: S5",tok(vecRound.Add(time.Minute),cmstest.TokenOptions{},tsa),tsa,vecRound.Add(time.Minute),capsule.VerdictSealedLate)
sealedAt("seal: before the round time: S4",tok(vecSigned,second,tsa),tsa,vecSigned,capsule.VerdictSealed)
late("seal: after the round time, without accuracy: S5, sealed after",tok(vecRound.Add(time.Minute),cmstest.TokenOptions{},tsa),tsa,vecRound.Add(time.Minute),capsule.ReasonLate)
early:=vecRound.Add(-time.Second)
sealedAt("seal: t plus the accuracy past the round time: S5",tok(early,cmstest.TokenOptions{Accuracy:2*time.Second},tsa),tsa,early,capsule.VerdictSealedLate)
sealedAt("seal: t plus the accuracy equal to the round time: S5",tok(early,cmstest.TokenOptions{Accuracy:time.Second},tsa),tsa,early,capsule.VerdictSealedLate)
late("seal: t plus the accuracy past the round time: S5",tok(early,cmstest.TokenOptions{Accuracy:2*time.Second},tsa),tsa,early,capsule.ReasonLate)
late("seal: t plus the accuracy equal to the round time: S5",tok(early,cmstest.TokenOptions{Accuracy:time.Second},tsa),tsa,early,capsule.ReasonLate)
late("seal: without accuracy, years before the round time: S5, it does not say its precision",tok(vecSigned,cmstest.TokenOptions{},tsa),tsa,vecSigned,capsule.ReasonNoAccuracy)
late("seal: of the BTSP policy of ETSI, without accuracy: S5, it does not say the precision its policy requires",tok(vecSigned,cmstest.TokenOptions{Policy:cmstest.BTSPPolicy},tsa),tsa,vecSigned,capsule.ReasonNoAccuracyBTSP)
late("seal: of the BTSP policy, without accuracy, after the round time: S5, sealed after",tok(vecRound,cmstest.TokenOptions{Policy:cmstest.BTSPPolicy},tsa),tsa,vecRound,capsule.ReasonLate)
sealedAt("seal: of the BTSP policy, with accuracy: S4",tok(vecSigned,cmstest.TokenOptions{Policy:cmstest.BTSPPolicy,Accuracy:time.Second},tsa),tsa,vecSigned,capsule.VerdictSealed)
sealedAt("seal: an accuracy of 0 seconds, a microsecond before the round time: S4",tok(vecRound.Add(-time.Microsecond),cmstest.TokenOptions{AccuracyRaw:cmstest.Seq(cmstest.Int(0))},tsa),tsa,vecRound.Add(-time.Microsecond),capsule.VerdictSealed)
sealedAt("seal: an empty accuracy, a precision of 0: S4",tok(vecSigned,cmstest.TokenOptions{AccuracyRaw:cmstest.Seq()},tsa),tsa,vecSigned,capsule.VerdictSealed)
sealedAt("seal: t plus an accuracy of 999 ms and 999 µs, a microsecond before the round time: S4",
sealedAt("seal: an accuracy of seconds, millis and micros: S4",tok(vecSigned,cmstest.TokenOptions{Accuracy:time.Second+5*time.Millisecond+7*time.Microsecond},tsa),tsa,vecSigned,capsule.VerdictSealed)
fraction:=vecSigned.Add(250*time.Millisecond)
sealedAt("seal: a genTime with a fraction of a second: S4, t with its fraction",tok(fraction,cmstest.TokenOptions{},tsa),tsa,fraction,capsule.VerdictSealed)
sealedAt("seal: the certificate of the authority twice: S4",tok(vecSigned,cmstest.TokenOptions{TSATwice:true},tsa),tsa,vecSigned,capsule.VerdictSealed)
sealedAt("seal: a CRL in the token decides nothing: S4",tok(vecSigned,cmstest.TokenOptions{CRL:cmstest.Seq(cmstest.Seq(cmstest.Int(1)),cmstest.Seq(cmstest.OID(cmstest.OIDECDSA256)),cmstest.BitString([]byte{0}))},tsa),tsa,vecSigned,capsule.VerdictSealed)
sealedAt("seal: signing-certificate-v2 in the token: S4",tok(vecSigned,cmstest.TokenOptions{SigCertV2:true},tsa),tsa,vecSigned,capsule.VerdictSealed)
sealedAt("seal: a genTime with a fraction of a second: S4, t with its fraction",tok(fraction,second,tsa),tsa,fraction,capsule.VerdictSealed)
sealedAt("seal: the certificate of the authority twice: S4",tok(vecSigned,cmstest.TokenOptions{TSATwice:true, Accuracy:time.Second},tsa),tsa,vecSigned,capsule.VerdictSealed)
sealedAt("seal: a CRL in the token decides nothing: S4",tok(vecSigned,cmstest.TokenOptions{CRL:cmstest.Seq(cmstest.Seq(cmstest.Int(1)),cmstest.Seq(cmstest.OID(cmstest.OIDECDSA256)),cmstest.BitString([]byte{0})), Accuracy:time.Second},tsa),tsa,vecSigned,capsule.VerdictSealed)
sealedAt("seal: signing-certificate-v2 in the token: S4",tok(vecSigned,cmstest.TokenOptions{SigCertV2:true, Accuracy:time.Second},tsa),tsa,vecSigned,capsule.VerdictSealed)
tsaName:=cmstest.TLV(0xa0,cmstest.TLV(0xa4,cmstest.Name(cmstest.ATV(cmstest.OIDCommonName,cmstest.UTF8("Autoridad de Sellado de prueba")))))
sealedAt("seal: ordering TRUE, a nonce, a tsa and extensions: S4",tok(vecSigned,cmstest.TokenOptions{After:[][]byte{cmstest.Bool(true),cmstest.Int(99),tsaName,exts}},tsa),tsa,vecSigned,capsule.VerdictSealed)
sealedAt("seal: ordering TRUE, a nonce, a tsa and extensions: S4",tok(vecSigned,cmstest.TokenOptions{After:[][]byte{cmstest.Bool(true),cmstest.Int(99),tsaName,exts}, Accuracy:time.Second},tsa),tsa,vecSigned,capsule.VerdictSealed)
rsaTSA:=named(cmstest.NewRSA("Autoridad RSA de prueba",2048,certFrom,certTo),"Autoridad RSA de prueba")
sealedAt("seal: an authority of RSA with RSASSA-PSS and SHA-512: S4",tok(vecSigned,cmstest.TokenOptions{CMS:cmstest.Options{PSS:true,Hash:crypto.SHA512}},rsaTSA),rsaTSA,vecSigned,capsule.VerdictSealed)
sealedAt("seal: an authority of RSA with RSASSA-PSS and SHA-512: S4",tok(vecSigned,cmstest.TokenOptions{CMS:cmstest.Options{PSS:true,Hash:crypto.SHA512}, Accuracy:time.Second},rsaTSA),rsaTSA,vecSigned,capsule.VerdictSealed)
// The case of §76, change 1: a name that lines up a text of its own.
spaced:=cmstest.NewECDSA("TSA"+strings.Repeat(" ",50)+"Firmado con la clave que guardaste como Banco",elliptic.P256(),certFrom,certTo)
vspaced:=vsigner{spaced,hashOfCert(spaced),""}
sealedAt("seal: an authority named with 50 spaces and the text of F3: S4, by its SHA-256",tok(vecSigned,cmstest.TokenOptions{},vspaced),vspaced,vecSigned,capsule.VerdictSealed)
sealedAt("seal: an authority named with 50 spaces and the text of F3: S4, by its SHA-256",tok(vecSigned,second,vspaced),vspaced,vecSigned,capsule.VerdictSealed)
// S3: it reads, and does not verify (§29.11, step 3).
seal("seal: over another subject: S3",cmstest.Token([]byte("other"),vecSigned,cmstest.TokenOptions{},tsa.Signer),capsule.VerdictSealInvalid)
g.add(vcase{name:"seal: over a capsule without a signature: F0 and S4",area:g.area(nil,g.must(capsule.EncodeSeal(capsule.SealTypeRFC3161,cmstest.Token(noSig[:],vecSigned,cmstest.TokenOptions{},tsa.Signer)))),
g.add(vcase{name:"seal: over a capsule without a signature: F0 and S4",area:g.area(nil,g.must(capsule.EncodeSeal(capsule.SealTypeRFC3161,cmstest.Token(noSig[:],vecSigned,second,tsa.Signer)))),
g.add(vcase{name:"seal: beside a signature of alg 4294967295, which it seals all the same: F1 and S4",area:g.area(unknown,g.must(capsule.EncodeSeal(capsule.SealTypeRFC3161,cmstest.Token(beside[:],vecSigned,cmstest.TokenOptions{},tsa.Signer)))),
g.add(vcase{name:"seal: beside a signature of alg 4294967295, which it seals all the same: F1 and S4",area:g.area(unknown,g.must(capsule.EncodeSeal(capsule.SealTypeRFC3161,cmstest.Token(beside[:],vecSigned,second,tsa.Signer)))),
secret:=[]file3{{path:"secreto.txt",content:[]byte("DateKeys fixture opened with a portable .dkk.\n"),mtime:when}}
return[]spec{
{name:"time_only",format:f1,description:"time_only capsule, two STREAM chunks, no extensions",round:1000,policy:capsule.TimeOnly,plaintext:large},
@ -537,6 +546,8 @@ func specs() []spec {
}},
{name:"format3_unsigned",format:f3,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",round:1000,policy:capsule.TimeOnly,padding:capsule.Reforzado,files:note},
{name:"format3_signed",format:f3,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",round:1000,policy:capsule.TimeOnly,padding:capsule.Reforzado,files:note,signer:signerSeed},
{name:"format3_time_and_key_words",format:f3,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»",round:1000,policy:capsule.TimeAndKey,padding:capsule.Reforzado,files:secret,words:annexWords},
{name:"format3_full_chunk",format:f3,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",round:1000,policy:capsule.TimeOnly,padding:capsule.Bloque256,files:fullChunk},
{name:"format3_signed_cms",format:f3,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",round:1000,policy:capsule.TimeOnly,padding:capsule.Reforzado,files:note,configure:configureCMS},
{name:"format3_note",format:f3,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)",round:1000,policy:capsule.TimeOnly,padding:capsule.Reforzado,files:note,
configure:func(o*capsule.EncryptOptions)error{o.PublicNote="Cartas del viaje a Lisboa";returnnil}},
returnnil,errors.New("the words hold a character outside the normalization without tables of the annex (79.7): give UnicodeData.txt of Unicode 18.0.0 with -unicodedata")
long-term recovery of capsules: the release object, the release of a
@ -80,14 +80,33 @@
software. It changes no format of `.dkc` or `.dkk`, and one verdict: a
valid release in hand opens a capsule with a clock behind its round time.
Its §76 records each change with its case.
- `datekeys.cddl`: the CBOR schemas of v0.15, those of v0.12, v0.13 and v0.14 with the rule `release` added, the three control
- `DateKeys_Protocol_Specification_v0.16.md`: frozen copy of the normative
draft v0.16 (7 October 2026), approved by its author on that date and
tagged `spec-v0.16`; this module implements it. SHA-256:
`807d4fe85ac09ad6f97abc75ab3e2156bb2f3fb0dc589777f4420627fad545e1`. It fixes what Astra's
review of v0.15 found: a valid seal without `accuracy` no longer proves
that it came before the unlock date (S5, with its reason); the recovery
annex derives a key of words without DateKeys software, with a recipe
without tables for the letters of the DateKeys lists and `UnicodeData.txt`
of Unicode 18.0.0, named by its SHA-256, for any other text; the last
chunk of an `age` file may be full; a signature with certificates keeps
the chains without their roots and the OCSP responses that fit, and the
writer says what it leaves out; and drand's JSON is read strictly, with no
repeated names, exact names and an integer round. It changes no format.
Its §76 records each change with its case.
- `datekeys.cddl`: the CBOR schemas of v0.16, the same as those of v0.15: those of v0.12, v0.13 and v0.14 with the rule `release` added, the three control
versions and the security and head objects of format 3 included, with the
encoding rules CDDL cannot express. Those of v0.9 and v0.8.2 are at the tags
`spec-v0.9` and `spec-v0.8.2`.
The specification is licensed under the Creative Commons Attribution 4.0
International License (CC-BY-4.0): <https://creativecommons.org/licenses/by/4.0/>.
The code of this repository is licensed separately under Apache-2.0.
The specification is licensed under the Creative Commons
Attribution-NoDerivatives 4.0 International License (CC-BY-ND-4.0):
<https://creativecommons.org/licenses/by-nd/4.0/>. It may be copied and shared
unchanged, with credit; a modified version or a translation needs the written
permission of its author. The recovery annex, `annex/recovery.md`, is §79 under
a title and carries the same license. The code of this repository is licensed
separately under Apache-2.0, and the word lists of `wordkey/lists` keep their
own licenses (`wordkey/lists/README.md`).
Changes to the specification follow its §76: a normative change should answer a
reproducible case found through the reference implementation, the CDDL, a
"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",
"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)",
"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",
"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",
"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",
"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",
"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",
"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»",
"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",
"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",
"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",
"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.",
"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.",
"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.",
"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.",
"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.",
"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.",
"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.",
"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.",
"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.",
"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.",
"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"
"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.",
"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.",