Format 3, step 7: the documentation of spec v0.10

- README and README.es: specification 0.10, format 3 written and formats
  1 to 3 read; BODY in the picture of a capsule; the CLI with folders,
  -comment, -author, -no-mtime and decrypt into a new folder, with the
  presentation of 29.7; EncryptFiles, Source and Sink in the library; the
  metadata that format 3 hides, safe extraction, and the declared author
  and comment that prove nothing; the fixtures, vectors and mutations of
  format 3; 18 normative errors.
- CHANGELOG: the section of specification v0.10.
- docs/traceability.md: rows 29.2 to 29.7 and 29.5.1, and the rows that
  format 3 changes (22, 23, 28, 29, 29.1, 31, 33, 39, 56, 57, 61 to 64,
  67 to 70, 75 and 76); ERR_HEAD_INVALID in the error mapping; two
  implementation decisions, the Sink and the width of the terminal.
- testdata/README.md: the nine fixtures of format 3 and their records,
  the four new vector files, the mutation corpus of 209 cases with its
  list of format 3 and the verdicts of the cases that open, and the two
  bases of format 3 of the differential corpus.
- SECURITY.md: specification v0.10, and the head as untrusted input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.10
dev 1 week ago
parent 18eb3c3f48
commit 20f9959b6e

@ -3,6 +3,128 @@
All notable changes to this module are documented here. The project follows
semantic versioning; `v0.x` versions make no API stability promise.
## Unreleased — specification v0.10
Moves the module to the DateKeys Protocol Specification v0.10, which adds
capsule format 3, delivery 1 of its design: a capsule holds files, with
their paths, sizes, SHA-256 and modification times, a comment and a
declared author, and a security area that later versions fill with an
author signature and a time seal. `EncryptFiles` writes format 3; `Open`
and `Inspect` read the three formats, and capsules of formats 1 and 2 keep
their verdicts.
### Format 3
- `VERSION` 3 in the PRELUDE and CONTROL_CBOR of schema version 3, with the
keys of version 2: L is the length of BODY (spec §22, §31).
- BODY (spec §29.2): a frame of three uint32, `AREA_LEN`, `SECURITY_LEN` and
`HEAD_LEN`, the security area, 512 bytes when written and from 512 to
65536 when read, the head and the files. A violation of the frame, L
under 12 included, or a byte of the area after SECURITY_CBOR that is not
zero, is `ERR_INTEGRITY` at step 17. `capsule.BodyFrame`,
`ParseBodyFrame`, `CheckArea`.
- Security (spec §29.3, §29.7): a map of version 1 whose keys 2 and 3 are
byte strings holding the author signature and the seal, encoded apart.
It never fails and never decides the opening: `capsule.EvaluateSecurity`
gives the verdicts X, F0, F1, S0, S1 and S2, and `Verdicts.Lines` the
texts of the table, in Spanish. This version implements no `alg` and no
`seal_type`.
- The head (spec §29.4): its type tag and version 1, then the CDDL with R1
and R8, then, in key order, the comment and the declared author, each
file with R2 to R6c, R10 and its layout, R7 and R9 over the tree, and the
critical extensions. A violation of a rule of layer 4 is the new
`ERR_HEAD_INVALID`. `capsule.Head`, `File`, `EncodeHead`, `DecodeHead`,
and `extension.Head` for its extensions.
- Paths and texts (spec §29.5, §29.6, §29.5.1): `internal/pathrule` applies
the rules with tables generated from 19 pinned data files, Unicode 18.0.0
and the 15 WindowsBestFit tables, never with the Unicode functions of the
platform; `TablesDigest` pins them. Its errors name the rule and the
character, never echo the text, and read the same in every
implementation. R4b and the invisibles rule of §29.6 keep ZWJ, ZWNJ,
VS15 and VS16 to their emoji and script uses.
### Reader (spec §63 step 17, §56, §57)
- `capsule.Sink` receives the files: `Begin` with the validated head,
`Create` for each file, `Commit` at step 18 only, and `Abort` once after
any failure that follows `Begin`. A format 3 capsule without
`OpenOptions.Sink` fails right after step 2 with `capsule.ErrSinkRequired`,
a caller error without a code, before any request; a capsule of format 1
or 2 without `dst` fails there too.
- Step 17 in its substeps: the frame and the area, security, the head, the
files filling CONTENT, the SHA-256 of each file and the padding. A failure
of age, or a plaintext whose length is not P, prevails; otherwise the
first substep that fails decides, and a code other than `ERR_INTEGRITY`
is reported only after reading PAYLOAD_AGE to its end.
- Reads of BODY grow with the bytes received, never with `AREA_LEN`,
`HEAD_LEN` or a declared size (spec §57).
- `Opened` gains `Head`, `Verdicts`, `AreaLen` and `UnusableHeadExtensions`.
### Writer (spec §61, §62, §62.1)
- `capsule.EncryptFiles` writes format 3 from a list of `capsule.Source`,
each read twice: first to check the paths and the texts with the rules
of the reader, measure L with a head whose salt and SHA-256 are zero, and
hash each file; then to write it, failing if its size or SHA-256 changed
(rules 14 to 18). Files go in the byte order of their paths; the comment
has its CR LF turned into LF; the mtime is kept from 1970 to 9999 and
omitted otherwise. The head, the control and security are decoded with
the rules of the reader before anything is written (rule 17), and the
area is always 512 bytes with the empty security (rule 13).
- `EncryptOptions` gains `Comment`, `Author`, `HeadCritical`,
`HeadNoncritical` and `TestVectors`. `Encrypt` writes format 2 only with
`TestVectors`, for generators of test vectors (rule 1). `Result.Head` is
the head written.
### CLI
- `datekeys encrypt` takes `-in` several times, files and folders; a folder
gives its name as the first segment and is walked with `Lstat`, following
no link, taking regular files only and leaving out `.DS_Store`,
`Thumbs.db`, `desktop.ini`, `._*` and `__MACOSX`, which it reports. New
`-comment`, `-author` and `-no-mtime`. A pipe is no longer accepted.
- `datekeys decrypt` writes the files of a format 3 capsule to the new
folder `-out`: `os.Mkdir` claims it, only when there are files; the tree
is staged in `-out/.datekeys-*` through an `os.Root`, with `O_EXCL` and
mode 0600; the mtimes are set and each entry of the first level is moved
into place at step 18, and any failure removes the folder. Formats 1 and
2 still write a file.
- The presentation of spec §29.7 goes to stdout: the verdicts, the declared
author and the comment box with their labels, the paths, and the
verdicts again. Each line of the creator goes in pieces of at most W − 3
columns behind `│ `, counting 2 for anything but printable ASCII, with
its TABs expanded; W is the width of the terminal, asked through
`syscall` on Unix and Windows, or 80. Shortcuts, `desktop.ini`, `.git`,
programs and a leading dash get a warning.
### Errors
- `ERR_HEAD_INVALID` joins the catalogue, last (spec §69): 18 codes.
### Test data
- `datekeys.SpecVersion` is `0.10`, and every test data file says so.
- Nine fixtures of format 3 (spec §67): `format3_single`, `format3_tree`,
`format3_comment_only`, `format3_bloque256` and
`format3_time_and_key_portable`, written with `EncryptFiles`, and
`format3_area_1024`, `format3_security_v2`,
`format3_signature_unsupported` and `format3_seal_unsupported`, which only
a generator of test vectors writes. Their records add the area, security,
the head, the salt, the comment, the declared author, each file and the
verdicts, and their plaintext file is BODY. The .dkk of formats 2 and 3
join the .dkk tests (spec §68).
- New vectors: `paths.json`, `path_fold.json`, `head_schema.json` and
`security.json`; `cbor.json` gains the control of version 3.
- The mutation corpus gains the 33 mutations of the first two lists of
spec §64 on format 3 and the 47 of the list of format 3, three of which
open with their verdicts; format 2 gains "format 2 time_only relabeled
format 3", and "version changed" sets `VERSION` 4. 209 cases, 169 of the
spec. `testkit.Splice` gives an edit for each run of changed bytes.
- The differential corpus gains two bases of format 3, one per policy:
5110 cases, the earlier ones unchanged.
- The tests of the paths and of format 3 hold their invisible and combining
characters as Go escapes.
## Unreleased — specification v0.9
Moves the module to the DateKeys Protocol Specification v0.9, which adds

@ -1,7 +1,7 @@
# datekeys-go
Implementación de referencia en Go de la **DateKeys Protocol Specification
v0.9** ([`spec/`](spec/DateKeys_Protocol_Specification_v0.9.md)).
v0.10** ([`spec/`](spec/DateKeys_Protocol_Specification_v0.10.md)).
[English version](README.md).
DateKeys cifra datos de forma que solo puedan abrirse a partir de un instante
@ -22,7 +22,8 @@ transportes no confiables.
| DateKey: fecha → ronda, cadena canónica `dk1_…` | §14–§19 | [`datekey`](datekey) |
| Provider Profile, perfil Quicknet pinneado, `profile_hash` | §10–§13 | [`profile`](profile) |
| Fuentes de releases, verificación BLS local, relays drand | §45–§52 | [`provider`](provider), [`provider/drand`](provider/drand) |
| DateKeyCap `.dkc`: `time_only` y `time_and_key` | §20–§39, §61–§63 | [`capsule`](capsule) |
| DateKeyCap `.dkc`: `time_only` y `time_and_key`, los ficheros del formato 3 | §20–§39, §61–§63 | [`capsule`](capsule) |
| Rutas y textos del head, con las tablas de Unicode 18.0.0 y best-fit | §29.5, §29.6 | [`internal/pathrule`](internal/pathrule) |
| DateKeys Access Key `.dkk` | §40–§44 | [`accesskey`](accesskey) |
| Extensiones | §54 | [`extension`](extension) |
| CBOR determinista | §58 | [`codec`](codec) |
@ -37,8 +38,9 @@ reglas de stanzas del protocolo dentro de las identities de age, para que un
fichero nunca se acepte solo porque age haya podido desenvolver una clave.
No implementa, a propósito: el servidor y la cola de la Release API, el
almacenamiento y la entrega, extensiones concretas ni el cliente TypeScript
(plan §2).
almacenamiento y la entrega, extensiones concretas, la firma de autor y el
sello de tiempo, que la especificación reserva para versiones posteriores, ni
el cliente TypeScript (plan §2).
## Versiones
@ -46,14 +48,14 @@ 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, 2 al escribir y 1 o 2 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 | Cambia el formato. Un lector rechaza una versión que no conoce (spec §22, §70) |
| Especificación | `datekeys.SpecVersion`, hoy `0.9`, y el tag `spec-v0.9` | Cambia el texto normativo. §76 del spec recoge cada cambio con su caso |
| 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.10`, y el tag `spec-v0.10` cuando se cierre | 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 `main`, aún sin publicar, cubre:
- la especificación 0.9: escribe el formato 2 de cápsula y lee los formatos 1 y 2;
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.10: escribe el formato 3 de cápsula y lee los formatos 1, 2 y 3;
- el perfil Quicknet pinneado, y cualquier perfil de los tres schemes de drand que soporta tlock;
- cifrado, inspección y apertura, y la CLI;
- todos los vectores y fixtures compartidos de [`testdata/`](testdata).
@ -68,15 +70,24 @@ El primer tag, v0.1.0, llegará cuando `go get` funcione desde una máquina limp
time_only: SEALED_CONTROL = age(tlock ronda R → CONTROL_CBOR)
time_and_key: SEALED_CONTROL = age(tlock ronda R → age(16 stanzas X25519 → CONTROL_CBOR))
CONTROL_CBOR = { header_binding = SHA-256(PRELUDE || PUBLIC_HEADER), I_PAYLOAD, extensiones, L, regla de relleno }
PAYLOAD_AGE = age(X25519 R_PAYLOAD → tus datos || ceros hasta P = regla(L)), en streaming
PAYLOAD_AGE = age(X25519 R_PAYLOAD → BODY || ceros hasta P = regla(L)), en streaming
BODY = AREA_LEN || SECURITY_LEN || HEAD_LEN (3 × uint32)
|| SECURITY_CBOR, ceros hasta AREA_LEN (512) firma y sello: vacío en la v0.10
|| HEAD_CBOR sal, comentario, autor declarado, los ficheros
|| los ficheros, uno tras otro
```
Es el formato 2, el que escribe `encrypt` (spec §22). Sus 16 stanzas llevan de
1 a 16 credenciales y un señuelo en cada hueco libre, en orden aleatorio, y su
payload se rellena hasta P: hasta la fecha nadie sabe cuántas credenciales hay
ni la longitud exacta L (spec §29.1, §39, §55.2). El formato 1, el de la
v0.8.2, lleva un stanza por credencial y no rellena; los lectores lo siguen
abriendo.
Es el formato 3, el que escribe `encrypt` (spec §22, §29.2). Sus 16 stanzas
llevan de 1 a 16 credenciales y un señuelo en cada hueco libre, en orden
aleatorio, y su payload se rellena hasta P: hasta la fecha nadie sabe cuántas
credenciales hay, ni la longitud exacta L, ni los nombres de los ficheros ni
cuántos son, más allá de lo que acota P (spec §29.1, §39, §55.2). Ya abierta,
da ficheros con sus rutas, tamaños, SHA-256 y fechas de modificación, un
comentario y un autor declarado, y los veredictos de su área de seguridad. El
formato 2, el de la v0.9, lleva un solo contenido; el formato 1, el de la
v0.8.2, lleva un stanza por credencial y no rellena. Los lectores siguen
abriendo los dos.
## CLI
@ -92,57 +103,85 @@ servidor o, en una red de confianza, define `GOINSECURE=g.activething.com`.
```bash
datekeys datekey resolve -at 2030-01-01T00:00:00Z
datekeys encrypt -at 2030-01-01T00:00:00Z -in carta.txt -out carta.dkc
datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -dkk carta.dkk -in carta.txt -out carta.dkc
datekeys encrypt -at 2030-01-01T00:00:00Z -padding bloque256 -in carta.txt -out carta.dkc
datekeys inspect -in carta.dkc
datekeys decrypt -in carta.dkc -out carta.txt -dkk carta.dkk
datekeys encrypt -at 2030-01-01T00:00:00Z -in fotos -in carta.txt -comment "Para Ana" -author "Juan" -out regalo.dkc
datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -dkk regalo.dkk -in fotos -out regalo.dkc
datekeys encrypt -at 2030-01-01T00:00:00Z -padding bloque256 -no-mtime -in carta.txt -out carta.dkc
datekeys inspect -in regalo.dkc
datekeys decrypt -in regalo.dkc -out regalo -dkk regalo.dkk
datekeys profile hash
datekeys version
```
`encrypt` nunca usa la red. Rellena el contenido con la regla reforzado, o con
bloque256 si se pide (spec §29.1). `inspect` ejecuta solo las comprobaciones
previas al desbloqueo (spec §63, pasos 1 a 8): nunca pide un release ni usa
secretos.
`decrypt` obtiene el release de relays públicos de drand, lo verifica
localmente y publica el plaintext solo cuando age lo ha autenticado entero.
Nunca se sobrescriben ficheros de salida.
`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
dejando fuera `.DS_Store`, `Thumbs.db`, `desktop.ini`, `._*` y `__MACOSX`. Una
ruta o un texto que incumple una regla de spec §29.5 o §29.6 se rechaza,
nombrando la regla y el carácter. Los ficheros conservan su fecha de
modificación salvo con `-no-mtime`. El contenido se rellena con la regla
reforzado, o con bloque256 si se pide (spec §29.1).
`inspect` ejecuta solo las comprobaciones previas al desbloqueo (spec §63,
pasos 1 a 8): nunca pide un release ni usa secretos.
`decrypt` obtiene el release de relays públicos de drand y lo verifica
localmente. Los ficheros de una cápsula de formato 3 van a la carpeta nueva
`-out`, preparados dentro de ella y movidos a su sitio solo cuando todas las
comprobaciones han pasado (spec §56); una cápsula con solo un comentario no
crea la carpeta. Muestra los veredictos al principio y al final, y el autor
declarado, el comentario y las rutas como texto del creador que nadie ha
comprobado, cada línea tras el prefijo `│ ` y nunca más ancha que el terminal
(spec §29.7). Los formatos 1 y 2 siguen dando un fichero. Nunca se
sobrescriben ficheros de salida.
## Librería
```go
reg, err := profile.Default() // perfil Quicknet pinneado, comprobado contra su profile_hash
// Cifrar: sin red, la ronda se resuelve localmente. El formato 2 sella la
// longitud del contenido antes que el contenido: src entrega exactamente
// Length bytes.
res, err := capsule.Encrypt(dst, src, capsule.EncryptOptions{
// EncryptFiles: sin red, la ronda se resuelve localmente. Lee cada fichero
// dos veces, y falla si un fichero cambia entre las dos lecturas.
res, err := capsule.EncryptFiles(dst, []capsule.Source{{
Path: "fotos/playa.jpg",
Size: info.Size(),
ModTime: info.ModTime(),
Open: func() (io.ReadCloser, error) { return os.Open(nombre) },
}}, capsule.EncryptOptions{
Profile: profile.Quicknet(),
UnlockAt: time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC),
Policy: capsule.TimeAndKey,
NewPortableKey: true, // res.PortableKey es la .dkk; se codifica con accesskey.Encode
Length: size,
Comment: "Para Ana",
Now: time.Now,
})
// Inspeccionar: pasos 1 a 8, sin red ni secretos.
in, err := capsule.Inspect(f, capsule.InspectOptions{Registry: reg})
// Abrir: pasos 1 a 18; el release se verifica localmente.
opened, err := capsule.Open(ctx, tmp, f, capsule.OpenOptions{
// Abrir: pasos 1 a 18; el release se verifica localmente. Una cápsula de
// formato 3 entrega sus ficheros a un Sink, que los publica en Commit, en el
// paso 18.
opened, err := capsule.Open(ctx, nil, f, capsule.OpenOptions{
Registry: reg,
Source: drand.New(),
AccessKey: key, // o Identities: []age.Identity{...}
Sink: sink,
Now: time.Now,
})
if errors.Is(err, datekeys.ErrReleaseUnavailable) { /* todavía no */ }
```
`Open` escribe el contenido en streaming, nunca el relleno; si falla, descarta
lo escrito y no lo presentes como válido (spec §56). `opened.Format` es el
formato de la cápsula: el formato 1 no oculta el número de credenciales ni la
longitud exacta del contenido, así que muéstralo (spec §70). Todo fallo del protocolo envuelve uno de los 17 errores normativos del
§69, así que `errors.Is` y `datekeys.Code(err)` lo identifican.
Un `Sink` recibe `Begin` con el head, `Create` para cada fichero y `Commit`
solo cuando el paso 17 ha pasado; tras cualquier fallo recibe `Abort`, y nada
de lo que recibió puede presentarse como válido (spec §56). `opened.Head`
lleva las rutas, los tamaños, los SHA-256 y las fechas de modificación, el
comentario y el autor declarado, y `opened.Verdicts.Lines()` los veredictos
que se muestran antes (spec §29.7). `Open` sin `Sink` se detiene justo tras
el paso 2 con `capsule.ErrSinkRequired`, un error del llamador sin código. Las
cápsulas de los formatos 1 y 2 siguen escribiendo su contenido en streaming en
`dst`, nunca el relleno. `opened.Format` es el formato de la cápsula: el
formato 1 no oculta el número de credenciales ni la longitud exacta del
contenido, así que muéstralo (spec §70). Todo fallo del protocolo envuelve uno
de los 18 errores normativos del §69, así que `errors.Is` y
`datekeys.Code(err)` lo identifican.
## Propiedades de seguridad y límites
@ -159,13 +198,19 @@ longitud exacta del contenido, así que muéstralo (spec §70). Todo fallo del p
un control nuevo para una cabecera pública; quién puede escribir cada parte
y desde qué paso queda vinculada es el modelo de confianza de spec §27 y
§55.1.
- **Privacidad de metadatos** (formato 2): hasta la fecha quedan ocultos la
longitud exacta del contenido y el número de credenciales; no la fecha, la
política de acceso, `capsule_id`, las extensiones de la cabecera ni el
tamaño con relleno P (spec §55.2).
- **Sin autoría**: `time_only` da coherencia interna, no prueba de quién creó
la cápsula, ni antes ni después de madurar; `time_and_key` añade una barrera
de acceso, no una firma (spec §36.1).
- **Privacidad de metadatos**: hasta la fecha quedan ocultos la longitud
exacta del contenido, el número de credenciales, y los nombres, tamaños y
número de los ficheros, más allá de lo que acota el tamaño con relleno P; no
la fecha, la política de acceso, `capsule_id` ni las extensiones de la
cabecera (spec §55.2).
- **Extracción segura**: las rutas del head no pueden salir de la carpeta,
colisionar en Windows, macOS o Linux, esconder caracteres ni cambiar la
dirección del texto; las reglas usan tablas fijas de Unicode 18.0.0, nunca
las de la plataforma (spec §29.5, §29.6).
- **Sin autoría**: el autor declarado y el comentario son texto del creador
que no prueba nada, y esta versión no comprueba firmas ni sellos: sus
veredictos solo lo dicen, y nunca deciden la apertura (spec §29.3, §29.7,
§36.1). `time_and_key` añade una barrera de acceso, no una firma.
- **Recuperar años después** exige el release histórico: de un relay drand que
aún lo sirva o de cualquier caché, verificado de nuevo localmente (spec §50).
@ -183,17 +228,20 @@ go test -tags integration ./capsule ./provider/drand # Quicknet en vivo
- `testdata/vectors`: vectores de `profile_hash`, fecha→ronda y `dk1_` (spec
§65, §66), vectores del perfil CBOR y de cada schema, vectores del relleno
(spec §29.1), el corpus de mutaciones exportado y un corpus diferencial de
las comprobaciones previas al desbloqueo; formatos en
(spec §29.1), las rutas, las claves de R7, los heads y las áreas de
seguridad del formato 3 (spec §67), el corpus de mutaciones exportado y un
corpus diferencial de las comprobaciones previas al desbloqueo; formatos en
[`testdata/README.md`](testdata/README.md).
- `testdata/fixtures`: fixtures oficiales `.dkc`/`.dkk` sobre rondas ya
publicadas, con la firma BLS embebida y todos los valores intermedios (spec
§67, §68); se descifran sin red. Siete son de formato 2; los cinco de formato
1, de la v0.8.2, se conservan por compatibilidad. Cada `.dkc` tiene
congelada su salida de `datekeys inspect -json`.
§67, §68); se descifran sin red. Nueve son de formato 3; las siete de
formato 2, de la v0.9, y las cinco de formato 1, de la v0.8.2, se conservan
por compatibilidad. Cada `.dkc` tiene congelada su salida de
`datekeys inspect -json`.
- `internal/testkit.Mutations`: las mutaciones del §64, las 33 de sus dos
primeras listas en cada formato y las 22 de la tercera, y 37 más, cada una
con su error y su paso exactos, comprobando además que los fallos previos al
primeras listas en cada formato, las 23 de la lista del formato 2 y las 47
de la del formato 3, y 40 más, cada una con su error y su paso exactos, o
sus veredictos cuando se abre, comprobando además que los fallos previos al
desbloqueo nunca provocan una petición de release; exportadas a
`testdata/vectors/mutations.json`.
- [`docs/traceability.md`](docs/traceability.md): sección del spec → código → test.

@ -1,7 +1,7 @@
# datekeys-go
Reference implementation in Go of the **DateKeys Protocol Specification
v0.9** ([`spec/`](spec/DateKeys_Protocol_Specification_v0.9.md)).
v0.10** ([`spec/`](spec/DateKeys_Protocol_Specification_v0.10.md)).
[Versión en español](README.es.md).
DateKeys encrypts data so that it can only be opened after a chosen instant.
@ -22,7 +22,8 @@ untrusted transports.
| DateKey: date → round, canonical `dk1_…` string | §14–§19 | [`datekey`](datekey) |
| Provider Profile, pinned Quicknet profile, `profile_hash` | §10–§13 | [`profile`](profile) |
| Release sources, local BLS verification, drand relays | §45–§52 | [`provider`](provider), [`provider/drand`](provider/drand) |
| DateKeyCap `.dkc`: `time_only` and `time_and_key` | §20–§39, §61–§63 | [`capsule`](capsule) |
| DateKeyCap `.dkc`: `time_only` and `time_and_key`, the files of format 3 | §20–§39, §61–§63 | [`capsule`](capsule) |
| Paths and texts of a head, with the Unicode 18.0.0 and best-fit tables | §29.5, §29.6 | [`internal/pathrule`](internal/pathrule) |
| DateKeys Access Key `.dkk` | §40–§44 | [`accesskey`](accesskey) |
| Extensions | §54 | [`extension`](extension) |
| Deterministic CBOR | §58 | [`codec`](codec) |
@ -37,7 +38,9 @@ stanza rules of the protocol inside the age identities, so that a file is never
accepted just because age could unwrap a key.
Not implemented on purpose: the Release API server and queue, storage and
delivery, concrete extensions, and the TypeScript client (plan §2).
delivery, concrete extensions, the author signature and the time seal, which
the specification reserves for later versions, and the TypeScript client
(plan §2).
## Versions
@ -45,14 +48,14 @@ 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, 2 when written and 1 or 2 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 format changes. A reader rejects a version it does not know (spec §22, §70) |
| Specification | `datekeys.SpecVersion`, today `0.9`, and the tag `spec-v0.9` | The normative text changes. Spec §76 records each change with its case |
| 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.10`, and the tag `spec-v0.10` once it is closed | 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 on `main`, not yet released, covers:
- specification 0.9: it writes capsule format 2 and reads formats 1 and 2;
Each release states what it covers, here and in the [CHANGELOG](CHANGELOG.md). The code of this branch, not yet released, covers:
- specification 0.10: it writes capsule format 3 and reads formats 1, 2 and 3;
- the pinned Quicknet profile, and any profile on the three drand schemes that tlock supports;
- encryption, inspection and opening, and the CLI;
- every shared vector and fixture of [`testdata/`](testdata).
@ -67,15 +70,23 @@ The first tag, v0.1.0, comes once `go get` works from a clean machine.
time_only: SEALED_CONTROL = age(tlock round R → CONTROL_CBOR)
time_and_key: SEALED_CONTROL = age(tlock round R → age(16 X25519 stanzas → CONTROL_CBOR))
CONTROL_CBOR = { header_binding = SHA-256(PRELUDE || PUBLIC_HEADER), I_PAYLOAD, extensions, L, padding rule }
PAYLOAD_AGE = age(X25519 R_PAYLOAD → your data || zeros up to P = rule(L)), streamed
PAYLOAD_AGE = age(X25519 R_PAYLOAD → BODY || zeros up to P = rule(L)), streamed
BODY = AREA_LEN || SECURITY_LEN || HEAD_LEN (3 × uint32)
|| SECURITY_CBOR, zeros up to AREA_LEN (512) signature and seal: empty in v0.10
|| HEAD_CBOR salt, comment, declared author, the files
|| the files, one after another
```
This is format 2, the one `encrypt` writes (spec §22). Its 16 stanzas hold
from 1 to 16 credentials and a dummy in each slot left, in a random order, and
its payload is padded to P, so that until the unlock date nobody learns the
number of credentials or the exact length L (spec §29.1, §39, §55.2). Format
1, that of v0.8.2, has one stanza per credential and no padding; readers
still open it.
This is format 3, the one `encrypt` writes (spec §22, §29.2). Its 16 stanzas
hold from 1 to 16 credentials and a dummy in each slot left, in a random
order, and its payload is padded to P, so that until the unlock date nobody
learns the number of credentials, the exact length L, the names of the files
or how many there are, beyond what P bounds (spec §29.1, §39, §55.2). Once
open, it gives files with their paths, sizes, SHA-256 and modification times,
a comment and a declared author, and the verdicts of its security area.
Format 2, that of v0.9, holds one content; format 1, that of v0.8.2, has one
stanza per credential and no padding. Readers still open both.
## CLI
@ -91,57 +102,82 @@ certificate or, on a trusted network, set `GOINSECURE=g.activething.com`.
```bash
datekeys datekey resolve -at 2030-01-01T00:00:00Z
datekeys encrypt -at 2030-01-01T00:00:00Z -in letter.txt -out letter.dkc
datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -dkk letter.dkk -in letter.txt -out letter.dkc
datekeys encrypt -at 2030-01-01T00:00:00Z -padding bloque256 -in letter.txt -out letter.dkc
datekeys inspect -in letter.dkc
datekeys decrypt -in letter.dkc -out letter.txt -dkk letter.dkk
datekeys encrypt -at 2030-01-01T00:00:00Z -in photos -in letter.txt -comment "For Ana" -author "Juan" -out gift.dkc
datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -dkk gift.dkk -in photos -out gift.dkc
datekeys encrypt -at 2030-01-01T00:00:00Z -padding bloque256 -no-mtime -in letter.txt -out letter.dkc
datekeys inspect -in gift.dkc
datekeys decrypt -in gift.dkc -out gift -dkk gift.dkk
datekeys profile hash
datekeys version
```
`encrypt` never touches the network. It pads the content with the rule
reforzado, or bloque256 if asked (spec §29.1). `inspect` runs only the
pre-unlock checks (spec §63 steps 1–8): it never requests a release and never
uses a secret.
`decrypt` fetches the release from public drand relays, verifies it locally
and publishes the plaintext only after age authenticated all of it. Outputs are
never overwritten.
`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
out `.DS_Store`, `Thumbs.db`, `desktop.ini`, `._*` and `__MACOSX`. A path or
a text that breaks a rule of spec §29.5 or §29.6 is refused, naming the rule
and the character. The files keep their modification times unless
`-no-mtime`. The content is padded with the rule reforzado, or bloque256 if
asked (spec §29.1).
`inspect` runs only the pre-unlock checks (spec §63 steps 1–8): it never
requests a release and never uses a secret.
`decrypt` fetches the release from public drand relays and verifies it
locally. The files of a format 3 capsule go to the new folder `-out`, staged
inside it and moved into place only after every check passed (spec §56); a
capsule with only a comment creates no folder. It shows the verdicts first
and last, and the declared author, the comment and the paths as text of the
creator that nobody has checked, each line behind a `│ ` prefix and never
wider than the terminal (spec §29.7). Formats 1 and 2 still give one file.
Outputs are never overwritten.
## Library
```go
reg, err := profile.Default() // pinned Quicknet profile, checked against its profile_hash
// Encrypt: no network, the round is resolved locally. Format 2 seals the
// content length before the content: src must deliver exactly Length bytes.
res, err := capsule.Encrypt(dst, src, capsule.EncryptOptions{
// EncryptFiles: no network, the round is resolved locally. It reads each
// file twice, and fails if a file changes between the two readings.
res, err := capsule.EncryptFiles(dst, []capsule.Source{{
Path: "photos/beach.jpg",
Size: info.Size(),
ModTime: info.ModTime(),
Open: func() (io.ReadCloser, error) { return os.Open(name) },
}}, capsule.EncryptOptions{
Profile: profile.Quicknet(),
UnlockAt: time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC),
Policy: capsule.TimeAndKey,
NewPortableKey: true, // res.PortableKey is the .dkk; encode it with accesskey.Encode
Length: size,
Comment: "For Ana",
Now: time.Now,
})
// Inspect: steps 1–8, no network, no secrets.
in, err := capsule.Inspect(f, capsule.InspectOptions{Registry: reg})
// Open: steps 1–18; the release is verified locally.
opened, err := capsule.Open(ctx, tmp, f, capsule.OpenOptions{
// Open: steps 1–18; the release is verified locally. A format 3 capsule
// gives its files to a Sink, which publishes them in Commit, at step 18.
opened, err := capsule.Open(ctx, nil, f, capsule.OpenOptions{
Registry: reg,
Source: drand.New(),
AccessKey: key, // or Identities: []age.Identity{...}
Sink: sink,
Now: time.Now,
})
if errors.Is(err, datekeys.ErrReleaseUnavailable) { /* not yet */ }
```
`Open` streams the content and never the padding; on error, discard what was
written and never present it as valid (spec §56). `opened.Format` is the
format of the capsule: format 1 hides neither the number of credentials nor
the exact length of the content, so show it (spec §70).
Every protocol failure wraps one of the 17 normative errors of spec §69, so
`errors.Is` and `datekeys.Code(err)` identify it.
A `Sink` gets `Begin` with the head, `Create` for each file and `Commit` only
when step 17 has passed; after any failure it gets `Abort`, and nothing it
received may be presented as valid (spec §56). `opened.Head` holds the paths,
sizes, SHA-256 and modification times, the comment and the declared author,
and `opened.Verdicts.Lines()` the verdicts to show before them (spec §29.7).
`Open` without a `Sink` stops right after step 2 with `capsule.ErrSinkRequired`,
a caller error with no code. Capsules of formats 1 and 2 still stream their
content to `dst`, never the padding. `opened.Format` is the format of the
capsule: format 1 hides neither the number of credentials nor the exact
length of the content, so show it (spec §70). Every protocol failure wraps
one of the 18 normative errors of spec §69, so `errors.Is` and
`datekeys.Code(err)` identify it.
## Security properties and limits
@ -157,13 +193,18 @@ Every protocol failure wraps one of the 17 normative errors of spec §69, so
published anyone can compute the time file key, and anyone can seal a new
control for a public header; who can write each part, and from which step
it is bound, is the trust model of spec §27 and §55.1.
- **Metadata privacy** (format 2): until the date, the exact length of the
content and the number of credentials stay hidden; the date, the access
policy, `capsule_id`, the header extensions and the padded size P do not
(spec §55.2).
- **No authorship**: `time_only` gives internal coherence, not proof of who
created a capsule, before or after it matures; `time_and_key` adds an access
barrier, not a signature (spec §36.1).
- **Metadata privacy**: until the date, the exact length of the content, the
number of credentials, and the names, sizes and number of the files stay
hidden, beyond what the padded size P bounds; the date, the access policy,
`capsule_id` and the header extensions do not (spec §55.2).
- **Safe extraction**: the paths of a head cannot leave the folder, collide
on Windows, macOS or Linux, hide characters or change the direction of the
text; the rules use fixed Unicode 18.0.0 tables, never those of the
platform (spec §29.5, §29.6).
- **No authorship**: the declared author and the comment are text of the
creator that proves nothing, and this version checks no signature and no
seal: its verdicts only say so, and never decide the opening (spec §29.3,
§29.7, §36.1). `time_and_key` adds an access barrier, not a signature.
- **Recovery years later** needs the historical release: from a drand relay
that still serves it or from any cache, re-verified locally (spec §50).
@ -180,18 +221,20 @@ go test -tags integration ./capsule ./provider/drand # live Quicknet
```
- `testdata/vectors`: profile hash, date→round and `dk1_` vectors (spec §65, §66),
CBOR profile and schema vectors, padding vectors (spec §29.1), the exported
mutation corpus and a differential corpus of the pre-unlock checks; formats in
[`testdata/README.md`](testdata/README.md).
CBOR profile and schema vectors, padding vectors (spec §29.1), the paths, the
keys of R7, the heads and the security areas of format 3 (spec §67), the
exported mutation corpus and a differential corpus of the pre-unlock checks;
formats in [`testdata/README.md`](testdata/README.md).
- `testdata/fixtures`: official `.dkc`/`.dkk` fixtures over published rounds,
with the BLS signature embedded and every intermediate value (spec §67, §68);
they decrypt offline. Seven are of format 2; the five of format 1, from
v0.8.2, are kept for compatibility. Each `.dkc` has its frozen
`datekeys inspect -json` output.
they decrypt offline. Nine are of format 3; the seven of format 2, from
v0.9, and the five of format 1, from v0.8.2, are kept for compatibility.
Each `.dkc` has its frozen `datekeys inspect -json` output.
- `internal/testkit.Mutations`: the mutations of spec §64, the 33 of its first
two lists in each format and the 22 of its third list, and 37 more, each
with its exact error and step, and a check that pre-unlock failures never
cause a release request; exported to `testdata/vectors/mutations.json`.
two lists in each format, the 23 of the list of format 2 and the 47 of the
list of format 3, and 40 more, each with its exact error and step, or its
verdicts when it opens, and a check that pre-unlock failures never cause a
release request; exported to `testdata/vectors/mutations.json`.
- [`docs/traceability.md`](docs/traceability.md): spec section → code → test.
- [`spec/datekeys.cddl`](spec/datekeys.cddl): CBOR schemas.

@ -19,9 +19,10 @@ The module is pre-1.0 (`v0.x`). Only the latest `v0.x` release receives fixes.
## Scope and assumptions
In scope: every rule of the DateKeys Protocol Specification v0.9 this module
In scope: every rule of the DateKeys Protocol Specification v0.10 this module
implements (see `docs/traceability.md`), the CLI, and the handling of
untrusted input (`.dkc`, `.dkk`, relay responses).
untrusted input (`.dkc`, `.dkk`, relay responses), among it the paths and
texts of a format 3 head, which the CLI writes to disk and to a terminal.
The protocol's own limits, which are not vulnerabilities of this module:
@ -29,6 +30,9 @@ The protocol's own limits, which are not vulnerabilities of this module:
to harvest-now, decrypt-later (spec §7.7, §53).
- Time confidentiality depends on drand's threshold assumption (spec §7.6).
- `time_only` and `time_and_key` do not authenticate the creator (spec §36.1).
In format 3 the declared author, the comment and the modification times are
text of the creator that proves nothing, and this version checks no author
signature and no time seal: its verdicts say so (spec §29.3, §29.7, §55.1).
- Opening a mature capsule years later needs the historical release (spec §50).
- A compromised device can copy plaintext or secrets (spec §7.8).
- Go cannot guarantee that secrets are erased from memory. The module wipes

@ -1,4 +1,4 @@
# Traceability: DateKeys Protocol Specification v0.9 ↔ datekeys-go
# Traceability: DateKeys Protocol Specification v0.10 ↔ datekeys-go
This table maps every normative section of the specification to the code that
implements it and to the tests that exercise it. It is updated in the same
@ -7,7 +7,7 @@ reviewer together with the specification, the fixtures and the mutation corpus
(plan §10).
Paths are relative to the repository root. `§` numbers refer to
`spec/DateKeys_Protocol_Specification_v0.9.md`.
`spec/DateKeys_Protocol_Specification_v0.10.md`.
## Section map
@ -31,28 +31,35 @@ Paths are relative to the repository root. `§` numbers refer to
| 19 | `dk1_` canonicality; steps 1 to 3 `ERR_DATEKEY_INVALID`, step 6 `ERR_DATEKEY_NON_CANONICAL`; step 1 accepts either Base64 alphabet, padding and non-zero trailing bits but no other character (CR and LF included); step 2 one RFC 8259 JSON object, no byte order mark; JSON numbers by their exact decimal value; round in 1..2^53−1 without the profile | `datekey.Parse` (`decodeBase64`, which rejects CR and LF before the Go decoders, `parseJSON`, which rejects invalid UTF-8 before `encoding/json` can replace it, `jsonUint`) | `datekey.TestGoldenDK1Vectors`, `TestReadingRules`, `TestNumberSpellings`, `FuzzParse`; mutation *non-canonical dk1_ JSON*; `testdata/vectors/dk1.json` (*byte order mark*, *line feed inside the Base64*, *carriage return and line feed after the Base64*, *version 1.0000000000000001: its exact value, not a double*, *invalid UTF-8 in a member a repeated name overwrites*) |
| 20 | File extensions and magic | magic checks in `capsule.ParsePrelude`, `accesskey.Decode` | mutation *a .dkk offered as a .dkc*; `accesskey.TestDecodeRejects` *a .dkc* |
| 21 | `capsule_id`: exactly 16 bytes from a CSPRNG (MUST) | `capsule.Encrypt` (16 bytes from `crypto/rand`), `capsule.DecodeHeader` | `capsule.TestPortableKeysAreNeverReused` |
| 22 | `.dkc` framing; `VERSION` is the capsule format, 1 or 2, which fixes the schema version of CONTROL_CBOR, the stanzas of INNER_ACCESS_AGE and the padding of the payload; `PUBLIC_HEADER_LEN` in 1..1 MiB, `SEALED_CONTROL_LEN` in 1..64 MiB; PAYLOAD_AGE to EOF, at least an age header | `capsule.Format` (`Format1`, `Format2`), `capsule.Prelude` (`Format`), `capsule.ParsePrelude` | `capsule.TestFormatDispatch`, `TestFormatRelabel`, `TestFrameLengthLowerBounds`, `FuzzParsePrelude`; mutations *version changed* (`VERSION` 3, in both formats), *flags != 0*, *reserved != 0*, *magic*, length limits, and the relabelings of the third list of §64 |
| 23 | PRELUDE; order of the checks of steps 1 and 2, the version being the format, 1 or 2; section bytes present at steps 3 and 5 | `Prelude.Bytes`, `capsule.ParsePrelude`, `capsule.Inspect` | `capsule.TestConformanceFixtures`, `TestFrameLengthLowerBounds`, `TestPrecedenceAcrossSteps`, `TestFormatDispatch`; `testdata/vectors/inspect_differential.json` |
| 22 | `.dkc` framing; `VERSION` is the capsule format, 1, 2 or 3, which fixes the schema version of CONTROL_CBOR, the stanzas of INNER_ACCESS_AGE and the padding of the payload; `PUBLIC_HEADER_LEN` in 1..1 MiB, `SEALED_CONTROL_LEN` in 1..64 MiB; PAYLOAD_AGE to EOF, at least an age header | `capsule.Format` (`Format1`, `Format2`, `Format3`), `capsule.Prelude` (`Format`), `capsule.ParsePrelude` | `capsule.TestFormatDispatch`, `TestFormatRelabel`, `TestFrameLengthLowerBounds`, `FuzzParsePrelude`; mutations *version changed* (`VERSION` 4, in the three formats), *flags != 0*, *reserved != 0*, *magic*, length limits, and the relabelings of the lists of formats 2 and 3 of §64 |
| 23 | PRELUDE; order of the checks of steps 1 and 2, the version being the format, 1, 2 or 3; section bytes present at steps 3 and 5 | `Prelude.Bytes`, `capsule.ParsePrelude`, `capsule.Inspect` | `capsule.TestConformanceFixtures`, `TestFrameLengthLowerBounds`, `TestPrecedenceAcrossSteps`, `TestFormatDispatch`; `testdata/vectors/inspect_differential.json` |
| 24 | PUBLIC_HEADER; keys 5 and 6 optional, 1 to 64 extensions each | `capsule.Header`, `EncodeHeader`, `DecodeHeader` (hand-written `headerWire` encode and decode; CDDL checked before the DateKey) | `capsule.TestConformanceFixtures` (exact extension data), `TestDecodeHeaderRejects`, `TestDecodeMapStructure`, `TestDecodeHeaderReportsTheCDDLFirst`, `FuzzDecodeHeader`, `FuzzEncodeImpliesDecode`; mutations *header schema version changed*, *unknown key in PUBLIC_HEADER* |
| 25 | Declared access policy | `capsule.Policy`; `capsule.DecodeHeader` (the value read, up to 2^53−1, must be 0 or 1 before any narrowing); `capsule.Open` step 12 | `capsule.TestDecodeMapStructure` (2, 255, 256, 257, 2^32, 2^53−256 and others), `FuzzDecodeHeader` (seeds 256, 257, 2^32); mutations *access_policy=… with … structure* (four cases), *undefined access_policy*, *access_policy 256 / 257 with a consistent header_binding* |
| 26 | Header binding; a mismatch at step 15 is `ERR_HEADER_BINDING` | `capsule.HeaderBinding`; `capsule.Open` step 15 | `capsule.TestConformanceFixtures`; mutation *PUBLIC_HEADER_A + SEALED_CONTROL_B* |
| 27 | Pre-unlock validation; the age header MAC authenticates only against whoever does not know the file key (anyone recomputes that of OUTER_TIME_AGE once the round is published), and `header_binding` gives internal coherence, not authorship or a date (§55.1) | `capsule.Inspect` (steps 1–8), `agewrap.Stanzas` probe | `capsule.TestMutationCorpus` (no release request for any pre-unlock failure), `FuzzInspect`, `TestTrustModel`; the U and stanza body mutations of §64, whose header MAC is recomputed |
| 28 | Three age files | `capsule.Encrypt`, `capsule.Open` | `capsule.TestEncryptRoundTripBothPolicies` |
| 28 | Three age files | `capsule.EncryptFiles`, `capsule.Encrypt`, `capsule.Open` | `capsule.TestEncryptRoundTripBothPolicies`, `TestEncryptFilesRoundTrip`, `TestEncryptFilesTimeAndKey` |
| 28.1 | Age file format: the C2SP header grammar (at least one stanza); malformed OUTER_TIME_AGE and PAYLOAD_AGE headers `ERR_INTEGRITY` (steps 5 and 6, or 11 and 17), a malformed INNER_ACCESS_AGE `ERR_POLICY_STRUCTURE_MISMATCH` (step 12); wrong stanza count or type `ERR_POLICY_STRUCTURE_MISMATCH`; in format 2, a plaintext of PAYLOAD_AGE of a length other than P, or whose padding is not zero, `ERR_INTEGRITY` at step 17 | `agewrap.Stanzas` (the header parser of `filippo.io/age`), `capsule.classify`, `capsule.Open` step 12, `capsule.checkPadding` | `capsule.TestMalformedAgeHeaders`, `TestPaddingChecksAtStep17`, `agewrap.TestStanzasProbe`, `FuzzStanzas`; the 10 header-without-stanzas cases of `testdata/vectors/inspect_differential.json` (5 at step 5, 5 at step 6) |
| 29 | PAYLOAD_AGE; `I_PAYLOAD` from a CSPRNG, new for each capsule and never derived; in format 2 its plaintext is the content followed by zeros up to P | `capsule.Encrypt` (`age.GenerateX25519Identity`, `writeContent`); `agewrap.PayloadIdentity`, `agewrap.CheckPayloadStanzas` | `agewrap.TestPayloadIdentityStrictness`; `capsule.TestPayloadIdentityReuse`, `TestEncryptWritesFormat2`; mutation *extra stanza in PAYLOAD_AGE* |
| 29.1 | Padding of the payload in format 2: codes 1 (`bloque256`) and 2 (`reforzado`, Padmé), no rule without padding, exact integer arithmetic of more than 32 bits, L_MAX = 2^53 − 2^46; the reader checks that the plaintext is P bytes with a zero padding, and delivers the first L | `capsule.Padding` (`Bloque256`, `Reforzado`), `capsule.PaddedLength`, `capsule.MaxPayloadLength`, `capsule.PayloadAgeLength`; `capsule.Open` steps 16 to 18 (`checkPadding`); `EncryptOptions.Padding`, reforzado by default; `datekeys encrypt -padding` | `capsule.TestPaddingRules`, `TestPaddingChecksAtStep17`, `TestPaddingAcrossChunks`, `TestCheckPadding`, `TestEncryptWritesFormat2`; `testdata/vectors/padding.json`, generated by `internal/testkit.PaddingVectors` and checked against a Padmé in `math/big` (`internal/testkit.TestVectorFilesAreCurrent`); fixtures `format2_time_only`, `format2_time_only_bloque256`, `format2_empty_payload`; the padding mutations of the third list of §64 |
| 29 | PAYLOAD_AGE; `I_PAYLOAD` from a CSPRNG, new for each capsule and never derived; in formats 2 and 3 its plaintext is the content, BODY in format 3, followed by zeros up to P | `capsule.EncryptFiles` and `capsule.Encrypt`, through the sealer of `capsule/encrypt.go` (`age.GenerateX25519Identity`, `copyExactly`, `writeZeros`); `agewrap.PayloadIdentity`, `agewrap.CheckPayloadStanzas` | `agewrap.TestPayloadIdentityStrictness`; `capsule.TestPayloadIdentityReuse`, `TestEncryptWritesFormat2`; mutation *extra stanza in PAYLOAD_AGE* |
| 29.1 | Padding of the payload in formats 2 and 3: codes 1 (`bloque256`) and 2 (`reforzado`, Padmé), no rule without padding, exact integer arithmetic of more than 32 bits, L_MAX = 2^53 − 2^46; the reader checks that the plaintext is P bytes with a zero padding, and delivers the first L | `capsule.Padding` (`Bloque256`, `Reforzado`), `capsule.PaddedLength`, `capsule.MaxPayloadLength`, `capsule.PayloadAgeLength`; `capsule.Open` steps 16 to 18 (`checkPadding`); `EncryptOptions.Padding`, reforzado by default; `datekeys encrypt -padding` | `capsule.TestPaddingRules`, `TestPaddingChecksAtStep17`, `TestPaddingAcrossChunks`, `TestCheckPadding`, `TestEncryptWritesFormat2`; `testdata/vectors/padding.json`, generated by `internal/testkit.PaddingVectors` and checked against a Padmé in `math/big` (`internal/testkit.TestVectorFilesAreCurrent`); fixtures `format2_time_only`, `format2_time_only_bloque256`, `format2_empty_payload`, `format3_bloque256`; the padding mutations of the lists of formats 2 and 3 of §64 |
| 29.2 | Content of format 3: BODY, a frame of `AREA_LEN`, `SECURITY_LEN` and `HEAD_LEN`, the security area, 512 bytes when written and a multiple of 512 from 512 to 65536 when read, zero after SECURITY_CBOR, then the head and CONTENT; any violation of the frame, L < 12 included, is `ERR_INTEGRITY` at step 17 | `capsule.BodyFrame`, `ParseBodyFrame`, `CheckArea`, `AreaLen`, `MaxAreaLen`, `MaxHeadLen`; `capsule.Open` step 17 (`openBody`); `capsule.EncryptFiles` | `capsule.TestBodyFrame`, `TestOpen3`, `TestOpen3Substeps`; fixtures `format3_*`, `format3_area_1024`; mutations *AREA_LEN …*, *SECURITY_LEN …*, *HEAD_LEN …*, *12 + AREA_LEN + HEAD_LEN = L + 1*, *L < 12: 11*, *a byte of the area not zero* |
| 29.3 | `security`: the outer map of version 1, keys 2 and 3 byte strings that hold the author signature and the seal encoded apart; layers 2 and 3 without a code; it never decides the opening; writers of this version write it empty, 22 bytes | `capsule.EncodeSecurity`, `EncodeSecurityWith`, `EvaluateSecurity`, `EncodeAuthorSignature`, `EncodeSeal` | `capsule.TestSecurityVerdicts`; `testdata/vectors/security.json` (`internal/testkit.SecurityVectors`, `TestFormat3VectorFiles`); fixtures `format3_security_v2`, `format3_signature_unsupported`, `format3_seal_unsupported`; the three mutations of §64 that open with their verdicts |
| 29.4 | Head: version 1, a salt of 32 bytes, the comment and the declared author, 1 to 65535 files in the order of R8 with their layout, SHA-256 and mtime up to 9999, at most 16 MiB; layer 2, layer 3 with R1 and R8, then layer 4 in key order, `ERR_HEAD_INVALID`, and the critical extensions | `capsule.Head`, `File`, `EncodeHead`, `DecodeHead` (`decodeHead`, `checkHeadFields`), `CheckHeadEnd`; `extension.Head` | `capsule.TestHeadRoundTrip`, `TestDecodeHeadLayers`, `TestOpen3Substeps`; `testdata/vectors/head_schema.json` (`internal/testkit.HeadSchemaVectors`); the head mutations of the list of format 3 of §64 |
| 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`; `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, F1, S0, S1 and S2 with the texts of the table; the presentation: the verdicts first and last, the labels of the text of the creator, TABs expanded, pieces of at most W − 3 columns behind `│ ` with the width counted by excess, and warnings of risky names | `capsule.Verdict`, `Verdicts.Lines`; `cmd/datekeys.present` (`writeCreator`, `pieces`, `expandTabs`, `outputWidth`, `termWidth`, `risks`) | `capsule.TestSecurityVerdicts`; `cmd/datekeys.TestPresent`, `TestEncryptDecryptRoundTrip`, `TestDecryptFormat3Fixtures` |
| 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` |
| 31 | CONTROL_CBOR; its schema version is the format of its capsule, another being `ERR_UNSUPPORTED_VERSION` at step 14; keys 4 and 5 optional; keys 6, `payload_length` (8 bytes, at most L_MAX), and 7, `padding` (1 or 2), required in version 2 and not defined in version 1, `ERR_NON_CANONICAL_CBOR`; a version 2 control without extensions is 103 bytes; extension entry rules (`extension_id` valid UTF-8 of at least 1 byte), elements in strictly ascending bytewise order of `extension_id` (§54) | `capsule.Control` (`PayloadLength`, `Padding`), `EncodeControl` and `DecodeControl`, which take the format (hand-written `controlWire` encode and decode); `extension` | `capsule.TestConformanceFixtures` (exact extension data), `TestDecodeControlRejects`, `TestControlLengthIsConstant`, `TestDecodeMapStructure`, `FuzzDecodeControl` (both formats), `FuzzEncodeImpliesDecode`; the `control_cbor` block of `testdata/vectors/cbor.json`, with `format`; mutations *unknown critical CONTROL_CBOR extension*, and those of keys 6 and 7 in the third list of §64 |
| 31 | CONTROL_CBOR; its schema version is the format of its capsule, another being `ERR_UNSUPPORTED_VERSION` at step 14; keys 4 and 5 optional; keys 6, `payload_length` (8 bytes, at most L_MAX), and 7, `padding` (1 or 2), required in versions 2 and 3 and not defined in version 1, `ERR_NON_CANONICAL_CBOR`; a control of version 2 or 3 without extensions is 103 bytes; extension entry rules (`extension_id` valid UTF-8 of at least 1 byte), elements in strictly ascending bytewise order of `extension_id` (§54) | `capsule.Control` (`PayloadLength`, `Padding`), `EncodeControl` and `DecodeControl`, which take the format (hand-written `controlWire` encode and decode); `extension` | `capsule.TestConformanceFixtures` (exact extension data), `TestDecodeControlRejects`, `TestControlLengthIsConstant`, `TestDecodeMapStructure`, `FuzzDecodeControl` (the three formats), `FuzzEncodeImpliesDecode`; the `control_cbor` block of `testdata/vectors/cbor.json`, with `format`; mutations *unknown critical CONTROL_CBOR extension*, and those of keys 6 and 7 in the third list of §64 |
| 32 | `time_only` | `capsule.Encrypt`; `agewrap.TimeRecipient` | fixtures `time_only*`, `empty_payload`; `capsule.TestInteropTleOpensSealedControl` (`-tags interop`, official `tle` CLI) |
| 33 | `time_and_key`: X25519 stanzas only, one per recipient, one or more in format 1 and exactly 16 in format 2 | `capsule.Encrypt` (`seal`); `agewrap.AccessIdentity`, `agewrap.AccessSlots` | fixtures `time_and_key_*`, `format2_time_and_key_*`; `capsule.TestEncryptRoundTripBothPolicies`, `TestInnerHasSixteenStanzas`; `agewrap.TestAccessSlots` |
| 33 | `time_and_key`: X25519 stanzas only, one per recipient, one or more in format 1 and exactly 16 in formats 2 and 3 | the sealer of `capsule/encrypt.go` (`seal`); `agewrap.AccessIdentity`, `agewrap.AccessSlots` | fixtures `time_and_key_*`, `format2_time_and_key_*`, `format3_time_and_key_portable`; `capsule.TestEncryptRoundTripBothPolicies`, `TestInnerHasSixteenStanzas`; `agewrap.TestAccessSlots` |
| 34 | SEALED_CONTROL | `capsule.Encrypt`; `capsule.Open` step 11 | `capsule.TestConformanceFixtures` |
| 35 | tlock strict mode; exactly two stanza arguments compared as strings (§63 step 8) | `agewrap.TimeRecipient`, `agewrap.TimeIdentity`, `agewrap.CheckTimeStanzas` (pinned parameters only, exact stanza arguments) | `agewrap.TestTimeIdentityStrictness`, `TestInteroperabilityWithTlockLibrary`, `TestTimeIdentityRelease`; `capsule.TestTlockStanzaArgumentComparison` |
| 36 | Policy ↔ structure; `time_only`: a plaintext that starts with the age intro line is a mismatch, any other is read as CONTROL_CBOR at step 14; `time_and_key`: a malformed age header, or two stanzas with one argument after the type and the same argument (a repeated X25519 ephemeral share), is a mismatch, and so is, in format 2, a number of stanzas other than 16 | `capsule.Open` step 12 (`looksLikeAge`, `agewrap.Stanzas`), `agewrap.CheckAccessStanzas` (with the slots of the format) | mutations *access_policy=…* (four cases), *non-X25519 stanza in INNER_ACCESS_AGE*; `capsule.TestMalformedAgeHeaders`; `agewrap.TestMalformedX25519Stanzas` (*repeated stanza*), `TestAccessSlots`; `capsule.TestFormat1Compatibility`; mutations *INNER_ACCESS_AGE with 15 stanzas*, *INNER_ACCESS_AGE with 17 stanzas* |
| 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` |
| 39 | Recipients of INNER_ACCESS_AGE: in format 2 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` (`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) |
| 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` |
| 41 | `.dkk` BODY_CBOR | `AccessKey.MarshalBody`, `accesskey.DecodeBody` (hand-written `bodyWire` encode and decode) | `accesskey.TestFixtures`, `TestDecodeBodyStructure` |
| 42 | `credential_id` | `capsule.Encrypt` (16 bytes from `crypto/rand`) | `capsule.TestPortableKeysAreNeverReused` |
@ -71,29 +78,29 @@ Paths are relative to the repository root. `§` numbers refer to
| 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) |
| 55.2 | Privacy: what a capsule reveals before and after the date and what format 2 hides; the reader reports the format | no code of its own: format 2 (rows 22, 29.1, 31 and 39); `Inspection.Prelude.Format`, `Opened.Format`, `datekeys inspect` (`format`), and `datekeys decrypt`, which warns about format 1 | `capsule.TestControlLengthIsConstant`, `TestSealedControlLength`, `TestFormatDispatch`; `cmd/datekeys.TestDecryptFixtures`, `TestEncryptDecryptRoundTrip`, `TestInspectJSONGoldens` |
| 56 | Atomic plaintext output; in format 2 the reader never writes the padding and never presents the content as valid before step 17 ends, and a reader that writes in streaming signals the error of step 17 so that what it wrote is discarded | `capsule.Open` contract (the first L bytes only, `checkPadding`); `cmd/datekeys.writeAtomic` | `cmd/datekeys.TestOutputNotPublishedOnFailureOrOverwrite`, `TestDecryptFailuresLeaveNothing`; `capsule.TestPaddingChecksAtStep17` (at most the content is written), `TestPaddingAcrossChunks` |
| 57 | Parser limits, MUST for encoders and decoders; frame lengths of 0 or above the limits and objects above their frame → `ERR_INTEGRITY` on encode and decode, a field of the wrong CBOR type → `ERR_NON_CANONICAL_CBOR` before its own code, CDDL violations → `ERR_NON_CANONICAL_CBOR`, Provider Profile names and public key → `ERR_UNKNOWN_PROFILE`; implementation limits not normative; L and P are not frame lengths, and nothing is reserved according to them | `capsule.MaxPublicHeaderLen` (`ParsePrelude`, `EncodeHeader`, `DecodeHeader`), `MaxSealedControlLen` (`ParsePrelude`, `Encrypt`), `accesskey.MaxBodyLen` (`Decode`, `DecodeBody`, `MarshalBody`), `extension.MaxExtensions`, `MaxDataLen`; the bounds each schema passes to `codec.Decoder` (`Map`, `Array`, `Uint`, `Bstr`, `Text`), with lengths checked against the remaining input before any copy; `profile.Validate` | `capsule.TestDeclaredLengthIsNotAllocated`; mutations *…_LEN above the limit*, *65 extensions in one array*; `capsule.TestHeaderLimit`, `TestHugeExtensionArraysAreRejected`; `accesskey.TestDecodeRejects` *body length above the limit*, `TestBodyLimit`; `profile.TestValidateRejectsTamperedProfiles`, `TestIntegerRanges`; `codec.TestDecoderRejects` |
| 56 | Atomic plaintext output; in format 2 the reader never writes the padding and never presents the content as valid before step 17 ends, and a reader that writes in streaming signals the error of step 17 so that what it wrote is discarded; in format 3 no file, head or verdict is presented before step 18, and a reader that writes to a file system stages the files in a place of its own, moves them at step 18 and removes them on failure, inside a folder it creates and that did not exist, following no link and overwriting nothing, and creates nothing for a capsule without files | `capsule.Open` contract (the first L bytes only, `checkPadding`); `capsule.Sink`, `ErrSinkRequired`; `cmd/datekeys.writeAtomic`, `cmd/datekeys.dirSink` (`os.Mkdir`, `os.OpenRoot`, `O_EXCL`, `Root.Rename`, `RemoveAll`) | `cmd/datekeys.TestOutputNotPublishedOnFailureOrOverwrite`, `TestDecryptFailuresLeaveNothing`, `TestDecryptFormat3LeavesNothing`, `TestDecryptFormat3Fixtures`; `capsule.TestPaddingChecksAtStep17` (at most the content is written), `TestPaddingAcrossChunks`, `TestOpen3Substeps`, `TestOpen3SinkFailures` |
| 57 | Parser limits, MUST for encoders and decoders; frame lengths of 0 or above the limits and objects above their frame → `ERR_INTEGRITY` on encode and decode, a field of the wrong CBOR type → `ERR_NON_CANONICAL_CBOR` before its own code, CDDL violations → `ERR_NON_CANONICAL_CBOR`, Provider Profile names and public key → `ERR_UNKNOWN_PROFILE`; implementation limits not normative; L and P are not frame lengths, and nothing is reserved according to them, nor according to `AREA_LEN`, `SECURITY_LEN`, `HEAD_LEN`, `size`, `start` or `end` in format 3 | `capsule.MaxPublicHeaderLen` (`ParsePrelude`, `EncodeHeader`, `DecodeHeader`), `MaxSealedControlLen` (`ParsePrelude`, `Encrypt`), `accesskey.MaxBodyLen` (`Decode`, `DecodeBody`, `MarshalBody`), `extension.MaxExtensions`, `MaxDataLen`; the bounds each schema passes to `codec.Decoder` (`Map`, `Array`, `Uint`, `Bstr`, `Text`), with lengths checked against the remaining input before any copy; `profile.Validate` | `capsule.TestDeclaredLengthIsNotAllocated`, `TestOpen3DeclaredLengths` (the reads of BODY, `readN`); mutations *…_LEN above the limit*, *65 extensions in one array*; `capsule.TestHeaderLimit`, `TestHugeExtensionArraysAreRejected`; `accesskey.TestDecodeRejects` *body length above the limit*, `TestBodyLimit`; `profile.TestValidateRejectsTamperedProfiles`, `TestIntegerRanges`; `codec.TestDecoderRejects` |
| 58 | Canonical CBOR and the protocol's CBOR profile (major types 0, 2, 3, 4, 5; unsigned integer keys; integers ≤ 2^53−1) | `codec`, without reflection or dependencies: `Encoder` (shortest heads, valid UTF-8, nil byte strings as empty, never `null`; a sticky first error, which `Fail` lets a schema encoder record), `Decoder` (strict cursor: profile major types only, shortest heads, definite lengths, strictly ascending unsigned keys per map, valid UTF-8, no trailing bytes), `Unmarshal` (re-encoding comparison), `Walk` (the profile only, for vectors, fuzzing and diagnostics); `codec.MaxSafeUint`; the profile covers the head of extension data only | `codec.TestDecoderAccepts`, `TestDecoderRejects` (negative integer, tag, float, simple values, indefinite lengths, non-shortest heads, text key, key order, UTF-8), `TestUnmarshalRejectsNonCanonical`, `TestWalk`, `TestEncoderAndWalkAgreeWithAReference` (against `internal/cbortest`), `TestSharedVectors`, `FuzzDecoder`, `FuzzUnmarshal`, `FuzzWalk`, `FuzzEncodeImpliesWalk`; `extension.TestData`; `internal/testkit.TestSchemaVectors`; `testdata/vectors/cbor.json` (generic vectors walked with `codec.Walk`, and one block per schema: Provider Profile, PUBLIC_HEADER, CONTROL_CBOR, `.dkk` body, `verification_metadata`, extension), generated by `internal/testkit.CBORVectors` |
| 58.1 | Absent optional fields are omitted; `h''` and `null` never stand for absence | `extension.Canonical` (nil for empty), `extension.DecodeArray` (empty array, empty data), re-encoding check, `accesskey` verification map | `codec` *empty optional array present*; `accesskey` *empty extension array*, *empty verification map*, *null verification*, *empty data*, *null data*; mutation *empty extension data (h'')* |
| 59 | Supply-chain security | pinned `go.mod`/`go.sum`, `.gitea/workflows`, `scripts/check.sh`, `.goreleaser.yaml`, `SECURITY.md` | CI jobs `vuln`, `sbom`, `verify` |
| 60 | Conceptual Go interfaces | `provider.ReleaseSource`, `provider.Verify`, `datekey.Resolve`, `datekey.RoundTime` | — |
| 61 | `time_only` encryption flow, format 2 | `capsule.Encrypt` | `capsule.TestEncryptRoundTripBothPolicies`, `TestEncryptWritesFormat2`, `TestSealedControlLength` |
| 62 | `time_and_key` encryption flow, format 2: 16 recipients, and the SEALED_CONTROL_LEN of an INNER_ACCESS_AGE of 16 stanzas | `capsule.Encrypt` | `capsule.TestEncryptRoundTripBothPolicies`, `TestPortableKeysAreNeverReused`, `TestSealedControlLength` |
| 62.1 | Writer rules: format 2 only; 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. SHOULD: code 2 by default, self-checks, wiping | `capsule.Encrypt` (`EncryptOptions.Length` and `Padding`, `accessRecipients`, `fillSlots`, `writeContent`, `selfCheckHeader`, `selfCheckControl`, `selfCheckInner`, `selfCheckPayload`); `agewrap.CheckX25519Recipient`; the only writer of format 1 is `internal/testkit.Build`, a generator of test vectors (§70) | `capsule.TestEncryptRejectsInvalidOptions`, `TestCredentialBounds`, `TestEncryptSourceLength`, `TestEncryptSelfCheck`, `TestSealedControlLength`, `TestPayloadIdentityReuse`, `TestStanzaOrderIsUniform`, `TestDummyRecipients`, `TestPortableKeysAreNeverReused` |
| 63 | Decryption flow; step 2 accepts the formats 1 and 2, and the steps after it apply the rules of the format: in format 2, 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; 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; `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` | `capsule.TestConformanceFixtures` (stage by stage), `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`: 4380 deterministic mutations of the 12 fixtures, 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`) |
| 64 | Mandatory mutation tests: the first two lists in both formats, the third in format 2 | `internal/testkit.Mutations` (the corpus: `specMutations` for each format, `furtherMutations`, `format2Mutations`), `internal/testkit.MutationCorpus` (its export); the format 2 cases derived without randomness, by sealing the fixtures again with their known file keys and nonces (`internal/testkit/reseal.go`) | `capsule.TestMutationCorpus`: the 88 listed mutations, 33 in each format and the 22 of the third list, plus 37 more, built afresh; `capsule.TestExportedMutationCorpus`: `testdata/vectors/mutations.json`, the same 125 cases as frozen data (capsule, `.dkk`, identities, recorded release, clock, registry, known extensions), replayed with the recorded error and step; `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` |
| 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 512 bytes with the empty security, 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. 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) | `capsule.TestEncryptFilesRejects`, `TestEncryptFilesChangedFile`, `TestEncryptIsForTestVectors`, `TestEncryptFilesHeadCritical`, `TestEncryptRejectsInvalidOptions`, `TestCredentialBounds`, `TestEncryptSourceLength`, `TestEncryptSelfCheck`, `TestSealedControlLength`, `TestPayloadIdentityReuse`, `TestStanzaOrderIsUniform`, `TestDummyRecipients`, `TestPortableKeysAreNeverReused`; `cmd/datekeys.TestEncryptRefusesPaths` |
| 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` | `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`) |
| 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, three of whose cases open with their verdicts | `internal/testkit.Mutations` (the corpus: `specMutations` for each format, `furtherMutations`, `format2Mutations`, `format3Mutations` with `LoadedFixture.WithBody`), `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`) | `capsule.TestMutationCorpus`: the 169 listed mutations, 33 in each format, the 23 of the list of format 2 and the 47 of that of format 3, plus 40 more, built afresh; `capsule.TestExportedMutationCorpus`: `testdata/vectors/mutations.json`, the same 209 cases as frozen data (capsule, `.dkk`, identities, recorded release, 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` |
| 65 | Quicknet vectors | `internal/testkit.RoundVectors` | `datekey.TestGoldenRoundVectors` |
| 66 | `dk1_` vectors | `internal/testkit.DK1Vectors` | `datekey.TestGoldenDK1Vectors` |
| 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 records give the format, L, the code, P and the stanza each credential opens; the padding vectors | `testdata/fixtures/*.dkc` + `*.json`, `internal/testkit/genfixtures`, which never regenerates a format 1 fixture and recomputes the derived fields of every record; the frozen `datekeys inspect -json` output of each, `*.inspect.json`; `testdata/vectors/padding.json`; formats in `testdata/README.md` | `capsule.TestConformanceFixtures`, `TestPaddingAcrossChunks` (a capsule generated at run time); `cmd/datekeys.TestInspectJSONGoldens`, `TestDecryptFixtures` |
| 68 | `.dkk` vectors, with the exact extension data; one carries an extension with data, and two accompany a format 2 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` | `accesskey.TestFixtures`, `TestFixtureWithExtension`; `capsule.TestAccessKeyFixtureWithExtension` |
| 69 | Normative errors, including `ERR_EXTENSION_DATA_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` |
| 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, and an unsupported signature and seal; 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; the padding vectors, and those of paths, keys of R7, heads and security | `testdata/fixtures/*.dkc` + `*.json`, `internal/testkit/genfixtures`, which never regenerates a format 1 fixture and recomputes the derived fields of every record; the frozen `datekeys inspect -json` output of each, `*.inspect.json`; `testdata/vectors/padding.json`; formats in `testdata/README.md` | `capsule.TestConformanceFixtures` (`checkBody3`), `TestPaddingAcrossChunks` (a capsule generated at run time); `internal/testkit.TestFormat3VectorFiles`; `cmd/datekeys.TestInspectJSONGoldens`, `TestDecryptFixtures`, `TestDecryptFormat3Fixtures` |
| 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 both formats and opens format 1 with the semantics of v0.8.2; an implementation that writes capsules writes format 2, and only a generator of test vectors writes format 1; 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`) | 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` | mutations; `capsule.TestFormatDispatch`, `TestFormatRelabel`, `TestFormat1Compatibility`; `codec.TestPeek`, `TestCheckSchema`, `TestCheckSchemaVersionForms`, `FuzzPeek`; `capsule.TestDecodeSchemaVersion`; `datekeys.TestSpecVersionNamesTheSpecification`, `TestVersion`; `cmd/datekeys.TestVersion` |
| 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`) | 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` | mutations; `capsule.TestFormatDispatch`, `TestFormatRelabel`, `TestFormat1Compatibility`; `codec.TestPeek`, `TestCheckSchema`, `TestCheckSchemaVersionForms`, `FuzzPeek`; `capsule.TestDecodeSchemaVersion`; `datekeys.TestSpecVersionNamesTheSpecification`, `TestVersion`; `cmd/datekeys.TestVersion` |
| 71 | Profile registry | `profile.Decode` + `profile.NewRegistry` with pinned hashes | `profile.TestRegistry` |
| 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 | `extension.Registry`, `extension.Set`, `extension.DataValidator`, `extension.Placement` (optional: a `Registry` without it knows its extensions in every object and array); self-checks in `capsule.Encrypt` and `accesskey.MarshalBody`, which take no `Registry`: the application writes each extension only where it is registered | `capsule.TestKnownCriticalExtensions`, `TestUnusableNoncriticalExtensions`, `TestExtensionPlacement`, `TestNestedDataSealsAndOpens`, `FuzzEncodeImpliesDecode`; `extension.TestPlacement` |
| 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 both 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 | `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 | 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 |
| 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 | `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 | 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 |
## Error mapping
@ -106,7 +113,7 @@ decides which code is reported.
|---|---|---|
| Bytes that are not the deterministic encoding of a valid schema instance: malformed CBOR, non-canonical encoding (including a map head or type tag head not in its shortest form before the schema version), unknown key, missing key, wrong type (also for a field with a code of its own), `null`, wrong type tag (key 0, or one longer than `codec.MaxTypeTagLen` bytes), a schema version that is missing, not the second key, not an unsigned integer, not in its shortest form or above 2^53−1, wrong field length, undefined `access_policy` (any value other than 0 and 1), empty optional array or map, extension rules including the order of `extension_id` | `ERR_NON_CANONICAL_CBOR` | §54, §57, §58, §69.1 |
| Schema version other than 1, read as the second key, after a type tag within the profile, as an unsigned integer in its shortest form of at most 2^53−1; whatever follows it | `ERR_UNSUPPORTED_VERSION` | §69.1, §70 |
| Truncated framing, length fields of 0 or beyond the §57 limits, an object above its §57 frame on encode or decode, data after BODY_CBOR, a malformed OUTER_TIME_AGE or PAYLOAD_AGE (an age header against the C2SP grammar, without stanzas, or beyond the parser limits of `filippo.io/age`: 1024 stanzas, 128 arguments, 2 MiB), a tlock stanza body of a length other than \|U\| + 32, with a U that is not the canonical encoding of a point of the key group (§12.2) or is the identity, or that fails the IBE check r·G == U (step 11), a malformed X25519 stanza (steps 13 and 17), a failed header MAC, a truncated or modified STREAM, trailing data after PAYLOAD_AGE, a PAYLOAD_AGE that I_PAYLOAD cannot open (step 17) | `ERR_INTEGRITY` | §22, §23, §28.1, §40, §57, §63 steps 11, 13 and 17, §74 |
| Truncated framing, length fields of 0 or beyond the §57 limits, an object above its §57 frame on encode or decode, data after BODY_CBOR, a malformed OUTER_TIME_AGE or PAYLOAD_AGE (an age header against the C2SP grammar, without stanzas, or beyond the parser limits of `filippo.io/age`: 1024 stanzas, 128 arguments, 2 MiB), a tlock stanza body of a length other than \|U\| + 32, with a U that is not the canonical encoding of a point of the key group (§12.2) or is the identity, or that fails the IBE check r·G == U (step 11), a malformed X25519 stanza (steps 13 and 17), a failed header MAC, a truncated or modified STREAM, trailing data after PAYLOAD_AGE, a PAYLOAD_AGE that I_PAYLOAD cannot open (step 17); in format 3 at step 17, a violation of the frame of BODY or of its area, files that do not fill CONTENT, or a file whose SHA-256 is not that of the head | `ERR_INTEGRITY` | §22, §23, §28.1, §40, §57, §63 steps 11, 13 and 17, §74 |
| Stanza count or type violations in OUTER_TIME_AGE, PAYLOAD_AGE or INNER_ACCESS_AGE in a header that parses; a repeated X25519 ephemeral share in INNER_ACCESS_AGE (step 12); an offered identity that unwraps more than one INNER_ACCESS_AGE stanza, whatever the order of the identities (step 13); a malformed INNER_ACCESS_AGE, including one beyond the parser limits, or none, under `time_and_key`; an age intro line under `time_only`; a tlock stanza without exactly two arguments | `ERR_POLICY_STRUCTURE_MISMATCH` | §28.1, §36, §63 steps 8, 12 and 13 |
| tlock stanza round argument not exactly the canonical decimal DateKey round (steps 8 and 11); a release for another round supplied directly, checked before its signature (step 10) | `ERR_ROUND_MISMATCH` | §17, §63 steps 8, 10 and 11 |
| A release supplied directly whose signature is not the canonical encoding of a point of the signature group of the scheme (§12.2), is the identity, or does not verify under the pinned key for the DateKey round | `ERR_RELEASE_INVALID` | §12.2, §51, §63 step 10 |
@ -116,6 +123,7 @@ decides which code is reported.
| Unknown `access_type`, wrong material length, `.dkk` for another `capsule_id`, `capsule_digest` mismatch (step 9.a); no offered identity is a recipient of INNER_ACCESS_AGE (step 13) | `ERR_ACCESS_INVALID` | §57, §63 steps 9 and 13 |
| `time_and_key` and no credential offered (nil identities are none), before the clock is consulted | `ERR_ACCESS_REQUIRED` | §63 step 9 |
| Round time not reached yet (no request is made), or no source delivered a release, whatever the cause: a network source discarded every response for breaking the rules of step 10, or the error of a source carries another code or none, of which only the text is kept | `ERR_RELEASE_UNAVAILABLE` | §63 step 9 |
| A head of format 3 that is well encoded but whose comment, declared author, paths, layout or tree break a rule of layer 4 (step 17) | `ERR_HEAD_INVALID` | §29.4–§29.6, §69.1 |
| An unknown critical extension, or a known one in an object or array it is not registered for, before any invalid data (steps 4, 9.a and 14) | `ERR_EXTENSION_CRITICAL_UNKNOWN` | §54, §63, §69.1, §72 |
## Implementation decisions
@ -223,6 +231,18 @@ provisional (§74). None changes the protocol semantics.
5. **Clock injection.** No library package reads the wall clock; `Encrypt`
and `Open` require a `Now` function, and `Open` never requests a release
for a round whose time has not been reached (§63 step 9).
6. **The files of format 3.** `Open` hands them to a `capsule.Sink`, which
gets `Abort` once after any failure that follows a successful `Begin`,
a failure of `Commit` included; a `Begin` that fails cleans up after
itself. A failure of the `Sink` is the caller's own error with
`ERR_INTEGRITY`, as a failure of `dst` is in formats 1 and 2.
`EncryptFiles` sorts its `Source` list in the byte order of the paths,
and refuses a path given twice before the rules of the tree.
7. **The width of the terminal.** The CLI asks it through `syscall`
(`TIOCGWINSZ` on Unix, `GetConsoleScreenBufferInfo` on Windows) rather
than through a new dependency; elsewhere, and for any output that is not
a terminal, W is 80 (§29.7). The folder of `decrypt` keeps mode 0700 and
its files 0600, and the extracted files get their recorded mtime.
`testdata/README.md` documents the formats of the vectors and corpora and
points to these sections for the rules that decide each verdict.

240
testdata/README.md vendored

@ -1,7 +1,7 @@
# DateKeys test data
Official vectors, fixtures and corpora of the DateKeys Protocol Specification
v0.9, generated by the reference implementation. Another implementation
v0.10, generated by the reference implementation. Another implementation
consumes them as they are: this file documents every format, so that no Go code
has to be read. The rules that decide each verdict are in the specification;
this file points to them, and states only what belongs to the files
@ -14,7 +14,7 @@ go run ./internal/testkit/genfixtures -out testdata
regenerates everything except the `.dkc` and `.dkk` fixtures, which are
generated once and frozen (spec §67). The records of each fixture
(`<name>.json`, `<name>.dkk.json`, `<name>.inspect.json`) are recomputed from
its frozen bytes, so the format 1 fixtures of v0.8.2 carry `"spec": "0.9"` and
its frozen bytes, so the fixtures of v0.8.2 and v0.9 carry `"spec": "0.10"` and
the fields added since. The local gate (`scripts/check.sh`) and CI run it and
fail if any committed file changes: every file below is exactly what the
implementation computes today.
@ -28,29 +28,33 @@ Conventions for every file:
pre-unlock checks (`datekeys inspect`, `capsule.Inspect`): no network, no
secret.
- The `spec` field names the version of the specification.
- `format` is a capsule format, the `VERSION` of the PRELUDE (spec §22): 1 or
2. A writer produces format 2 only; format 1, that of v0.8.2, is still read
(spec §70).
- `format` is a capsule format, the `VERSION` of the PRELUDE (spec §22): 1, 2
or 3. A writer produces format 3 only; formats 1 and 2, those of v0.8.2 and
v0.9, are still read (spec §70).
| File | Content | Spec |
|---|---|---|
| `vectors/profile_quicknet.json` | Quicknet Provider Profile: its canonical CBOR and `profile_hash` | §11, §12 |
| `vectors/quicknet_rounds.json` | date → round resolution | §15, §16, §65 |
| `vectors/dk1.json` | canonical `dk1_` strings, and rejected encodings with their code | §18, §19, §66 |
| `vectors/cbor.json` | the CBOR profile, and one block of vectors per schema, CONTROL_CBOR in both formats | §58, CDDL |
| `vectors/cbor.json` | the CBOR profile, and one block of vectors per schema, CONTROL_CBOR in the three formats | §58, CDDL |
| `vectors/tlock_ibe.json` | H2 of the tlock IBE: the serialization of an element of GT | §63 step 11 |
| `vectors/padding.json` | the padding of format 2: P for each content length L, and the length of PAYLOAD_AGE | §29.1 |
| `vectors/mutations.json` | the mutation corpus: the 89 mutations of §64 and further cases | §63, §64 |
| `vectors/inspect_differential.json` | 4380 mutations of the fixtures with the verdict of steps 1 to 8 | §63 |
| `vectors/padding.json` | the padding of formats 2 and 3: P for each content length L, and the length of PAYLOAD_AGE | §29.1 |
| `vectors/paths.json` | the paths of a format 3 head: the rules of one entry, and those of the paths of a head | §29.5 |
| `vectors/path_fold.json` | the key of R7 of segments, and their NFD | §29.5, §29.5.1 |
| `vectors/head_schema.json` | heads of format 3 and the result of decoding them | §29.4 to §29.6, §69.1 |
| `vectors/security.json` | security areas of format 3 and their verdicts | §29.3, §29.7 |
| `vectors/mutations.json` | the mutation corpus: the 169 mutations of §64 and further cases | §63, §64 |
| `vectors/inspect_differential.json` | 5110 mutations of fourteen fixtures with the verdict of steps 1 to 8 | §63 |
| `fixtures/<name>.dkc`, `<name>.json` | official capsules and every intermediate value | §67 |
| `fixtures/<name>.dkk`, `<name>.dkk.json` | official access keys | §68 |
| `fixtures/<name>.plaintext` | the content of each capsule, what the reader delivers: in format 2, without its padding | §67 |
| `fixtures/<name>.plaintext` | the content of each capsule: what the reader delivers in formats 1 and 2, without the padding of format 2, and in format 3 BODY, whose files its record lays out | §67 |
| `fixtures/<name>.inspect.json` | the exact output of `datekeys inspect -json` for each `.dkc` | §63 |
There are twelve official capsules. Five are in format 1, the fixtures of
There are twenty-one official capsules. Five are in format 1, the fixtures of
v0.8.2, kept for compatibility: `time_only`, `time_only_extensions`,
`time_and_key_portable`, `time_and_key_recipients` and `empty_payload`. Seven
are in format 2:
are in format 2, the fixtures of v0.9, kept for compatibility too:
| Fixture | Policy | Credentials | L | Padding code | P |
|---|---|---|---|---|---|
@ -68,6 +72,30 @@ extension and a noncritical CONTROL_CBOR extension. The release that opens each
capsule, a published Quicknet signature, is in its `<name>.json`, so they all
decrypt offline.
Nine are in format 3. Their plaintext file is BODY, L bytes: the frame, the
security area, the head and the files (spec §29.2).
| Fixture | Policy | Files | Comment | L | Padding code | P | Area | Verdicts |
|---|---|---|---|---|---|---|---|---|
| `format3_single` | `time_only` | 1, `nota.txt`, with mtime | — | 659 | 2 | 768 | 512 | F0, S0 |
| `format3_tree` | `time_only` | 5 in three folders, one of 80000 bytes, one without mtime | two lines, and a declared author | 84078 | 2 | 86016 | 512 | F0, S0 |
| `format3_comment_only` | `time_only` | — | two lines, the second with a TAB, and a declared author | 636 | 2 | 768 | 512 | F0, S0 |
| `format3_bloque256` | `time_only` | 1 of 20000 bytes | — | 20644 | 1 | 20736 | 512 | F0, S0 |
| `format3_time_and_key_portable` | `time_and_key`, a portable `.dkk` and 15 dummies | 1 | — | 688 | 2 | 768 | 512 | F0, S0 |
| `format3_area_1024` | `time_only` | 1 | — | 1171 | 2 | 1280 | 1024 | F0, S0 |
| `format3_security_v2` | `time_only` | 1 | — | 659 | 2 | 768 | 512 | X |
| `format3_signature_unsupported` | `time_only` | 1 | — | 659 | 2 | 768 | 512 | F1, S0 |
| `format3_seal_unsupported` | `time_only` | 1 | — | 659 | 2 | 768 | 512 | F1, S1 |
The first five are what `capsule.EncryptFiles` writes. The other four only a
generator of test vectors may write (spec §62.1 rule 13): an area larger than
the 512 bytes of this version, which a reader accepts, a security map of
version 2, which a reader of this version cannot read, an author signature of
`alg` 1 with a random key of 32 bytes and a random signature of 64, and that
with a seal of `seal_type` 1 and a random token of 32 bytes. None of them has
a verdict that stops the opening. The BODY of `format3_tree` is over two STREAM
chunks, with its head in the first.
## Edited files
`mutations.json` and `inspect_differential.json` give each mutated `.dkc` as
@ -92,7 +120,7 @@ edits of a base file, not as its full bytes:
```json
{
"spec": "0.9",
"spec": "0.10",
"walk": { "max_depth": 3, "max_len": 64 },
"accept": [ { "name": "uint 2^53 eight bytes", "hex": "1b0020000000000000", "value": "9007199254740992" } ],
"reject": [ { "name": "tag", "hex": "c101", "error": "ERR_NON_CANONICAL_CBOR" } ],
@ -142,9 +170,9 @@ Each vector is one encoded object:
- `control_cbor`: CONTROL_CBOR (§31), decoded for the capsule format given
by `format`, 1 when the field is absent: the schema version must equal it
(another is `ERR_UNSUPPORTED_VERSION`, layer 2), and keys 6,
`payload_length`, and 7, `padding`, are required in format 2 and not
defined in format 1. The names of the format 2 vectors start with
"format 2: ".
`payload_length`, and 7, `padding`, are required in formats 2 and 3 and
not defined in format 1. The names of the vectors of formats 2 and 3
start with "format 2: " and "format 3: ".
- `dkk_body`: BODY_CBOR of a `.dkk` (§41), without the 12-byte DKK1
prelude.
- `block` names the schema the vector exercises: the same as `schema`, or
@ -175,7 +203,10 @@ defined, and in format 2: the minimal control of 103 bytes (L = 0),
unsigned integer or with a length not in its shortest form, `padding` 1
(valid), 0, 3, 257 and as a byte string, a missing key 6 or 7, keys 6 and 7
out of order, an unknown key 8, and schema versions 1 and 3
(`ERR_UNSUPPORTED_VERSION`).
(`ERR_UNSUPPORTED_VERSION`); and in format 3: the minimal control, both
extension arrays, `payload_length` 84078 with `padding` 1 (valid),
`payload_length` L_MAX + 1, a missing key 6 or 7, schema version 3 without
keys 6 and 7, and schema versions 1, 2 and 4 (`ERR_UNSUPPORTED_VERSION`).
#### Implementation limits
@ -197,7 +228,7 @@ the flow.
```json
{
"spec": "0.9",
"spec": "0.10",
"description": "…",
"vectors": [
{ "name": "H2(e(G1, G2)), the generators of G1 and G2", "g1": "97f1…", "g2": "93e0…", "gt": "0f41…", "h2": "cb87319f24560b5231579a09ad79f12e" }
@ -224,13 +255,13 @@ writes them, give `0118eea9d5971745f71e3c94926f1717` and another FK_TIME.
## `vectors/padding.json`
The padding of the payload of a format 2 capsule, spec §29.1: for each content
length L, the length P of the plaintext of PAYLOAD_AGE with each code, and the
The padding of the payload of a capsule of format 2 or 3, spec §29.1, where L
is the length of BODY in format 3: for each content length L, the length P of the plaintext of PAYLOAD_AGE with each code, and the
length of PAYLOAD_AGE.
```json
{
"spec": "0.9",
"spec": "0.10",
"l_max": 8936830510563328,
"vectors": [
{ "l": 78000, "bloque256": 78080, "reforzado": 79872, "payload_age_bloque256": 78296, "payload_age_reforzado": 80088, "e": 16, "s": 5, "last_bits": 11 }
@ -257,6 +288,98 @@ include both sides of the boundaries where 32-bit arithmetic breaks
(2^31 − 2^25, 2^32 − 2^26, 2^32) and 2^49 − 1, the first L for which a
floating-point logarithm gives an E one too large (P does not change).
## `vectors/paths.json` and `vectors/path_fold.json`
The rules of the paths of a format 3 head, spec §29.5, with the tables of
§29.5.1. Both files name the tables they were computed with:
`"unicode_version": "18.0.0"` and `tables_digest`, the SHA-256 of the
canonical text of the generated tables, which a second implementation that
generates them from the same files can recompute.
```json
{
"paths": [ { "name": "TAB", "path": "a\tb", "result": "R4: segment 1: control U+0009" } ],
"trees": [ { "name": "A.txt and a.txt", "paths": ["A.txt", "a.txt"], "result": "ERR_HEAD_INVALID", "detail": "R7: path 2 collides with path 1 in segment 1" } ]
}
```
- `paths`: one path and the rules of one entry, R2, R3, R4, R4b, R5, R6, R6b,
R6c and R10, in that order; `result` is `ok` or the violation of the first
rule that fails, as every implementation must word it: the rule, the
segment when it is about one, and the character as `U+` and at least four
upper-case hexadecimal digits, never the text itself.
- `trees`: the paths of a head, each a file of 0 bytes, in the order given,
and the result of decoding that head (see `head_schema.json`): R1 and R8
in layer 3 (`ERR_NON_CANONICAL_CBOR`), then each entry and R7 and R9 over
the tree in layer 4 (`ERR_HEAD_INVALID`, with its `detail`).
Among them: U+00A0, accepted, and U+3000, refused by R6c, at both ends of a
segment; the best-fit projections, the full-width forms of '/', '\', ':'
and '.', and CON.txt in full-width forms; 8.3 aliases, `~1` alone included;
unassigned code points and noncharacters; U+206A to U+206F, the tags and other
ignorables outside the whitelist; a dot followed by ZWJ and a segment of ZWJ
alone; 127 and 85 times U+0390; U+F03A; `.datekeys-x` at the first level and
at another; U+FF5E and U+1F600 in both orders; `ab` with and without ZWNJ;
U+00BF, U+00A7 and U+2665, accepted; VS16 after U+2764 and after `a`; ZWJ at
the start, at the end and twice; the rainbow flag, accepted, and the flag of
Scotland, refused; and `["b/..", "a"]`, an error of R8 before R3.
`path_fold.json` gives, for each `segment`, its `nfd` and its `key` of R7,
NFD(fold(NFD(s'))) with s' the segment without ZWNJ, ZWJ, VS15 and VS16:
among them the entries F of CaseFolding, the dotless i, the Kelvin and
Angstrom signs, Cherokee, Hangul, the canonical order of two marks, and the
whitelist dropped before NFD.
## `vectors/head_schema.json`
Heads of format 3, HEAD_CBOR, and the result of decoding them with no
extension known (spec §29.4 to §29.6): `ok`, or the code of the first
failing layer of §69.1. A head is decoded alone: the frame of BODY that
bounds it and the files it lays out are not checked here (spec §63 steps
17.2 and 17.5).
```json
{ "name": "a comment with U+202E", "hex": "a4006d…", "result": "ERR_HEAD_INVALID", "detail": "comment: text: bidirectional control U+202E" }
```
- Layer 2: the type tag and version 1; another type tag is
`ERR_NON_CANONICAL_CBOR`, another version `ERR_UNSUPPORTED_VERSION`.
- Layer 3: the CDDL with its sizes and ranges, R1 and R8, and the equality
with the re-encoding: `ERR_NON_CANONICAL_CBOR`.
- Layer 4, in key order: the comment and the declared author (§29.6), then
each file with R2 to R6c, R4b and R10 and its layout, then R7 and R9 over
the tree, all `ERR_HEAD_INVALID`, and then the critical extensions,
`ERR_EXTENSION_CRITICAL_UNKNOWN` with no extension known.
- `detail`, for `ERR_HEAD_INVALID` only: the violation as every
implementation must word it, `comment: text: …`, `declared author:
text: …`, `file N: …` for an entry, counted from 1, or the violation of the
tree.
Besides the cases of each layer, it has a comment with tags that spell a
text, one with VS16 after a letter and one with a run of variation
selectors, one with VS17, one with CR LF, and the precedence of a comment and
a path that both break, and of a path that breaks and an unknown critical
extension.
## `vectors/security.json`
Security areas of format 3, SECURITY_CBOR exactly as its SECURITY_LEN bytes,
and their verdicts (spec §29.3, §29.7), which never stop the opening. This
version implements no `alg` and no `seal_type`, so a reader of it reaches X,
F0, F1, S0, S1 and S2 only.
```json
{ "name": "a signature of alg 0, and the seal intact", "hex": "a4006f…", "signature": "F1", "seal": "S1" }
```
X stands for both, when the outer map fails its layer 2 or 3: key 2 that is
not a byte string, or an empty one, an unknown key 4, a byte more after the
map, version 2, another type tag, keys out of order, an array. Otherwise the
signature and the seal are evaluated apart, and the first row of the table of
§29.7 that holds decides: `alg` 0, an empty key or content that is not CBOR
give F1 with the seal intact, and a seal that breaks its schema gives S2 even
with an unknown `seal_type`, which is read only from a seal that meets it.
## `vectors/mutations.json`
The mutation corpus of spec §64, as frozen data. Each case is a `.dkc`, what
@ -279,12 +402,15 @@ reading flow (`capsule.Open`, §63) must fail.
```
- `name`: unique, stable.
- `spec`: true for the 89 mutations listed in spec §64, false for the further
- `spec`: true for the 169 mutations listed in spec §64, false for the further
cases of the reference. The cases come in this order: the 33 mutations of
the first two lists of §64 on the format 1 fixtures (cases 1 to 33), 32
further cases (34 to 65), the same 33 mutations on the format 2 fixtures,
named "format 2: …" (66 to 98), the 23 of the third list (99 to 121), and 5
further cases (122 to 126).
named "format 2: …" (66 to 98), the 23 of the list of format 2 (99 to 121),
5 further cases (122 to 126), the same 33 on the format 3 fixtures, named
"format 3: …" (127 to 159), the 47 of the list of format 3 (160 to 206),
and 3 further cases (207 to 209). A line of the lists of §64 with several
values, such as "AREA_LEN 0, 511, 513 o 66048", is one case for each.
- `dkc`: the capsule, as edits of a fixture (see above). The reader gets it as a
seekable file, so that the `capsule_digest` of an offered `.dkk` is checked
before any release request (spec §63 step 9.a).
@ -320,11 +446,15 @@ reading flow (`capsule.Open`, §63) must fail.
of steps 1 to 8 and of step 9 before the request (9.a to 9.c).
- `frozen`: the capsule was built once with age randomness; its bytes are kept
and never regenerated. These cases have no `base`.
- `error`, `step`: the expected code and the step of §63 that fails.
- `error`, `step`: the expected code and the step of §63 that fails. For a
capsule that opens, `error` is `ok`, `step` is 0 and `verdicts` holds the
verdicts of its security area, `signature` and `seal`, with the `lines`
that show them (spec §29.7): the three cases of security of the list of
format 3.
Every case reproduces offline: the recorded release stands in for the network.
A reader that implements only steps 1 to 8 can replay every case whose `step` is
at most 8: 44 cases, 26 of them from §64. Steps 1 to 8 are summarised in "The
at most 8: 57 cases, 39 of them from §64. Steps 1 to 8 are summarised in "The
checks of steps 1 to 8" below.
### Credentials and the release: step 9
@ -371,9 +501,9 @@ Ten cases of §64, in each format, test the canonical point encoding of spec
In format 2 the same ten edit `format2_time_only.dkc` and its release, and the
x + p case has a frozen format 2 capsule for round 1004.
### Format 2: the third list of §64
### Format 2: its list of §64
The 23 cases of the third list of §64 test what format 2 adds:
The 23 cases of the list of format 2 of §64 test what format 2 adds:
- the format against the rest of the capsule, with another `VERSION`, a
public byte (§22, §76): a format 1 `time_and_key` capsule of one stanza
@ -394,9 +524,43 @@ nonces of the fixture; a stanza it adds takes its ephemeral scalar from a fixed
seed. So each case is a `base` with edits, and regenerates byte for byte. A
case that changes a `time_and_key` capsule offers the identity of its `.dkk`,
because the `.dkk` itself would fail at step 9.a: its `capsule_digest` no
longer matches. The last five cases of the corpus offer the `.dkk` and show
longer matches. Five further cases, 122 to 126, offer the `.dkk` and show
that.
### Format 3: its list of §64
The 47 cases of the list of format 3 of §64 test what format 3 adds, all at
step 17 but the first:
- `VERSION` 2 on a format 3 capsule, at step 14 (`VERSION` 4 is
"format 3: version changed", as in format 2);
- the frame of BODY and the area (§29.2): each value of `AREA_LEN`,
`SECURITY_LEN` and `HEAD_LEN` that §64 lists, a frame that does not fit in
L, L < 12, a byte of the area that is not zero, all `ERR_INTEGRITY`;
- the head (§29.4 to §29.6): its version and type tag, a byte more within
`HEAD_LEN`, R8 and R1 in layer 3, and in layer 4 the paths, the comment,
the declared author, the layout and the tree, 65536 implicit folders
included;
- the files: the end of the last file that is not C, and a byte of a file
changed, `ERR_INTEGRITY`;
- the precedence of the end of step 17: a path `..` with a padding byte that
is not zero is `ERR_HEAD_INVALID`, but with the next STREAM chunk corrupt,
or with PAYLOAD_AGE cut right after the chunk that holds the head, it is
`ERR_INTEGRITY`;
- three cases of security that open, without a code, with the verdicts X,
F1 and S1.
They derive from `format3_single`, and the two that need a head followed by
another chunk from `format3_tree`. What a case changes in BODY is sealed again
with FK_PAYLOAD and the nonce of its fixture, followed by the zeros of the
padding up to the P of its L, and when L changes the control is sealed again
with the new L, as anyone can seal the control of a `time_only` capsule
(spec §36.1). The case of 65536 implicit folders carries a head of 235 KB,
2115 paths, all but the last of 32 segments; it is most of the size of the
file. The three
further cases relabel format 3 as 1, and a `time_and_key` capsule as 2 with
its identity and with its `.dkk`.
## The checks of steps 1 to 8
`mutations.json` and `inspect_differential.json` follow the rules of the
@ -421,13 +585,15 @@ implementation limits. No case of these corpora depends on them.
## `vectors/inspect_differential.json`
A differential corpus of the pre-unlock checks: 4380 deterministic mutations of
the twelve official `.dkc` fixtures, 365 of each, with the verdict of steps 1
A differential corpus of the pre-unlock checks: 5110 deterministic mutations of
fourteen official `.dkc` fixtures, 365 of each, with the verdict of steps 1
to 8 of §63 as the reference computes it (`capsule.Inspect` with the `default`
registry, no extension known, no network, no secret), by the rules that "The
checks of steps 1 to 8" above points to. The first 1825, those of the five
format 1 fixtures, are the corpus of v0.8.2 unchanged; the seven format 2
fixtures follow. The `format` field of the file describes the layout below in
fixtures follow, and then two of format 3, `format3_single` and
`format3_time_and_key_portable`, one for each policy: steps 1 to 8 see nothing
of format 3 that format 2 does not have, but `VERSION`. The `format` field of the file describes the layout below in
words; it is not a capsule format.
```json
@ -502,7 +668,15 @@ at least `step`, `name`, `ok` and `error`, and every other field.
`access_key_stanza` and `identity_stanzas`: the index, from 0, in
`inner_stanzas` of the stanza that the `.dkk` and each identity of
`identities`, in order, open. The other stanzas are dummies; the official
vectors are the only place where that is recorded (spec §39).
vectors are the only place where that is recorded (spec §39). In format 3,
as in format 2, with `payload_length` the length of BODY, it adds
`area_len`, `security_cbor`, `head_cbor`, `salt`, `comment`,
`declared_author`, `head_extensions`, `content_offset`, the offset of the
files in BODY, 12 + `AREA_LEN` + `HEAD_LEN`, `files`, each with its `path`,
`size`, `start`, `end`, `sha256` and `mtime` when it has one, its bytes
being those of BODY from `content_offset + start` to `content_offset +
end`, and `verdicts`, with `signature`, `seal` and the `lines` that show
them.
- `fixtures/<name>.dkk.json`: for each `.dkk`, its SHA-256, `credential_id`,
`capsule_id`, `access_type`, `access_material` (a test secret),
`capsule_digest`, extensions and the capsule it opens.

Loading…
Cancel
Save

Powered by TurnKey Linux.