What the official SDK says when it seals: the recovery annex, §7.6 and §71

The SHOULDs of the official SDK that the CLI did not follow yet. encrypt
writes next to the .dkc the recovery annex, FILE.dkc.recuperacion.txt
(spec §62.1, rule 27): datekeys.RecoveryAnnex, annex/recovery.md, which is
§79 of the specification under a title with its version and SHA-256, the
same for every capsule; TestRecoveryAnnex checks it against the text of
SpecVersion. -no-recovery leaves it out. encrypt also says what opening the
capsule years later will take (rule 26): the .dkc, a credential of a
time_and_key capsule, and the release of its round, which an archive of
releases or a cache service must keep if drand no longer serves it; and
beyond one year it recommends time_and_key to a time_only capsule (§7.6).

profile.Status and StatusOf give the state of a pinned profile in the
registry of §71, which DateKeys does not publish yet: Quicknet is active.
encrypt writes no capsule with a profile that is not active, and decrypt
and inspect warn when the profile of a capsule is compromised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.15
dev 6 hours ago
parent 92e7154c23
commit aefc8f6dfe

@ -32,6 +32,21 @@ opens a capsule even when the clock is before the round time.
The alphabet of `en` is `a` to `z` and the ASCII hyphen of its four
compound words, such as `t-shirt`.
- **What the official SDK says when it seals** (spec §7.6, §62.1 rules 26
and 27, §71). `encrypt` writes next to the `.dkc` the recovery annex,
`FILE.dkc.recuperacion.txt` (`datekeys.RecoveryAnnex`, §79 of the
specification under a title with its version and SHA-256, the same for
every capsule; `-no-recovery` leaves it out), and says what opening the
capsule years later will take: the `.dkc`, a credential of a
`time_and_key` capsule, and the release of its round, which an archive
of releases or a cache service must keep if drand no longer serves it.
Beyond one year, `time_only` gets the recommendation of `time_and_key`.
`profile.Status` and `StatusOf` give the state of a pinned profile in
the registry of §71, which DateKeys does not publish yet (Quicknet is
active); `encrypt` writes no capsule with a profile that is not active,
and `decrypt` and `inspect` warn when the profile of a capsule is
compromised.
- **Dice.** For whoever does not trust the random numbers of a computer,
as the author decided: five dice for each word give a number from 11111
to 66666, its position in a list of 7776 words. `wordkey.DiceNumber`,

@ -137,6 +137,8 @@ datekeys profile hash
datekeys version
```
`encrypt` escribe junto a la cápsula `FICHERO.dkc.recuperacion.txt`, el anexo de la especificación sobre cómo abrir una cápsula sin software de DateKeys (spec §79), salvo con `-no-recovery`; y dice qué hará falta para abrirla años después: el `.dkc`, una credencial si es `time_and_key` y el release de su ronda, que tendrá que conservar un archivo de releases o un servicio de caché si drand ya no lo sirve (spec §50). A más de un año, recomienda `time_and_key` a una cápsula `time_only` (spec §7.6). Con un perfil que no esté activo en el registro de §71 no escribe ninguna cápsula, y `decrypt` e `inspect` avisan si el perfil de una cápsula está comprometido.
`encrypt` nunca usa la red. Cada `-in` es un fichero o una carpeta; una
carpeta da su nombre como primer segmento de sus rutas, como hace un
navegador, y se recorre sin seguir enlaces, tomando solo ficheros regulares y

@ -139,6 +139,8 @@ datekeys profile hash
datekeys version
```
`encrypt` writes next to the capsule `FILE.dkc.recuperacion.txt`, the annex of the specification, in Spanish, on how to open a capsule without DateKeys software (spec §79), unless `-no-recovery`; and says what opening the capsule years later will take: the `.dkc`, a credential of a `time_and_key` capsule and the release of its round, which an archive of releases or a cache service must keep if drand no longer serves it (spec §50). Beyond one year, it recommends `time_and_key` to a `time_only` capsule (spec §7.6). A profile that is not active in the registry of §71 writes no capsule, and `decrypt` and `inspect` warn when the profile of a capsule is compromised.
`encrypt` never touches the network. Each `-in` is a file or a folder; a
folder gives its name as the first segment of its paths, as a browser does,
and is walked without following links, taking regular files only and leaving

@ -0,0 +1,21 @@
package datekeys
import _ "embed"
// RecoveryAnnex is the text that the official SDK saves next to each .dkc
// (spec §62.1, rule 27): the informative annex of the specification on how
// to open a capsule without DateKeys software (spec §79), under a title that
// names the version of the specification and the SHA-256 of its text. It is
// the same for every capsule and holds nothing of one. TestRecoveryAnnex
// checks that it is §79 of spec/DateKeys_Protocol_Specification_v<SpecVersion>.md,
// and writes it again, when a new version is approved, with
//
// DATEKEYS_WRITE_ANNEX=1 go test -run TestRecoveryAnnex .
//
//go:embed annex/recovery.md
var RecoveryAnnex string
// RecoveryAnnexSuffix is what the official SDK appends to the name of a .dkc
// to name the file of its recovery annex: carta.dkc.recuperacion.txt. The
// annex is in Spanish, as the specification.
const RecoveryAnnexSuffix = ".recuperacion.txt"

@ -0,0 +1,158 @@
# Cómo abrir una cápsula DateKeys sin software de DateKeys
Este texto acompaña a una cápsula del tiempo de DateKeys, un fichero `.dkc`: dice cómo abrirla, llegada su fecha, sin ningún software de DateKeys, por si ya no existe. Es el anexo informativo §79 de la especificación del protocolo DateKeys v0.15, cuyo texto tiene el SHA-256 45105e693be4187af4dd30f4d254402612587b6427c746f5d29f07a541c1e3f3. Es el mismo para toda cápsula: no lleva ningún dato de esta.
## 79. Anexo informativo: recuperación sin software DateKeys
Este anexo no es normativo. Dice cómo abrir una cápsula de Quicknet sin ningún software de DateKeys, por si dentro de décadas no existe. Repite lo que fijan las secciones que cita, que deciden en caso de duda.
La regla 27 de §62.1 recomienda al SDK oficial guardar este anexo junto al `.dkc`. No contiene ningún dato de una cápsula.
Hace falta:
- el `.dkc`;
- el release de su ronda, de cualquier fuente: un relay de drand, un archivo de releases, un servicio de caché (§50) o cualquier copia. No hace falta confiar en quien lo da: se verifica con la clave pública de 79.1 (79.3);
- en `time_and_key`, una credencial: la `.dkk`, la identity `age` de un recipient o las palabras de una llave de palabras (§38.1);
- una librería de BLS12-381 con pairing y con el hash a G1 de RFC 9380, SHA-256, HMAC-SHA256, HKDF-SHA256 (RFC 5869), ChaCha20-Poly1305 (RFC 8439), un decodificador de CBOR y la herramienta `age` (§77) o una librería compatible.
No sirven las herramientas de drand: `tle` pide el release a la red y no acepta uno dado, y `age` no acepta una file key, que es lo que da el stanza tlock (79.4). Por eso este anexo describe esos dos pasos enteros (79.4 y 79.5).
La implementación de referencia lo sigue en `scripts/recovery`, un programa que no importa ningún paquete de DateKeys, tlock ni drand: solo la librería estándar de Go, `golang.org/x/crypto`, `filippo.io/age` y la librería BLS12-381 `drand/kyber-bls12381`. `scripts/recovery_check.sh` abre con él una cápsula `time_only` y otra `time_and_key` de los fixtures oficiales.
### 79.1 Parámetros de Quicknet
Son los de §12, y pueden no estar ya en ningún otro sitio:
```text
chain_hash 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971
clave pública (G2, 96 bytes)
83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c
8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb
5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a
genesis_time 1692803367 (segundos Unix de la ronda 1)
period 3 segundos
round_time(r) = genesis_time + (r − 1)·3
q 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001
DST BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_
```
Los puntos se codifican comprimidos, en el formato de ZCash (§12.2): 48 bytes en G1, la firma, y 96 en G2, la clave pública y U, con la coordenada c1 antes que c0.
### 79.2 La cápsula
El `.dkc` empieza por 16 bytes (§22):
```text
0 4 "DKC1"
4 1 VERSION: el formato, 1, 2 o 3
5 1 0
6 2 0
8 4 PUBLIC_HEADER_LEN, entero big-endian
12 4 SEALED_CONTROL_LEN, entero big-endian
```
Le siguen `PUBLIC_HEADER`, de `PUBLIC_HEADER_LEN` bytes; `SEALED_CONTROL`, de `SEALED_CONTROL_LEN` bytes; y `PAYLOAD_AGE`, desde el byte 16 + `PUBLIC_HEADER_LEN` + `SEALED_CONTROL_LEN` hasta el final del fichero.
`PUBLIC_HEADER` es un mapa CBOR (§24). Su clave 2 es `capsule_id`, 16 bytes; su clave 4, la política, 0 para `time_only` y 1 para `time_and_key`; y su clave 3, la DateKey, un texto `dk1_` seguido del Base64URL sin relleno de un JSON (§18):
```json
{"version":1,"network":"datekeys:quicknet:v1","round":1000}
```
`round` es la ronda de la cápsula.
### 79.3 El release
Un objeto release es un mapa CBOR (§47.1): la clave 0 es `"datekeys-release"`; la 2, el `chain_hash`, que ha de ser el de 79.1; la 3, la ronda, que ha de ser la de la DateKey; y la 4, la firma, de 48 bytes. Así lo sirven la Release API y un servicio de caché. En un archivo de releases (§50), la firma de la ronda r son los 48 bytes que empiezan en |cabecera| + (r − primera ronda)·48, y 48 ceros si el archivo no la tiene. Un relay de drand la entrega como JSON, `{"round": …, "signature": "…"}`, con la firma en hexadecimal. Hoy se pide así, aunque las direcciones pueden cambiar:
```text
GET https://api.drand.sh/v2/chains/<chain_hash>/rounds/<ronda>
```
La firma no necesita confianza: se verifica (§63, paso 10). Con M el SHA-256 de la ronda en 8 bytes big-endian, y H el hash a G1 de RFC 9380 con la suite `BLS12381G1_XMD:SHA-256_SSWU_RO_` y el DST de 79.1:
```text
e(H(M), clave_pública) == e(firma, G2)
```
con e el pairing de G1 × G2, el primer argumento en G1 y el segundo en G2, y G2 el generador de G2. La firma y la clave pública se decodifican como puntos comprimidos válidos del subgrupo, distintos del punto en el infinito. Para una ronda solo hay una firma válida: cualquier copia que verifique es el release.
### 79.4 El stanza tlock y FK_TIME
`SEALED_CONTROL` es un fichero `age` (79.5) cuya cabecera tiene un solo stanza:
```text
-> tlock <ronda en decimal> <chain_hash en hexadecimal en minúsculas>
<cuerpo en Base64 estándar sin relleno, en líneas de 64 caracteres>
```
El cuerpo mide 128 bytes, U ‖ V ‖ W: U, de 96 bytes, un punto de G2, y V y W, de 16 bytes. Con la firma del release (§63, paso 11):
```text
sigma = V XOR H2(e(firma, U))
FK_TIME = W XOR H4(sigma)
r = H3(sigma, FK_TIME)
comprobar r·G2 == U
```
- H2(x) son los 16 primeros bytes de SHA-256(`IBE-H2` ‖ x), con x los 576 bytes del elemento de GT en el orden de §63, «Serialización de GT en H2»: c1 antes que c0 en cada nivel de la torre, y c2, c1, c0 en Fp6, cada elemento de Fp en 48 bytes big-endian. Una librería que serializa con c0 primero da otro H2. El vector de `testdata/vectors/tlock_ibe.json` lo comprueba: H2(e(G1, G2)) = `cb87319f24560b5231579a09ad79f12e`, con G1 y G2 los generadores.
- H4(sigma) son los 16 primeros bytes de SHA-256(`IBE-H4` ‖ sigma).
- H3(sigma, FK_TIME): base = SHA-256(`IBE-H3` ‖ sigma ‖ FK_TIME); para i = 1, 2, … hasta 65534, d = SHA-256(uint16_le(i) ‖ base), con el contador en 2 bytes little-endian delante de base; se desplaza un bit a la derecha el primer byte de d, solo ese byte; y si d, como entero big-endian de 32 bytes, es menor que q, r = d.
Las etiquetas son los bytes ASCII, sin longitud ni terminador. FK_TIME, de 16 bytes, es la file key del fichero `age` de `SEALED_CONTROL`. `testdata/vectors/tlock_steps.json` da cada valor intermedio de cinco stanzas.
### 79.5 Abrir un fichero `age` con su file key
Es la especificación `age` v1 de C2SP (§77), resumida. Un fichero `age` es una cabecera de texto y un payload binario:
```text
age-encryption.org/v1
-> <tipo> <argumentos…>
<cuerpo del stanza en Base64 sin relleno, líneas de 64 caracteres, la última más corta>
--- <MAC en Base64 sin relleno, 43 caracteres>
<payload>
```
Con la file key FK, de 16 bytes:
1. La cabecera: clave_mac = HKDF-SHA256(ikm = FK, salt = vacío, info = `header`), 32 bytes. El MAC es HMAC-SHA256(clave_mac, la cabecera desde `age-encryption.org/v1` hasta `---` inclusive, sin el espacio que lo sigue). Si no coincide con el de la línea `---`, la file key o la cabecera son otras.
2. El payload empieza tras el salto de línea del MAC por un nonce de 16 bytes. clave = HKDF-SHA256(ikm = FK, salt = nonce, info = `payload`), 32 bytes.
3. Lo demás son bloques de ChaCha20-Poly1305 de 65 536 bytes de texto, 65 552 cifrados, el último más corto. El nonce de 12 bytes del bloque n, desde 0, es n en 11 bytes big-endian seguido de 0x01 en el último bloque y de 0x00 en los demás. No hay datos asociados. El último bloque solo puede estar vacío si es el único, y nada sigue al último bloque.
### 79.6 Las capas siguientes
El plaintext de `SEALED_CONTROL` es:
- en `time_only`, `CONTROL_CBOR`;
- en `time_and_key`, otro fichero `age`, `INNER_ACCESS_AGE`, con stanzas X25519, uno por credencial y señuelos hasta 16 en los formatos 2 y 3. Se abre con `age -d -i clave.txt`, con la identity de la credencial en `clave.txt`. La de una `.dkk` es su `access_material`. La `.dkk` empieza por 12 bytes, `DKK1`, `01`, `00`, `00 00` y `BODY_LEN` en 4 bytes big-endian (§40), y le sigue un mapa CBOR cuya clave 5 es ese `access_material`, 32 bytes (§41), y cuya clave 3 es el `capsule_id` de su cápsula. Una llave de palabras da la identity con §38.1.
`CONTROL_CBOR` es un mapa CBOR (§31). Su clave 3 es `I_PAYLOAD`, otra identity X25519 de 32 bytes, y en los formatos 2 y 3 su clave 6 es L, una cadena de 8 bytes con un entero big-endian, no un entero CBOR. Con `I_PAYLOAD`, `age -d -i payload.txt` abre `PAYLOAD_AGE`.
Una identity X25519 de 32 bytes se escribe para `age` en Bech32 (BIP 173, §77), no Bech32m: el prefijo `age-secret-key-`, los 32 bytes reagrupados de 8 en 5 bits con ceros al final, que dan 52 caracteres, y la suma de comprobación de BIP 173, calculada con el prefijo en minúsculas. Después, todo en mayúsculas: `AGE-SECRET-KEY-1…`.
### 79.7 El contenido
El plaintext de `PAYLOAD_AGE` es:
- en formato 1, el contenido entero;
- en formato 2, el contenido en sus L primeros bytes, seguido de ceros;
- en formato 3, `BODY` en sus L primeros bytes, seguido de ceros (§29.2).
`BODY` empieza por tres enteros de 4 bytes big-endian: `AREA_LEN`, `SECURITY_LEN` y `HEAD_LEN`. Siguen el área de `security`, de `AREA_LEN` bytes, que se puede saltar: solo da los veredictos de la firma y del sello (§29.7); el head, un mapa CBOR de `HEAD_LEN` bytes que empieza en 12 + `AREA_LEN`; y los ficheros, desde 12 + `AREA_LEN` + `HEAD_LEN`, el origen de sus desplazamientos.
La clave 5 del head es la lista de ficheros (§29.4). Cada uno es un mapa:
```text
0 → ruta, con "/" entre carpetas
1 → tamaño
2 → start
3 → end
4 → SHA-256 de sus bytes
5 → mtime, en segundos Unix, opcional
```
Sus bytes van de start a end, sin incluir end, contados desde el origen. Las claves 3 y 4 del head son el comentario y el autor declarado: textos del creador que no prueban nada (§29.7). Una ruta que saldría de la carpeta de destino no se escribe.
### 79.8 Lo que el anexo no comprueba
Este anexo comprueba lo que decide que el resultado es el correcto: la firma del release, r·G2 == U, los MAC de cada fichero `age` y el SHA-256 de cada fichero. No comprueba, entre otras cosas, la codificación canónica de cada objeto, `header_binding` (§26), los 16 stanzas de `INNER_ACCESS_AGE` (§39), los ceros del relleno (§29.1) ni las reglas de las rutas (§29.5). Una cápsula que el lector de §63 rechazaría puede abrirse siguiendo este anexo; su contenido es el que sellaron las MAC de `age`, pero no tiene la garantía de un lector conforme.

@ -0,0 +1,53 @@
package datekeys
import (
"crypto/sha256"
"fmt"
"os"
"strings"
"testing"
)
// recoveryAnnex is the text of RecoveryAnnex for the specification spec:
// its §79, from its title to the end of the document, under a title of its
// own and a paragraph that names the version and the SHA-256 of spec.
func recoveryAnnex(spec []byte) (string, error) {
text := string(spec)
i := strings.Index(text, "\n## 79. ")
if i < 0 {
return "", fmt.Errorf("the specification %s has no §79", SpecVersion)
}
return fmt.Sprintf("# Cómo abrir una cápsula DateKeys sin software de DateKeys\n\n"+
"Este texto acompaña a una cápsula del tiempo de DateKeys, un fichero `.dkc`: dice cómo abrirla, llegada su fecha, "+
"sin ningún software de DateKeys, por si ya no existe. Es el anexo informativo §79 de la especificación del protocolo "+
"DateKeys v%s, cuyo texto tiene el SHA-256 %x. Es el mismo para toda cápsula: no lleva ningún dato de esta.\n\n%s\n",
SpecVersion, sha256.Sum256(spec), strings.TrimRight(text[i+1:], " \n")), nil
}
func TestRecoveryAnnex(t *testing.T) {
spec, err := os.ReadFile("spec/DateKeys_Protocol_Specification_v" + SpecVersion + ".md")
if err != nil {
t.Fatal(err)
}
want, err := recoveryAnnex(spec)
if err != nil {
t.Fatal(err)
}
if os.Getenv("DATEKEYS_WRITE_ANNEX") == "1" {
if err := os.WriteFile("annex/recovery.md", []byte(want), 0o644); err != nil {
t.Fatal(err)
}
t.Skip("wrote annex/recovery.md; build the package again to embed it")
}
if RecoveryAnnex != want {
t.Fatalf("annex/recovery.md is not §79 of the specification %s: write it again with DATEKEYS_WRITE_ANNEX=1 go test -run TestRecoveryAnnex .", SpecVersion)
}
for _, s := range []string{"## 79. Anexo informativo: recuperación sin software DateKeys", "### 79.8 Lo que el anexo no comprueba", "DateKeys v" + SpecVersion + ","} {
if !strings.Contains(RecoveryAnnex, s) {
t.Errorf("the annex lacks %q", s)
}
}
if !strings.HasSuffix(RecoveryAnnex, "conforme.\n") || strings.Contains(RecoveryAnnex, "\r") {
t.Errorf("the annex ends with %q", RecoveryAnnex[len(RecoveryAnnex)-20:])
}
}

@ -52,7 +52,7 @@ import (
)
const usage = `usage:
datekeys encrypt -at TIME -in FILE|FOLDER... -out FILE.dkc [-comment TEXT] [-author TEXT] [-no-mtime] [-policy time_only|time_and_key] [-recipient age1...]... [-dkk FILE.dkk] [-words TEXT|-words-file FILE|-new-words FILE [-dic LIST] [-word-count N]|-dice TEXT|-dice-file FILE [-dic LIST]] [-padding reforzado|bloque256] [-note TEXT] [-sign KEY [-sign-pass-file FILE]] [-large-area]
datekeys encrypt -at TIME -in FILE|FOLDER... -out FILE.dkc [-comment TEXT] [-author TEXT] [-no-mtime] [-policy time_only|time_and_key] [-recipient age1...]... [-dkk FILE.dkk] [-words TEXT|-words-file FILE|-new-words FILE [-dic LIST] [-word-count N]|-dice TEXT|-dice-file FILE [-dic LIST]] [-padding reforzado|bloque256] [-note TEXT] [-sign KEY [-sign-pass-file FILE]] [-large-area] [-no-recovery]
datekeys decrypt -in FILE.dkc -out PATH [-dkk FILE.dkk] [-identity FILE]... [-words TEXT|-words-file FILE] [-expect-author dkauthor1...] [-relay URL]... [-release FILE]
datekeys author keygen -out FILE (-pass-file FILE|-plain)
datekeys author public -key FILE [-pass-file FILE]
@ -98,6 +98,17 @@ SHA-256: for en it is the list of the EFF, byte for byte. encrypt shows the
words: keep the words, which open the capsule; the numbers give them only
with that list.
encrypt also writes FILE.dkc.recuperacion.txt next to the capsule, unless
-no-recovery: the annex of the specification, in Spanish, on how to open a
capsule without DateKeys software (spec §79), the same for every capsule.
Opening it years later needs the .dkc, a credential of a time_and_key
capsule, and the release of its round, which drand publishes at the date:
if drand no longer serves it then, an archive of releases or a cache service
must have kept it (spec §50). For a long horizon or a valuable content,
time_and_key adds a credential that drand does not hold (spec §7.6). A
profile that is not active writes no capsule, and decrypt and inspect warn
when the profile of a capsule is compromised (spec §71).
-note puts a public note in the capsule, in clear: anyone who has the .dkc
reads it before the date, nobody can check who wrote it, and with the date it
can identify someone. decrypt shows it as text of the creator.
@ -120,6 +131,18 @@ var errUsage = errors.New("invalid command line; run 'datekeys help'")
// decrypt-later warning (spec §53).
const longHorizon = 365 * 24 * time.Hour
// profileStatus is the state of a profile in the registry of profiles
// (spec §71), as this release of the module knows it; the tests replace it
// to see a profile that is not active.
var profileStatus = func(p *profile.Profile) profile.Status {
h, err := p.Hash()
if err != nil {
return profile.Active
}
s, _ := profile.StatusOf(h)
return s
}
func main() {
if err := run(os.Args[1:], os.Stdout, os.Stderr, time.Now); err != nil {
if errors.Is(err, errUsage) {
@ -230,6 +253,7 @@ func encrypt(args []string, stderr io.Writer, now func() time.Time) error {
note := fs.String("note", "", "public note of the capsule: one line that anyone with the .dkc reads before the date, and that can identify someone with it")
sign := fs.String("sign", "", "file with the author key that signs the capsule")
signPass := fs.String("sign-pass-file", "", "file with the passphrase of the author key, or - for the standard input")
noRecovery := fs.Bool("no-recovery", false, "do not write the recovery annex of the specification next to the .dkc (spec §79)")
largeArea := fs.Bool("large-area", false, "let the security area grow to 64 KiB if a signature does not fit in 32 KiB (an author key always fits)")
if err := parse(fs, args); err != nil {
return err
@ -256,6 +280,9 @@ func encrypt(args []string, stderr io.Writer, now func() time.Time) error {
}
opts := capsule.EncryptOptions{Profile: profile.Quicknet(), UnlockAt: unlock, Policy: pol, NewPortableKey: *dkk != "", Padding: code,
Comment: *comment, Author: *author, Now: now}
if s := profileStatus(opts.Profile); s != profile.Active {
return fmt.Errorf("encrypt: the profile %s is %s: no new capsule is written with it (spec §71)", opts.Profile.ID, s)
}
for _, r := range recipients {
x, err := age.ParseX25519Recipient(r)
if err != nil {
@ -322,6 +349,12 @@ func encrypt(args []string, stderr io.Writer, now func() time.Time) error {
return err
}
}
annex := *out + datekeys.RecoveryAnnexSuffix
if !*noRecovery {
if err := checkNew(annex); err != nil {
return err
}
}
if *signPass != "" && *sign == "" {
return errors.New("encrypt: -sign-pass-file needs -sign")
}
@ -361,6 +394,11 @@ func encrypt(args []string, stderr io.Writer, now func() time.Time) error {
return fmt.Errorf("the capsule was written to %s but its words could not be: %w", *out, err)
}
}
if !*noRecovery {
if err := writeAtomic(annex, func(w io.Writer) error { _, err := io.WriteString(w, datekeys.RecoveryAnnex); return err }); err != nil {
return fmt.Errorf("the capsule was written to %s but its recovery annex could not be: %w", *out, err)
}
}
fmt.Fprintf(stderr, "Encrypted locally for %s (round %d)\n datekey %s\n capsule_id %x\n format %d: %d files, a payload of %d bytes, padded to %d (%s)\n",
res.UnlockAt.Format(time.RFC3339), res.DateKey.Round, res.DateKey.Compact(), res.CapsuleID, res.Format, len(res.Head.Files), res.Length, res.PaddedLength, res.Padding)
for _, p := range skipped {
@ -377,9 +415,24 @@ func encrypt(args []string, stderr io.Writer, now func() time.Time) error {
fmt.Fprintf(stderr, " words %s: the %d words of the dice in the list %s, %d bits; keep these words, which open the capsule: the numbers give them only with this list\n",
text, len(fromDice), *dic, int(wordkey.Bits(listSize, len(fromDice))))
}
if !*noRecovery {
fmt.Fprintf(stderr, " recovery %s: how to open the capsule without DateKeys software (spec §79); keep it with the .dkc\n", annex)
}
// What opening it years later will take (spec §62.1, rule 26).
needs := "the .dkc"
if pol == capsule.TimeAndKey {
needs = "the .dkc, one of its credentials"
}
fmt.Fprintf(stderr, " to open %s and the release of round %d, which drand publishes at %s: years later, if drand no longer\n"+
" serves it, an archive of releases or a cache service must have kept it (spec §50)\n",
needs, res.DateKey.Round, res.UnlockAt.Format(time.RFC3339))
if res.UnlockAt.Sub(now()) > longHorizon {
fmt.Fprintln(stderr, "warning: Quicknet V1 timelock is not post-quantum. The ciphertext may stay available for years,\n"+
" and its future confidentiality depends on the provider and on the underlying cryptography (spec §53).")
if pol == capsule.TimeOnly {
fmt.Fprintln(stderr, "note: for a horizon this long, or a valuable content, -policy time_and_key adds a credential that drand does\n"+
" not hold: an early signature of the round would not open the capsule (spec §7.6).")
}
}
return nil
}
@ -522,6 +575,10 @@ func decrypt(args []string, stdout, stderr io.Writer, now func() time.Time) erro
}
fmt.Fprintf(stderr, "Decrypted capsule %s (round %d, unlocked at %s); release verified locally\n",
opened.Inspection.Header.CapsuleIDHex(), opened.Release.Round, opened.Inspection.UnlockAt.Format(time.RFC3339))
if profileStatus(opened.Inspection.Profile) == profile.Compromised {
fmt.Fprintf(stderr, "warning: the profile %s is compromised: the content of this capsule may have been read before its date (spec §71)\n",
opened.Inspection.Profile.ID)
}
if opened.ClockBehind {
fmt.Fprintf(stderr, "warning: the release proves that round %d was published at %s, and this clock says %s: it may be behind\n",
opened.Release.Round, opened.Inspection.UnlockAt.Format(time.RFC3339), now().UTC().Format(time.RFC3339))
@ -674,6 +731,10 @@ func inspect(args []string, stdout io.Writer) error {
} else {
v.WriteText(stdout)
showNote(stdout, result)
if result != nil && result.Profile != nil && profileStatus(result.Profile) == profile.Compromised {
fmt.Fprintf(stdout, "warning: the profile %s is compromised: the content of this capsule may have been read before its date (spec §71)\n",
result.Profile.ID)
}
}
return inspectErr
}

@ -662,6 +662,106 @@ func TestKeyOfWords(t *testing.T) {
}
}
// Spec §62.1, rules 26 and 27: encrypt writes the recovery annex next to the
// .dkc, never over another file, and says what opening it years later will
// take; for a long horizon and time_only, it recommends time_and_key (§7.6).
func TestRecoveryAndNotices(t *testing.T) {
dir := t.TempDir()
in := filepath.Join(dir, "carta.txt")
os.WriteFile(in, []byte("para dentro de mucho"), 0o600)
p := profile.Quicknet()
genesis := time.Unix(p.GenesisTime, 0)
at := time.Unix(p.GenesisTime+999*3, 0).UTC().Format(time.RFC3339) // round 1000
dkc := filepath.Join(dir, "carta.dkc")
_, stderr, err := cli(t, genesis, "encrypt", "-at", at, "-in", in, "-out", dkc)
if err != nil {
t.Fatalf("encrypt: %v\n%s", err, stderr)
}
if b, err := os.ReadFile(dkc + ".recuperacion.txt"); err != nil || string(b) != datekeys.RecoveryAnnex {
t.Fatalf("the annex next to the .dkc: %v", err)
}
for _, want := range []string{
" recovery " + dkc + ".recuperacion.txt: how to open the capsule without DateKeys software (spec §79); keep it with the .dkc",
" to open the .dkc and the release of round 1000, which drand publishes at 2023-08-23T",
"an archive of releases or a cache service must have kept it (spec §50)",
} {
if !strings.Contains(stderr, want) {
t.Errorf("stderr lacks %q:\n%s", want, stderr)
}
}
if strings.Contains(stderr, "(spec §7.6)") || strings.Contains(stderr, "(spec §53)") {
t.Errorf("a short horizon got a warning:\n%s", stderr)
}
// time_and_key, without the annex.
keyed := filepath.Join(dir, "llave.dkc")
_, stderr, err = cli(t, genesis, "encrypt", "-at", at, "-policy", "time_and_key", "-dkk", filepath.Join(dir, "llave.dkk"), "-no-recovery", "-in", in, "-out", keyed)
if err != nil {
t.Fatalf("encrypt -no-recovery: %v\n%s", err, stderr)
}
if _, err := os.Stat(keyed + ".recuperacion.txt"); err == nil || strings.Contains(stderr, "recovery ") {
t.Error("-no-recovery wrote the annex")
}
if !strings.Contains(stderr, " to open the .dkc, one of its credentials and the release of round 1000") {
t.Errorf("stderr:\n%s", stderr)
}
// A file where the annex goes: nothing is written.
taken := filepath.Join(dir, "otra.dkc")
os.WriteFile(taken+".recuperacion.txt", []byte("mine"), 0o600)
if _, _, err := cli(t, genesis, "encrypt", "-at", at, "-in", in, "-out", taken); err == nil || !strings.Contains(err.Error(), "already exists") {
t.Errorf("encrypt over an annex: %v", err)
}
if _, err := os.Stat(taken); err == nil {
t.Error("a refused encrypt wrote its .dkc")
}
// A long horizon: the warning of §53, and the note of §7.6 for time_only.
far := genesis.Add(400 * 24 * time.Hour).Format(time.RFC3339)
_, stderr, err = cli(t, genesis, "encrypt", "-at", far, "-in", in, "-out", filepath.Join(dir, "lejos.dkc"))
if err != nil || !strings.Contains(stderr, "(spec §53)") || !strings.Contains(stderr, "note: for a horizon this long, or a valuable content, -policy time_and_key adds a credential") {
t.Errorf("a long horizon, time_only: %v\n%s", err, stderr)
}
_, stderr, err = cli(t, genesis, "encrypt", "-at", far, "-policy", "time_and_key", "-dkk", filepath.Join(dir, "lejos2.dkk"), "-in", in, "-out", filepath.Join(dir, "lejos2.dkc"))
if err != nil || !strings.Contains(stderr, "(spec §53)") || strings.Contains(stderr, "(spec §7.6)") {
t.Errorf("a long horizon, time_and_key: %v\n%s", err, stderr)
}
}
// Spec §71: a profile that is not active writes no capsule, and a
// compromised one gets a warning when a capsule of it is opened or inspected.
func TestProfileStatus(t *testing.T) {
saved := profileStatus
t.Cleanup(func() { profileStatus = saved })
dir := t.TempDir()
in := filepath.Join(dir, "carta.txt")
os.WriteFile(in, []byte("abierta"), 0o600)
p := profile.Quicknet()
genesis := time.Unix(p.GenesisTime, 0)
at := time.Unix(p.GenesisTime+999*3, 0).UTC().Format(time.RFC3339) // round 1000
dkc := filepath.Join(dir, "carta.dkc")
profileStatus = func(*profile.Profile) profile.Status { return profile.ReadOnly }
if _, _, err := cli(t, genesis, "encrypt", "-at", at, "-in", in, "-out", dkc); err == nil ||
err.Error() != "encrypt: the profile datekeys:quicknet:v1 is read-only: no new capsule is written with it (spec §71)" {
t.Fatalf("encrypt with a read-only profile: %v", err)
}
if _, err := os.Stat(dkc); err == nil {
t.Fatal("a read-only profile wrote a capsule")
}
profileStatus = saved
if _, stderr, err := cli(t, genesis, "encrypt", "-at", at, "-in", in, "-out", dkc); err != nil {
t.Fatalf("encrypt: %v\n%s", err, stderr)
}
const warning = "warning: the profile datekeys:quicknet:v1 is compromised: the content of this capsule may have been read before its date (spec §71)"
if stdout, _, err := cli(t, later, "inspect", "-in", dkc); err != nil || strings.Contains(stdout, warning) {
t.Errorf("inspect with an active profile: %v\n%s", err, stdout)
}
profileStatus = func(*profile.Profile) profile.Status { return profile.Compromised }
if stdout, _, err := cli(t, later, "inspect", "-in", dkc); err != nil || !strings.Contains(stdout, warning) {
t.Errorf("inspect with a compromised profile: %v\n%s", err, stdout)
}
if _, stderr, err := cli(t, later, "decrypt", "-in", dkc, "-out", filepath.Join(dir, "abierta"), "-relay", relay(t)); err != nil || !strings.Contains(stderr, warning) {
t.Errorf("decrypt with a compromised profile: %v\n%s", err, stderr)
}
}
// The words of dice, as the list numbered for dice gives them: the capsule
// opens with them.
func TestDice(t *testing.T) {

@ -18,7 +18,7 @@ v0.14, and §47.1 and §79, new in v0.15. A case of §64 that is not in the repo
|---|---|---|---|
| 3 | Guiding principle: verify locally | `profile.Registry`, `datekey.Resolve`, `provider.Verify`, `capsule.Inspect` | `capsule.TestMutationCorpus` |
| 4 | Security goals, among them goal 8, the privacy of metadata in format 2 | whole module | whole suite; goal 8: the tests of rows 29.1, 39 and 55.2 |
| 7 | Threat model | creator model in `internal/testkit.Build`, `RewriteAge`; third-party edits in the mutation corpus | `agewrap.TestTimeIdentityStrictness`, `TestPayloadIdentityStrictness`, `TestAccessIdentityStrictness`, `capsule.TestMutationCorpus` |
| 7 | Threat model; §7.6, SHOULD: recommend `time_and_key` for long horizons or valuable content | creator model in `internal/testkit.Build`, `RewriteAge`; third-party edits in the mutation corpus; `cmd/datekeys` recommends `-policy time_and_key` beyond one year with `time_only` (`TestRecoveryAndNotices`) | `agewrap.TestTimeIdentityStrictness`, `TestPayloadIdentityStrictness`, `TestAccessIdentityStrictness`, `capsule.TestMutationCorpus` |
| 9 | Provider abstraction | `provider.Condition`, `provider.Release`, `provider.ReleaseSource` | `provider/*` |
| 10 | Provider Profile | `profile.Profile`, `Profile.Validate` | `profile.TestValidateRejectsTamperedProfiles` |
| 11 | Canonical profile encoding, `profile_hash`; `period` in 1..2^53−1, `genesis_time` in 0..2^53−1 | `Profile.CanonicalCBOR`, `Profile.Hash`, `profile.Decode` (hand-written `wire` encode and decode: keys 0 to 10, all required, in order; unsigned `genesis_time`, `codec.MaxSafeUint`; `period` limited to 1..86400 s, an implementation limit marked in `spec/datekeys.cddl`, `ERR_NON_CANONICAL_CBOR`) | `profile.TestQuicknetMatchesGoldenVector`, `TestQuicknetCBORLayout`, `TestDecodeRoundTrip`, `TestDecodeStructure`, `TestIntegerRanges`, `FuzzDecode`; `testdata/vectors/profile_quicknet.json`; the `provider_profile` block of `testdata/vectors/cbor.json` (*period of one day, the implementation limit*, *… above the implementation limit*) |
@ -85,7 +85,7 @@ v0.14, and §47.1 and §79, new in v0.15. A case of §64 that is not in the repo
| 50 | Historical release dependency; v0.15: long-term recovery on archives of all rounds and on cache services that serve them, with no hosting promise, the release archive as an informative format | documented in `README.md`; `provider.Archive`, `provider.EncodeArchiveHeader`; `datekeys decrypt -release` with a local archive | `provider.TestArchive`; `cmd/datekeys.TestDecryptWithReleaseInHand` (*a local archive*); the `archive` block of `testdata/vectors/release.json`, `testdata/releases/archive_1000_1004.bin` |
| 51 | Quicknet release verification; order and codes of §63 step 10: the round (`ERR_ROUND_MISMATCH`), then the signature, the canonical encoding of a point of G1 other than the identity (§12.2) that verifies as the BLS signature of the round (`ERR_RELEASE_INVALID`); those codes for a release supplied directly, a network source discarding an invalid one at step 9 (`ERR_RELEASE_UNAVAILABLE`) | `provider.Verify`; v0.15: first the chain hash a release object names, `ERR_PROFILE_MISMATCH` (`provider.Verify`) | `provider.TestVerifyPublishedReleases`, `TestVerifyRejects` (x + p, the point at infinity alone, with a payload or with the sort flag, no compression flag, the negated signature), `TestVerifyUsesThePinnedKeyOnly`; `capsule.TestReleaseFromANetworkSource`; mutations *DateKey A + release of round B*, *release of another round*, *release signature …*; the `objects` of `testdata/vectors/release.json`; mutations *release object of another chain* |
| 52 | DNS / MITM | `provider/drand` (no redirects, bounded responses, BLS) | `drand.TestRedirectsAreNotFollowed`, `TestRejectMalformedRelayResponses`, `TestRandomnessMustMatchWhenPresent` |
| 53 | Harvest now, decrypt later | `cmd/datekeys` warning beyond one year; v0.15: the release among what a long horizon needs (text) | `cmd/datekeys.TestLongHorizonWarning` |
| 53 | Harvest now, decrypt later | `cmd/datekeys` warning beyond one year; v0.15: the release among what a long horizon needs (text) | `cmd/datekeys.TestLongHorizonWarning`, `TestRecoveryAndNotices` |
| 54 | Extensions: data absent or a non-empty opaque byte string, never decoded; 1 to 64 per array; `extension_id` of at least 1 byte; `extension_version` ≤ 2^32−1; elements in strictly ascending unsigned bytewise order of the UTF-8 bytes of `extension_id` (a proper prefix first, never UTF-16 code units or a collation), so one `extension_id` per array, and none in both arrays; a known extension in an object or array it is not registered for is unknown there, its data never interpreted | `extension.New`, `Canonical`, `EncodeArray` (refuses, through `codec.Encoder.Fail`, an array that `DecodeArray` rejects), `DecodeArray` (64 entries checked on the array head, explicit key 2 check), `CheckDisjoint` (linear merge), `CheckCritical`, `CheckNoncritical`, `Unusable`; `Object`, `Array`, the optional `Placement` of a `Registry`, `KnownIn`, and `CheckCriticalIn` and `CheckNoncriticalIn`, which `capsule` runs at steps 4, 9.a and 14 | `extension.TestNew`, `TestData`, `TestCanonicalSorts`, `TestOrderIsUnsignedBytewise`, `TestCanonicalRejects`, `TestEncodeArrayRejects`, `TestDecodeArrayRejects`, `TestCheckDisjoint`, `TestCheckDisjointIsLinear`, `TestCheckCritical`, `TestCheckNoncritical`, `TestPlacement`, `FuzzDecodeArray`; `capsule.TestKnownCriticalExtensions`, `TestUnusableNoncriticalExtensions`, `TestExtensionPlacement`; mutations *unknown critical … extension*, *known critical … extension with invalid data*, *extension_version above 2^32-1*, *null extension data* |
| 55 | Auxiliary integrity | `capsule_digest` treated as UX only | — |
| 55.1 | Trust model: who writes each section, from which step and by what it is bound, what it never proves | no code of its own: `header_binding` (step 15), the age header MACs (steps 11, 13 and 17), `capsule_id` and `capsule_digest` (step 9.a) | `capsule.TestTrustModel` (a capsule forged from the public bytes of `time_only.dkc` or `format2_time_only.dkc` opens; a control that declares L + 1 over a zero padding byte opens to another content; edited PUBLIC_HEADER data passes steps 1 to 8 and fails step 15; other `.dkk` extension data opens the capsule) |
@ -108,12 +108,12 @@ v0.14, and §47.1 and §79, new in v0.15. A case of §64 that is not in the repo
| 69 | Normative errors, including `ERR_EXTENSION_DATA_INVALID` and `ERR_HEAD_INVALID`; every error of the module wraps exactly one | `errors.go`; `provider/drand.Client` and step 9 of `capsule.Open` keep another code of a failure as text only | `datekeys.TestCatalogueMatchesSpec`, `TestCode`; `drand.TestFetchErrorHasOneCode`; `capsule.TestReleaseSourceErrorsAtStep9` |
| 69.1 | Error precedence: the first failing layer of each object (frame, a truncated prelude before the version; type tag and schema version; CBOR profile and CDDL, except the rules with codes of their own; fields with codes of their own in ascending key order, an extension unknown in its object or array before invalid data), the step order of §63 across objects and steps; only the optional inspection of steps 5, 6 and 8 and the `capsule_digest` check can change the code; the codes of step 10 are those of a release supplied directly, one from a network source being discarded at step 9; in format 2, the 16 stanzas belong to step 12, the version of CONTROL_CBOR against the format to layer 2 of step 14, the rules of keys 6 and 7 to its layer 3, and the length and padding of the plaintext to step 17 | `capsule.ParsePrelude`, `capsule.DecodeHeader`, `capsule.DecodeControl`, `accesskey.Decode`, `accesskey.DecodeBody`, `profile.Decode`, `codec.CheckSchema`, `codec.Unmarshal`, `extension.CheckCriticalIn`, `capsule.checkAccessKey`, `OpenOptions.AccessKeyFile`, `agewrap.AccessIdentity`, `provider/drand.Client` | `capsule.TestPrecedenceWithinPublicHeader`, `TestPrecedenceAcrossSteps` (with the examples of format 2), `TestDecodeHeaderReportsTheCDDLFirst`, `TestAccessKeyCheckOrder`, `TestAccessKeyFileAtStep9`, `TestControlCriticalBeforeHeaderBinding`, `TestReleaseFromANetworkSource`, `TestExtensionPlacement`; `accesskey.TestDecodePrecedence`; `agewrap.TestAccessIdentityStrictness`; `profile.TestDecodePrecedence`, `TestPinPathMatchesDecode`; `cmd/datekeys.TestDecryptAccessKeyOrder`; `extension.TestCheckCritical`, `TestPlacement`; `codec.TestCheckSchemaVersionForms`; `testdata/vectors/cbor.json`, `inspect_differential.json` |
| 70 | Compatibility: a reader accepts the three formats and opens formats 1 and 2 with the semantics of v0.8.2 and v0.9; an implementation that writes capsules writes format 3, and only a generator of test vectors writes formats 1 and 2; a reader should report the format; the format is neither the version of the specification (`datekeys.SpecVersion`) nor that of the module (`datekeys.Version`, `datekeys version`); a reader of v0.10 opens the capsules of v0.11, with an area of 32 or 64 KiB, F1 for `alg` 1 and 2, S1 for `seal_type` 2, and the public note and `datekeys.capsule` ignored, and a reader of v0.11 those of v0.10, with their area of 512 bytes; v0.12 changes no format, only texts of the verdicts and how a certificate and a token are read | magic and version checks; `capsule.Format`; `codec.Peek` and `codec.CheckSchema` read keys 0 and 1 only, before strict decoding, with a type tag of at most `codec.MaxTypeTagLen` bytes; `Inspection.Prelude.Format`, `Opened.Format`; `internal/testkit.Build`; `version.go`, where `SpecVersion` stays 0.11 until v0.12 is approved; `capsule.EvaluateSecurity`, which reads security without a context, as a reader of v0.10; `ParseBodyFrame`, which accepts any area of the frame; v0.15: a valid release in hand opens a capsule with a clock behind its round time | mutations; `capsule.TestFormatDispatch`, `TestFormatRelabel`, `TestFormat1Compatibility`; `codec.TestPeek`, `TestCheckSchema`, `TestCheckSchemaVersionForms`, `FuzzPeek`; `capsule.TestDecodeSchemaVersion`; `datekeys.TestSpecVersionNamesTheSpecification`, `TestVersion`; `cmd/datekeys.TestVersion`; `capsule.TestEvaluateSecurityIn` and `TestEvaluateCMS` (no context: F1), `TestEvaluateSeal` (no context: S1), `TestCMSVectors` (the cases without a context); the fixtures of v0.10, with their area of 512 bytes, among those of v0.11 (`TestConformanceFixtures`); mutation *round not reached yet* |
| 71 | Profile registry | `profile.Decode` + `profile.NewRegistry` with pinned hashes | `profile.TestRegistry` |
| 71 | Profile registry; the states `activo`, `solo lectura` and `comprometido`; SHOULD: warn of a compromised profile | `profile.Decode` + `profile.NewRegistry` with pinned hashes; `profile.Status`, `StatusOf` (the states that this release of the module knows, by profile_hash: Quicknet active); `cmd/datekeys`: `encrypt` writes no capsule with a profile that is not active, and `decrypt` and `inspect` warn when the profile of a capsule is compromised | `profile.TestRegistry`, `TestStatus`; `cmd/datekeys.TestProfileStatus` |
| 72 | Extension registry and registration rules, among them the objects and arrays where each extension may appear, and an encoder never writes one elsewhere; the encoder decodes its own output before sealing; security-relevant claims in CONTROL_CBOR or under a signature extension, `.dkk` extension data advisory; the registered extensions, `datekeys.note` in the noncritical array of PUBLIC_HEADER and `datekeys.capsule` in the noncritical array of a `.dkk`, both informative | `extension.Registry`, `extension.Set`, `extension.DataValidator`, `extension.Placement` (optional: a `Registry` without it knows its extensions in every object and array); `extension.Standard`, the registry of the extensions of the specification (`NoteID`, `CapsuleID`), and `locator.Standard`, which validates the data of `datekeys.capsule`; `extension.CheckWrite`, which the writers of capsules and `.dkk` files apply with `extension.Standard` (`capsule.newSealer`, `accesskey.AccessKey.MarshalBody`); self-checks in `capsule.Encrypt`, `capsule.EncryptFiles`, `accesskey.MarshalBody` and `locator.Info.Extension`; these writers take no `Registry`: the application writes each extension of its own only where it is registered | `capsule.TestKnownCriticalExtensions`, `TestUnusableNoncriticalExtensions`, `TestExtensionPlacement`, `TestNestedDataSealsAndOpens`, `TestRegisteredExtensionsWhereRegistered`, `TestPublicNoteRules`, `FuzzEncodeImpliesDecode`; `extension.TestPlacement`, `TestCheckWrite`; `locator.TestInfo` |
| 74 | Provisional aspects; the implementation limits of the reference (name lengths, `public_key`, `period`, maximum `extension_id` length, `dk1_` length, age parser limits, `ERR_POLICY_STRUCTURE_MISMATCH` for INNER_ACCESS_AGE) | `profile.ValidID`, `validName`, `maxPublicKeyLen`, `maxPeriod`; `extension.MaxIDLen`; `datekey.MaxEncodedLen`; `filippo.io/age` | `profile.TestValidateRejectsTamperedProfiles`, `TestIntegerRanges`; `extension.TestNew`; the vectors of `cbor.json` named after the implementation limit |
| 75 | Blocking requirements before v1.0 | items 1–9 and 11 above, with fixtures and mutations in the three formats; item 10 (external review) pending | — |
| 76 | Change policy; the normative changes of v0.8.2: the extension change and its reproducible cases; the refinements and theirs; the amendment on point canonicality and its case (a second implementation on `tlock-js` and `@noble/curves` 1.9.7 accepted U with c0 + p and a signature with x + p); the corrections of the formal review (an invalid release from a network source, the objects and arrays of each extension, the serialization of GT in H2) and their cases, and those of its second round (the encoder rule of §72, the codes of step 10 in §17 and §51 for a release supplied directly, one code for any failure of a source at step 9); the normative changes of v0.9, capsule format 2, and their cases; those of v0.10, capsule format 3, and theirs; those of v0.11, the area of 32 KiB, what is signed, the signatures of `alg` 1 and 2, the seal of `seal_type` 2, the key of words, the public note and `datekeys.capsule`, and theirs; and those of the draft v0.12, which change no format: the names of certificates and the seal of each signer of F6 in the verdicts, the holder without its identifier, the profile of the certificate, identifiers, repetitions and edge cases, the addresses and the padding of the locator, errata, and the vectors of v0.11 | `extension`, `codec`, fixture `time_only_extensions` regenerated; refinements: the order of `capsule.checkAccessKey`, `BODY_LEN` 0 in `accesskey.Decode`, CR and LF and invalid UTF-8 in `datekey.Parse`, the `.dkk` decoded at step 9.a (`OpenOptions.AccessKeyFile`, the CLI), nil identities in `capsule.Open`, every identity tried in `agewrap.AccessIdentity`, `profile.NewRegistry` through `Decode`, `Profile.Validate` rule 1 first; four new `dk1.json` vectors; corrections: `extension.Placement` and the object-aware checks, the `provider.ReleaseSource` contract, `testdata/vectors/tlock_ibe.json`; second round: the error of `provider/drand.Client` and of step 9 in `capsule.Open`; v0.9: rows 22, 29, 29.1, 31, 33, 36, 37, 39, 55.2, 56, 57, 61, 62, 62.1, 63 and 70; v0.10: rows 22, 23, 29 to 29.7, 31, 56, 57, 61 to 64 and 67 to 70; v0.11: rows 24.1, 29.2, 29.3, 29.7 to 29.12, 38.1, 44.1, 62.1, 64, 67, 70 and 72; v0.12: rows 29.3, 29.7, 29.10, 29.11, 44.1, 64, 67 and 70, and the sizes of the locator in `spec/datekeys.cddl` | case 2: `extension.TestNew`; case 3: `capsule.TestNaNKeyedDataHasOneVerdict`; case 4: `capsule.TestExtensionFixtureData`; case 5: `capsule.TestNestedDataSealsAndOpens`; case 6: `capsule.TestHugeExtensionArraysAreRejected`, `extension.TestCheckDisjointIsLinear`; refinements: the tests of rows 12.1, 15, 17, 19, 22, 28.1, 35, 36, 40, 51, 55.1, 63 and 69.1, and `extension.TestOrderIsUnsignedBytewise`; amendment: the tests of rows 12.2 and 64; corrections: `capsule.TestReleaseFromANetworkSource`, `TestExtensionPlacement`, `extension.TestPlacement`, `agewrap.TestTlockH2Vector`; second round: `capsule.TestReleaseSourceErrorsAtStep9`, `TestExtensionPlacement` (the noncritical array of a `.dkk`), `drand.TestFetchErrorHasOneCode`, `TestUnavailabilityAndCancellation`, `datekeys.TestCode`; v0.9: the tests that §76 names for each change, in rows 22, 29.1, 31, 37, 39, 55.2, 57, 62.1, 64 and 70; v0.10: those of the rows it changed; v0.11: those of the rows it added and changed; v0.12: changes 1 and 2, `capsule.TestEvaluateCMS`, `TestIssuerTextFiltered`, `cmd/datekeys.TestRows` and the record of `format3_signed_cms`; change 5, `der.TestSetOfSorted`, `TestCheck` and `cms.TestTSTInfoStrict`; changes 6 and 7, `locator.TestAddresses`, `TestUsableAddresses`, `TestLeastMultiple` and `TestPaddingBoundaries`; change 9, `testdata/vectors/security.json` and the fixture `format3_seal_unsupported`; the cases of changes 1 and 3 to 5 in `security_cms.json`, and those of changes 6 and 7 in `locator.json` (`TestLocatorVectors`); the changes of v0.15, each with its case: the release object (`release.json`, `testdata/releases`), its chain hash at step 10 and step 9.c (`mutations.json`, field `source`), the recovery (`scripts/recovery_check.sh`) |
| 79 | Informative annex: recovery without DateKeys software (v0.15) | `scripts/recovery` (no package of this module, tlock or drand: Go, `golang.org/x/crypto`, `filippo.io/age`, `drand/kyber-bls12381`), `scripts/recovery_check.sh`, run by `scripts/check.sh` | `scripts/recovery.TestImports`, `TestRecoverFixtures` and the other tests of the package, over eight fixtures of the three formats; `scripts/recovery_check.sh` on `format3_single` and `format3_time_and_key_portable` |
| 79 | Informative annex: recovery without DateKeys software (v0.15); §62.1 rule 27, the annex next to each `.dkc` | `datekeys.RecoveryAnnex` (`annex/recovery.md`, §79 under a title with the version and the SHA-256 of the specification) and `RecoveryAnnexSuffix`; `encrypt` writes it as `FILE.dkc.recuperacion.txt` unless `-no-recovery`; `scripts/recovery` (no package of this module, tlock or drand: Go, `golang.org/x/crypto`, `filippo.io/age`, `drand/kyber-bls12381`), `scripts/recovery_check.sh`, run by `scripts/check.sh` | `scripts/recovery.TestImports`, `TestRecoverFixtures` and the other tests of the package, over eight fixtures of the three formats; `datekeys.TestRecoveryAnnex` (the annex is §79 of the specification of `SpecVersion`), `cmd/datekeys.TestRecoveryAndNotices`; `scripts/recovery_check.sh` on `format3_single` and `format3_time_and_key_portable` |
## Error mapping

@ -0,0 +1,46 @@
package profile
import "encoding/hex"
// Status is the state of a Provider Profile in the registry of profiles of
// DateKeys (spec §71).
type Status int
const (
// Active: capsules are written and opened with the profile.
Active Status = iota
// ReadOnly: no new capsule is written with the profile, because its
// provider announces its end or is suspected; the capsules that exist
// still open.
ReadOnly
// Compromised: there is proof that the confidentiality of the profile
// failed. The capsules that exist still open, and the official SDK warns
// that their content may have been read before their date.
Compromised
)
func (s Status) String() string {
switch s {
case Active:
return "active"
case ReadOnly:
return "read-only"
case Compromised:
return "compromised"
}
return "unknown"
}
// statuses are the states of the profiles that this release of the module
// pins, by profile_hash: DateKeys does not publish the signed registry of
// §71 yet, so a change of state comes with a new release of the module.
var statuses = map[string]Status{
QuicknetProfileHash: Active,
}
// StatusOf returns the state of the profile whose profile_hash is hash, and
// whether this release of the module knows it.
func StatusOf(hash [32]byte) (Status, bool) {
s, ok := statuses[hex.EncodeToString(hash[:])]
return s, ok
}

@ -0,0 +1,21 @@
package profile
import "testing"
func TestStatus(t *testing.T) {
h, err := Quicknet().Hash()
if err != nil {
t.Fatal(err)
}
if s, ok := StatusOf(h); !ok || s != Active {
t.Errorf("StatusOf(Quicknet) = %v, %v", s, ok)
}
if s, ok := StatusOf([32]byte{1}); ok || s != Active {
t.Errorf("StatusOf of an unknown profile = %v, %v", s, ok)
}
for s, want := range map[Status]string{Active: "active", ReadOnly: "read-only", Compromised: "compromised", 7: "unknown"} {
if s.String() != want {
t.Errorf("%d: %q, want %q", s, s.String(), want)
}
}
}
Loading…
Cancel
Save

Powered by TurnKey Linux.