You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

72 KiB

DateKeys v0.9 design note: format 2, 16 slots, payload padding, writer rules and privacy

Date: 29-09-2026. Scope: specification text only, which goes in spec/DateKeys_Protocol_Specification_v0.9.md, spec/datekeys.cddl and spec/README.md. I edited no files. Line numbers refer to the current v0.9 working copy, which is byte-identical to v0.8.2 (2488 lines).

Checked against the repository:

  • all section lengths of the 5 fixtures;
  • capsule.Encrypt;
  • filippo.io/age v1.3.2 x25519.go: lines 75, 85-87 and 176-183;
  • mutations.json, cbor.json and inspect_differential.json;
  • the PURBs paper, from its PoPETs page and PDF: title, authors, 2019(4), pp. 6–33, DOI and Algorithm 1.

A script computed every padding value in this note exactly, as integers.


0. Decisions at a glance

Question Recommendation Reason (one line)
Version mechanism (D4) PRELUDE VERSION = 2 ("formato 2"), plus CONTROL_CBOR schema version 2. PUBLIC_HEADER stays schema 1. .dkk stays DKK1 v1. This is the earliest check (step 2, no network), and the change covers the whole capsule, not the header. The sealed control then states its own format, so a relabel fails at step 14 in both old and new readers.
Rejected alternatives PUBLIC_HEADER schema 2: bumps a schema that did not change, and breaks 2 cbor.json vectors, one mutation and the §69.1 example. MAGIC DKC2: a v0.8.2 reader would say ERR_INVALID_MAGIC ("not a capsule"), which misleads. —
New CONTROL_CBOR keys 6 = payload_length, as bstr .size 8 (L, uint64 big-endian). 7 = padding, as uint in {1, 2}. Required in v2, forbidden in v1. A fixed width keeps SEALED_CONTROL_LEN independent of L. A CBOR uint would leak L's width class.
Bound on L 0 ≤ L ≤ L_MAX = 2⁵³ − 2⁴⁶ = 8936830510563328, the same bound for both codes. This is the largest L for which both rules keep P ≤ 2⁵³ − 1: reforzado(L_MAX+1) = 2⁵³.
Codes of the new checks No new error code. Control/format version mismatch → ERR_UNSUPPORTED_VERSION, step 14, layer 2. Rules of keys 6 and 7 → ERR_NON_CANONICAL_CBOR, step 14, layer 3. INNER count ≠ 16 → ERR_POLICY_STRUCTURE_MISMATCH, step 12 (and as a MUST at step 13). Payload length ≠ P or non-zero padding → ERR_INTEGRITY, step 17. Each code already covers its class of failure (§57, §28.1, §30.1).
Where the padding is checked Step 17: length and zero bytes. Step 18: output the first L bytes. Step 16 computes P and cannot fail. L and the code are validated at step 14, so P is known before the first payload byte.
Pre-unlock size check (A10) Do not add. It would be one more optional check that changes the step and code (§69.1, "Alcance").
Stanza order The writer MUST shuffle the 16 recipients with a CSPRNG. age keeps recipient order, and the reference appends R_ACCESS last.
Credential bounds The writer MUST accept 1 to 16 credentials, no duplicates, and none with time_only. With 0 credentials nobody can open the capsule. A duplicate fails its holder at step 13.
Non-canonical or low-order recipients The writer MUST reject them. Readers cannot detect them. Their stanzas cannot be opened, or (low order) anyone could open them.
Self-check and wiping secrets SHOULD, as approved in D7. The official SDK SHOULD default to code 2. —
SEALED_CONTROL_LEN circularity Normative rule: the exact length, known before header_binding is computed. The writer MAY measure it with a draft seal or compute it; either way it MUST check the real seal has the same length. The formula is informative. —
New sections §29.1 «Relleno del payload», §55.2 «Consideraciones de privacidad», §62.1 «Reglas del escritor». No renumbering; §63 keeps its 18 steps. Many sections cite step numbers, and §76 cites "18 pasos".
Terminology «formato 1» / «formato 2». "V1" stays the protocol generation. —

1. Versioning (D4)

1.1 Mechanism

  • VERSION (PRELUDE byte 4) is the capsule format: 1 (v0.8.2 semantics) or 2 (v0.9). The layout is the same in both.
  • CONTROL_CBOR key 1 MUST equal the format.
  • PUBLIC_HEADER is schema 1 in both formats.
  • The .dkk is unchanged. It carries no count and no size, so nothing forces a change.
  • v0.9 writers MUST write format 2. v0.9 readers MUST open both formats.

1.2 How a v0.9 reader dispatches, step by step

Step Format 1 (VERSION 1) Format 2 (VERSION 2)
1 same same
2 accepted accepted. Any other VERSION → ERR_UNSUPPORTED_VERSION
3–11 same; header schema 1; same pre-inspection (5, 6, 8) same
12 time_and_key: one or more X25519 stanzas, distinct shares (v0.8.2) time_and_key: exactly 16 X25519 stanzas, distinct shares, else ERR_POLICY_STRUCTURE_MISMATCH
13 "uno o más" "exactamente 16"; the other rules are unchanged
14 control version MUST be 1; keys 6 and 7 absent (closed map) control version MUST be 2; keys 6 and 7 present and valid
15 same same
16 I_PAYLOAD I_PAYLOAD, L and the code; P = regla(L)
17 v0.8.2 v0.8.2, plus: plaintext length = P and bytes [L, P) all zero, else ERR_INTEGRITY
18 the whole plaintext the first L bytes

A format-1 INNER_ACCESS_AGE with any count (1, 3, 16, 17…) keeps the v0.8.2 rules. The reference parser limit of 1024 stanzas still gives ERR_POLICY_STRUCTURE_MISMATCH at step 12.

1.3 Relabelling

VERSION is public, so anyone can edit it.

Input v0.9 reader v0.8.2 reader
format 2, genuine opens ERR_UNSUPPORTED_VERSION, step 2, no network
format 2 time_only, relabelled 1 ERR_UNSUPPORTED_VERSION, step 14 (control v2 in format 1) same code, step 14
format 2 time_and_key, relabelled 1 16 stanzas pass the format-1 rule; ERR_UNSUPPORTED_VERSION, step 14 same code, step 14
format 1 time_only, relabelled 2 ERR_UNSUPPORTED_VERSION, step 14 (control v1 in format 2) —
format 1 time_and_key with ≠ 16 stanzas, relabelled 2 ERR_POLICY_STRUCTURE_MISMATCH, step 12 —
format 1 time_and_key with exactly 16 stanzas, relabelled 2 ERR_UNSUPPORTED_VERSION, step 14 —
VERSION 3 ERR_UNSUPPORTED_VERSION, step 2 same

header_binding covers the PRELUDE, so it is a third guard at step 15. No path outputs padding as content, or cuts content short.


2. CONTROL_CBOR version 2 (D3)

Keys:

  • 6 → payload_length: bstr .size 8, L as uint64 big-endian with leading zeros. L = 0 is 48 0000000000000000.
  • 7 → padding: unsigned integer, 1 (bloque256) or 2 (reforzado).
  • Keys 4 and 5 keep their meaning, so one decoder serves both versions. Canonical key order puts 6 and 7 after 4 and 5.

Minimal v2 control: 103 bytes, versus 91 for v1.

a60070646174656b6579732d636f6e74726f6c0102025820<11×32>035820<22×32>064800000000000000000702

Why bstr .size 8 (A2(a)): with a shortest-form uint, the value would take 1, 2, 3, 5 or 9 bytes. SEALED_CONTROL_LEN would then reveal, inside the P = 256 bucket, whether L < 24, 24 ≤ L ≤ 255 or L = 256. It would also single out L = 65536 in the P = 65536 bucket, and L = 2³² in the 2³² bucket.

Layers of §69.1 at step 14:

  • Layer 2: the type tag, then the version. The version MUST equal the format, otherwise ERR_UNSUPPORTED_VERSION whatever follows.
  • Layer 3:
    • v2: keys 6 and 7 present;
    • v1: keys 6 and 7 absent;
    • key 6 is a bstr of exactly 8 bytes with value ≤ L_MAX;
    • key 7 is 1 or 2;
    • plus all existing rules → ERR_NON_CANONICAL_CBOR. The order inside the layer does not matter, because every rule gives the same code.
  • Layer 4: critical extensions (key 4).
  • Then step 15 (header_binding) and step 17 (padding).

3. Padding rules (D2)

3.1 Exact definition, for 0 ≤ L ≤ L_MAX

L ≤ 256:  bloque256(L) = reforzado(L) = 256
L > 256:  bloque256(L) = 256·⌈L/256⌉
          E = bitlen(L) − 1   (8..52)
          S = bitlen(E)       (4..6)
          lastBits = E − S
          mask = 2^lastBits − 1
          Padme(L) = (L + mask) AND NOT mask   (= 2^lastBits·⌈L/2^lastBits⌉)
          reforzado(L) = max(bloque256(L), Padme(L))

This matches Algorithm 1 of the paper (E = ⌊log2 L⌋, S = ⌊log2 E⌋ + 1, z = E − S). Handling L ≤ 256 separately means log2 is never taken of 0, and E, S ≥ 4 whenever Padme is used.

3.2 Arithmetic

  • For L ≤ L_MAX, L + mask ≤ 2⁵³ − 1, so every value is exact.
  • Floating-point log2 is wrong: log2(2^53 − 1) evaluates to 53.0 in IEEE 754, but the correct value is 52.
  • JavaScript's bitwise operators are 32-bit. TypeScript should use BigInt or the ⌈⌉ form, which is exact in doubles (division by a power of two).

3.3 Properties (verified for L = 0..200000 and 2·10⁵ random L up to L_MAX)

  • P is a multiple of 256, P ≥ 256 and P ≥ L.
  • The two rules are identical for L ≤ 8192. The first difference is at L = 8193: 8448 with bloque256, 8704 with reforzado.
  • reforzado = Padme for L ≥ 4096.
  • Above 8192, reforzado adds less than L/2^S: under 6.25 %, under 3.125 % from 64 KiB, and under 1.5625 % from 2³².
  • At most 2^S values of P per power of two: 16, 32 or 64.

3.4 Worked examples

The last column is the length of PAYLOAD_AGE: 184 + P + 16·max(1, ⌈P/65536⌉).

L code 1 (bloque256) code 2 (reforzado) E, S, lastBits, Padme PAYLOAD_AGE (1 / 2)
0 256 256 — 456 / 456
1 256 256 — 456 / 456
40 256 256 — 456 / 456
255 256 256 — 456 / 456
256 256 256 — 456 / 456
257 512 512 8, 4, 4, 272 712 / 712
1000 1024 1024 9, 4, 5, 1024 1224 / 1224
4096 4096 4096 12, 4, 8, 4096 4296 / 4296
8192 8192 8192 13, 4, 9, 8192 8392 / 8392
8193 8448 8704 13, 4, 9, 8704 8648 / 8904
10000 10240 10240 13, 4, 9, 10240 10440 / 10440
65536 65536 65536 16, 5, 11, 65536 65736 / 65736
65537 65792 67584 16, 5, 11, 67584 66008 / 67800
78000 (time_only fixture) 78080 79872 16, 5, 11, 79872 78296 / 80088
1000000 1000192 1015808 19, 5, 14, 1015808 1000632 / 1016248
3000000 3000064 3014656 21, 5, 16, 3014656 3000984 / 3015576
600000000 600000000 603979776 29, 5, 24, 603979776 600146680 / 604127416
10⁹ 1000000000 1006632960 29, 5, 24, 1006632960 1000244328 / 1006878904
2³² − 1 4294967296 4294967296 31, 5, 26, 4294967296 4296016056 / 4296016056
L_MAX = 8936830510563328 8936830510563328 8936830510563328 52, 6, 46, L_MAX 8939012353949880 / same
L_MAX + 1 out of range out of range (would be 2⁵³) — —

4. Reader checks for padding (D3)

  • Step 16 computes P and cannot fail.
  • Step 17, after the existing stanza, MAC and STREAM rules:
    • the plaintext length MUST equal P;
    • bytes L..P−1 MUST be 0x00;
    • otherwise ERR_INTEGRITY.
  • Why ERR_INTEGRITY: a mismatch is a failure of the control↔payload binding of §30.1. The existing failures of that binding (A+B swap, truncation, data after the end) are already ERR_INTEGRITY at step 17. ERR_POLICY_STRUCTURE_MISMATCH means stanza structure, ERR_HEADER_BINDING covers only the header, and a new code would change §69.
  • Streaming reader: P is known before the first byte.
    • It writes bytes [0, L) to its temporary output.
    • It MAY fail as soon as it goes past P or finds a non-zero byte at or after L.
    • A plaintext shorter than P is found only at the end of the STREAM.
    • Every one of these failures has the same code, so the moment of detection never changes the result.
    • Nothing is committed before step 17 ends (§56, step 18).
  • Limit (A12): the check adds determinism (one valid plaintext per content, every reader agrees) and catches writer bugs. It adds no authenticity.
    • Whoever knows FK_TIME or FK_ACCESS can seal a control with another L that has the same P, for example L + 1 when the next padding byte is zero.
    • Whoever knows I_PAYLOAD can re-encrypt the payload.

5. Sixteen slots (D1)

  • §33 "uno o más": becomes format-dependent (one or more in format 1, exactly 16 in format 2).
  • §36 / step 12: the structural count check, no secrets needed → ERR_POLICY_STRUCTURE_MISMATCH. Step 13 enforces it again as a MUST.
  • Parser limit: the 1024-stanza limit changes no code in format 2. Any header above 1024 stanzas also has more than 16, and both give the same code at the same step.
  • Other step 13 rules are unchanged: a malformed stanza is ERR_INTEGRITY; an identity that unwraps more than one stanza is ERR_POLICY_STRUCTURE_MISMATCH; no identity unwrapping anything is ERR_ACCESS_INVALID. Dummies are opened by nobody.
  • Dummy rule: for each free slot, generate a fresh X25519 identity from a CSPRNG, take its recipient, and discard the private key at once. It MUST NOT be stored, logged or returned, reused, or derived.
  • Why dummies are indistinguishable:
    • An age X25519 stanza holds only a fresh ephemeral share and AEAD(HKDF(X25519(e, R), share‖R), FK). R never appears in it.
    • Testing a stanza against a candidate R needs the shared secret, which means r or e. The ephemeral e is discarded by age.
    • This holds even for a holder who knows FK_ACCESS.
    • A dummy's key has the same distribution as a real key, and every X25519 stanza is 98 bytes.
    • Colluding holders learn only a lower bound on the number of credentials.
  • Order: a uniformly random permutation from a CSPRNG (MUST).

6. Writer rules (D7): MUST vs SHOULD

Rule Level
Write format 2; never format 1 MUST / MUST NOT
Requested instant strictly after the writer's clock (the reference already rejects "equal to now") MUST
time_only: no credentials. time_and_key: 1–16 credentials, no duplicate public keys, X25519 only, canonical (bit 255 clear, u < p) and not low order MUST
Dummies fill the free slots; private keys never stored or output; random order MUST
capsule_id, I_PAYLOAD, I_ACCESS, credential_id, dummies and the permutation from a CSPRNG; I_PAYLOAD and dummies never reused or derived MUST
L known before sealing; L ≤ L_MAX; code 1 or 2; abort if the source length ≠ L MUST
SEALED_CONTROL_LEN exact; known before header_binding (draft seal or formula); real seal checked to have the same length MUST
§57 limits; extension rules of §54 and §72 MUST
Discard the output on any error MUST
Official SDK defaults to code 2 SHOULD
Self-check: decode own PUBLIC_HEADER and CONTROL_CBOR with the reader's rules; INNER has 16 X25519 stanzas with distinct shares; each kept identity (I_ACCESS) opens exactly one; I_PAYLOAD opens the PAYLOAD_AGE header SHOULD
Wipe I_PAYLOAD, CONTROL_CBOR, spare copies of I_ACCESS, dummy key memory and content buffers SHOULD

Length formula (informative, Quicknet, checked against all 5 fixtures), with c(n) = max(1, ⌈n/65536⌉), X25519 stanza = 98 bytes, tlock stanza = 249 + d bytes (d = digits of the round):

  • PAYLOAD_AGE = 184 + P + 16c;
  • INNER_ACCESS_AGE = 86 + 98k + C + 16c(C);
  • OUTER_TIME_AGE = 335 + d + n + 16c(n).

In format 2, with round 1000 and no control extensions: time_only 458, time_and_key 2128 (INNER 1773).


7. Privacy (D5, D6)

The full text is in §8, under "§55.2". Summary:

  • Visible to anyone with the .dkc, before and after the date: format, frame lengths, capsule_id, DateKey (and so the date), access_policy (kept visible for the fail-fast at step 9), header extensions, tlock stanza, P exactly, and the control length. The control length now reveals only the size of the control extensions.
  • Hidden until the date: L, the code (except when P gives it away), the control, the number of credentials and the recipients.
  • After the date: time_only exposes everything. time_and_key exposes the 16-stanza INNER header only.
  • Never visible: recipients, whether a stanza is a dummy, and the credential count beyond the colluders' lower bound.
  • Format 1 leaks both the credential count and the exact L.

8. Spec text, section by section (ready to paste)

Title (L3, L6)

### Borrador normativo v0.9

**Estado:** Draft / pre-estándar  
**Fecha:** 29 septiembre 2026  

§1: add to the list

After «- recipient X25519 V1;»:

- formatos 1 y 2 de `.dkc` y su compatibilidad;
- huecos fijos de `INNER_ACCESS_AGE` y relleno del payload;

After «- verificación;»:

- reglas del escritor;

Change «- extensiones genéricas.» to «- extensiones genéricas;» and add:

- consideraciones de privacidad.

§4: add goal 8

8. **Privacidad de metadatos**  
   Antes de la fecha, una cápsula de formato 2 no debe revelar la longitud exacta de su contenido ni el número de credenciales que la abren (§55.2).

§5: add a non-goal

- ocultar la fecha de apertura, la política de acceso, `capsule_id` o el tamaño rellenado del contenido (§55.2);

§15: after «Nunca se redondea hacia atrás.»

Al escribir una cápsula, el instante pedido MUST ser posterior al instante actual del reloj del escritor (§62.1).

§22

In the table: 4 1 VERSION = 1 o 2 (formato). After «El payload es el resto del fichero hasta EOF.», add:

`VERSION` es el formato de la cápsula. La trama es la misma en los dos formatos; cambian `INNER_ACCESS_AGE`, el plaintext de `PAYLOAD_AGE` y la versión de `CONTROL_CBOR`:

| Formato | `VERSION` | Semántica |
|---|---|---|
| 1 | 1 | La de la v0.8.2. `INNER_ACCESS_AGE` lleva uno o más stanzas; el plaintext de `PAYLOAD_AGE` es el contenido; `CONTROL_CBOR` tiene la versión de schema 1. |
| 2 | 2 | La de esta versión. `INNER_ACCESS_AGE` lleva exactamente 16 stanzas (§39); el plaintext de `PAYLOAD_AGE` es el contenido con relleno (§29.1); `CONTROL_CBOR` tiene la versión de schema 2 (§31). |

Un escritor MUST escribir el formato 2 (§62.1). Un lector MUST aceptar los dos (§70).

In the «V1 MUST exigir» block, add a first line: 1 <= VERSION <= 2.

§23

Item 3 → «3. VERSION distinto de 1 y de 2 → ERR_UNSUPPORTED_VERSION (paso 2);». After the final paragraph, add:

Un lector que solo conoce el formato 1, como los de la v0.8.2, rechaza aquí una cápsula de formato 2, antes de cualquier petición de red (§70).

§24: after «Las claves 5 y 6 son opcionales…»

`PUBLIC_HEADER` es la misma en los dos formatos, con la versión de schema 1. No contiene nada del relleno ni del número de credenciales (§55.2).

§26: add at the end

El PRELUDE incluye `VERSION`: una cápsula a la que se cambia el formato falla, como tarde, en el paso 15. Antes falla en el paso 12 o en el 14 (§31, §39, §63).

§27

  • «- version;» → «- versión, que es el formato (1 o 2);».
  • L762: «de las secciones 29, 32 y 33» → «de las secciones 29, 32, 33 y 39».

§28.1

  • Table header: «viola §29, §32 o §33» → «viola §29, §32, §33 o §39».
  • After the paragraph at L824, add:
En formato 2 también es `ERR_INTEGRITY`, en el paso 17, un plaintext de `PAYLOAD_AGE` cuya longitud no es la P de su L o cuyo relleno no es nulo (§29.1).

§29: replace the whole section

## 29. PAYLOAD_AGE

El contenido del usuario se cifra como un **fichero age v1 estándar completo**.

Durante la creación se genera:

```text
I_PAYLOAD = X25519 identity aleatoria de 32 bytes
R_PAYLOAD = X25519 public recipient correspondiente
```

`I_PAYLOAD` MUST salir de un CSPRNG, nueva para cada cápsula. MUST NOT reutilizarse en otra cápsula ni derivarse de otro valor, como el contenido, `capsule_id` o `I_ACCESS`: el binding de §30.1 solo separa dos cápsulas si sus `I_PAYLOAD` son distintas.

Entonces:

```text
PAYLOAD_AGE =
age.Encrypt(
    recipient = R_PAYLOAD,
    plaintext = PAYLOAD_PLAINTEXT
)
```

donde, con L la longitud del contenido en bytes:

```text
formato 1:  PAYLOAD_PLAINTEXT = contenido
formato 2:  PAYLOAD_PLAINTEXT = contenido || 0x00^(P − L),  P = regla(L)  (§29.1)
```

`PAYLOAD_AGE` MUST contener exactamente un stanza, de tipo X25519, para `R_PAYLOAD`.

`age` genera internamente:

```text
FK_PAYLOAD = 16 random bytes
```

y la protege para `R_PAYLOAD`.

`I_PAYLOAD` se almacena dentro de `CONTROL_CBOR`; en formato 2, también L y el código de la regla de relleno (§31).

§29.1 (new, after §29)

## 29.1 Relleno del payload

En formato 2 el plaintext de `PAYLOAD_AGE` es el contenido seguido de ceros:

```text
PAYLOAD_PLAINTEXT = contenido || 0x00^(P − L)
P = regla(L)
```

donde L es la longitud del contenido en bytes. La longitud de `PAYLOAD_AGE`, visible para cualquiera, revela P y no L (§55.2).

El escritor elige la regla. Su código viaja sellado en `CONTROL_CBOR`, junto a L (claves 6 y 7, §31); nada del relleno va en `PUBLIC_HEADER`.

| Código | Nombre | P |
|---|---|---|
| 1 | `bloque256` | el menor múltiplo de 256 mayor o igual que L, y como mínimo 256 |
| 2 | `reforzado` | el mayor de `bloque256(L)` y `Padme(L)` |

No existe una regla sin relleno. Un lector MUST aceptar los dos códigos. El SDK oficial SHOULD usar el código 2 por defecto.

Definición exacta, para 0 ≤ L ≤ L_MAX:

```text
L_MAX = 2^53 − 2^46 = 8936830510563328

si L <= 256:
    bloque256(L) = 256
    reforzado(L) = 256

si L > 256:
    bloque256(L) = 256 · ceil(L / 256)
    E            = bitlen(L) − 1        ; floor(log2 L), de 8 a 52
    S            = bitlen(E)            ; floor(log2 E) + 1, de 4 a 6
    lastBits     = E − S
    mask         = 2^lastBits − 1
    Padme(L)     = (L + mask) AND NOT mask
    reforzado(L) = max(bloque256(L), Padme(L))
```

`bitlen(x)` es el número de bits de x sin ceros a la izquierda: `bitlen(1) = 1`, `bitlen(256) = 9`. `Padme` es la función Padmé de Nikitin et al. (§77): conserva los S + 1 bits más significativos de L y redondea hacia arriba los demás. `(L + mask) AND NOT mask` es igual a `2^lastBits · ceil(L / 2^lastBits)`.

Aritmética:

- todos los valores son enteros exactos; para L ≤ L_MAX, ningún resultado intermedio supera 2⁵³ − 1;
- `floor(log2 L)` MUST calcularse con enteros, como `bitlen(L) − 1`, nunca con un logaritmo en coma flotante: en IEEE 754, `log2(2^53 − 1)` redondea a 53, y el resultado correcto es 52;
- los operadores de bits de 32 bits, como los de JavaScript, no bastan: una implementación en ese lenguaje usa BigInt o la forma con `ceil`, exacta en doubles porque divide y multiplica por potencias de dos.

L_MAX es el mayor L para el que las dos reglas dan una P de como mucho 2⁵³ − 1 (§58): `reforzado(L_MAX + 1) = 2^53`. Un escritor MUST NOT sellar un L mayor (§62.1), y un lector lo rechaza en `CONTROL_CBOR` (§31).

Propiedades, para todo L de 0 a L_MAX (informativo):

- P es múltiplo de 256, P ≥ 256 y P ≥ L;
- las dos reglas coinciden para L ≤ 8192; la primera diferencia está en L = 8193: 8448 con `bloque256` y 8704 con `reforzado`;
- `bloque256` añade menos de 256 bytes; por encima de L = 8192, `reforzado` añade menos de L / 2^S: menos del 6,25 %, del 3,125 % desde L = 65 536 y del 1,5625 % desde L = 2³²;
- entre 2^E y 2^(E+1), `reforzado` admite como mucho 2^S valores de P (16, 32 o 64), y `bloque256`, 2^(E − 8).

Vectores (`testdata/vectors/padding.json`), con la longitud de `PAYLOAD_AGE`, `184 + P + 16·max(1, ⌈P / 65536⌉)` (§62.1):

| L | P, código 1 | P, código 2 | `PAYLOAD_AGE`, código 1 / 2 |
|---|---|---|---|
| 0, 1, 40, 255, 256 | 256 | 256 | 456 / 456 |
| 257 | 512 | 512 | 712 / 712 |
| 1000 | 1024 | 1024 | 1224 / 1224 |
| 8192 | 8192 | 8192 | 8392 / 8392 |
| 8193 | 8448 | 8704 | 8648 / 8904 |
| 65 537 | 65 792 | 67 584 | 66 008 / 67 800 |
| 78 000 | 78 080 | 79 872 | 78 296 / 80 088 |
| 3 000 000 | 3 000 064 | 3 014 656 | 3 000 984 / 3 015 576 |
| 600 000 000 | 600 000 000 | 603 979 776 | 600 146 680 / 604 127 416 |
| 2³² − 1 | 4 294 967 296 | 4 294 967 296 | 4 296 016 056 / 4 296 016 056 |
| L_MAX | L_MAX | L_MAX | 8 939 012 353 949 880 |

El código va sellado, pero P lo delata a veces (§55.2).

§30: add at the end

El relleno de §29.1 forma parte del plaintext de `age`: no existe un subformato DateKeys para él.

§30.1: add at the end

En formato 2 el control fija, además, la longitud y el relleno del plaintext de `PAYLOAD_AGE`: su L y su código dan P (§29.1), y el paso 17 rechaza un plaintext de otra longitud o con un byte de relleno distinto de 0x00 (`ERR_INTEGRITY`). Esta comprobación complementa el binding de `I_PAYLOAD`, no lo sustituye. Tampoco añade autenticidad: quien conoce `FK_TIME` o `FK_ACCESS` puede sellar otro control con otra L de la misma P, y quien conoce `I_PAYLOAD` puede cifrar otro plaintext (§55.1).

El binding solo separa dos cápsulas si sus `I_PAYLOAD` son distintas: `I_PAYLOAD` MUST ser nueva para cada cápsula (§29).

§31: replace L904–L919 (schema up to «…identificada por namespace.»)

Schema base:

```text
0 → "datekeys-control"
1 → versión de schema: 1 en formato 1, 2 en formato 2
2 → header_binding (32 bytes)
3 → payload_identity (32 raw bytes, I_PAYLOAD)
4 → critical_extensions
5 → noncritical_extensions
6 → payload_length (8 bytes: L, entero sin signo big-endian)
7 → padding (código de relleno: 1 = bloque256, 2 = reforzado)
```

Reglas del schema:

- la versión de schema (clave 1) MUST ser el formato de la cápsula, el `VERSION` de su PRELUDE (§22); otra es `ERR_UNSUPPORTED_VERSION` en el paso 14 (§69.1, capa 2);
- las claves 4 y 5 son opcionales y se omiten cuando no hay extensiones (§58.1); significan lo mismo en las dos versiones;
- las claves 6 y 7 MUST existir en la versión 2 y MUST NOT existir en la versión 1;
- `payload_length` MUST ser una cadena de bytes de exactamente 8 bytes con L, la longitud del contenido, como entero sin signo big-endian, con ceros a la izquierda; su valor MUST ser como mucho L_MAX = 2⁵³ − 2⁴⁶ (8936830510563328, §29.1). L = 0 se codifica `48 0000000000000000`;
- `padding` MUST ser el entero sin signo 1 o 2 (§29.1);
- cualquier violación de las reglas de las claves 6 y 7 es `ERR_NON_CANONICAL_CBOR` (§57).

`payload_length` tiene tamaño fijo para que la longitud de `CONTROL_CBOR`, visible en `SEALED_CONTROL_LEN`, no dependa de L (§55.2): un entero CBOR en su forma más corta ocuparía 1, 2, 3, 5 o 9 bytes según L. Un `CONTROL_CBOR` de versión 2 sin extensiones mide 103 bytes.

No contiene el payload grande.

DateKeys V1 utiliza **un único mecanismo de extensión**. No existe un campo core separado para aplicaciones o semánticas superiores. Cualquier semántica adicional se registra como una extensión identificada por namespace. Las claves 6 y 7 son campos del protocolo, no semántica de aplicación.

§33

In the code block: recipients = X25519: en formato 2, 16, credenciales y señuelos (§39). Replace L1002 with:

`INNER_ACCESS_AGE` MUST contener stanzas de tipo X25519, exactamente uno por recipient: uno o más en formato 1, y exactamente 16 en formato 2 (§39).

§36 (time_and_key)

Replace «cuyo header contenga uno o más stanzas, todos ellos de tipo X25519,» with «cuyo header contenga uno o más stanzas en formato 1, o exactamente 16 en formato 2 (§39), todos ellos de tipo X25519,». After the paragraph «age genera un share efímero nuevo…», add:

En formato 2 el límite de 1024 stanzas del parser de la implementación de referencia (§74) no cambia ningún código: una cabecera que lo supera también tiene más de 16 stanzas, y las dos causas dan `ERR_POLICY_STRUCTURE_MISMATCH` en el paso 12.

§36.1: add at the end

Los señuelos de §39 no añaden ninguna barrera de acceso ni ninguna autenticidad.

§37: add after «No se define un KEM propio.»

Un recipient X25519 son 32 bytes: la coordenada u de RFC 7748 en little-endian. El escritor MUST rechazar (§62.1):

- un recipient no canónico: con el bit 255 a 1, o con u ≥ p = 2²⁵⁵ − 19;
- un recipient de orden bajo: aquel para el que X25519(k, u) es la cadena de 32 ceros. Con el recorte de escalares de RFC 7748, el resultado no depende de k.

X25519 ignora el bit 255 y reduce u módulo p, pero `age` pone los 32 bytes recibidos en el salt de HKDF, y la identity, su clave pública canónica: el stanza de un recipient no canónico no lo abre nadie. Con un recipient de orden bajo, el secreto compartido es cero: `age` se niega a cifrar, y una librería que no lo comprobara dejaría abrir ese stanza a cualquiera. Un lector no puede detectar ninguno de los dos casos: el stanza no contiene el recipient.

§38

L1128 → «R_ACCESS se usa como recipient de INNER_ACCESS_AGE. En formato 2 cuenta como una de sus credenciales, como mucho 16, y ocupa una posición aleatoria entre los 16 stanzas (§39).»

§39: replace the whole section

## 39. Recipients de `INNER_ACCESS_AGE`

Una **credencial** es un recipient X25519 que el escritor recibe o la clave portable `R_ACCESS` (§38).

En formato 2:

- `INNER_ACCESS_AGE` MUST contener exactamente 16 stanzas X25519, uno por hueco;
- una cápsula `time_and_key` MUST tener entre 1 y 16 credenciales, sin repetir ninguna;
- cada hueco que no ocupa una credencial lleva un **señuelo**: la clave pública de una identity X25519 nueva que el escritor genera con un CSPRNG y descarta en el acto. La clave privada de un señuelo MUST NOT almacenarse, registrarse ni entregarse; ningún señuelo se reutiliza ni se deriva de otro valor;
- el escritor MUST pasar los 16 recipients a `age` en un orden uniformemente aleatorio, obtenido de un CSPRNG, porque `age` escribe los stanzas en el orden de sus recipients;
- toda identity real abre exactamente un stanza, y ninguna abre el de un señuelo.

En formato 1, `INNER_ACCESS_AGE` MAY contener uno o más stanzas X25519, uno por recipient, sin señuelos ni orden prescrito: es la regla de la v0.8.2.

En los dos formatos todos los stanzas envuelven la misma:

```text
FK_ACCESS
```

incluidos los de los señuelos. Por tanto el `CONTROL_CBOR` y `PAYLOAD_AGE` no se duplican.

Un señuelo es indistinguible de una credencial. Un stanza X25519 de `age` contiene un share efímero nuevo y la file key cifrada con una clave derivada del secreto compartido con el recipient (C2SP age, §77); el recipient no aparece en él. Un señuelo es una clave pública con la misma distribución que la de una identity real, y su stanza tiene la misma forma y la misma longitud, 98 bytes. Saber si un stanza es para una clave pública dada exige el secreto compartido: la clave privada del recipient o el escalar efímero, que `age` descarta. Ni siquiera quien conoce `FK_ACCESS` puede comprobarlo. Quien tiene una credencial sabe cuál es su stanza y nada de los otros 15; varias credenciales juntas solo saben cuántos stanzas abren entre todas.

§40: add after the frame

La `.dkk` conserva `VERSION` 1 con los dos formatos de `.dkc`: no contiene nada que dependa del número de credenciales ni del relleno.

§55.1: row PAYLOAD_AGE

  • «Vinculada desde el paso…» column: append «En formato 2, L y el código de CONTROL_CBOR fijan además su longitud y su relleno (§29.1).»
  • «Nunca prueba» column: append «Tampoco que L sea la longitud original: quien puede reescribir el control puede declarar otra L de la misma P (§30.1).»

§55.2 (new, after §55.1)

## 55.2 Consideraciones de privacidad

Esta sección enumera qué revela una cápsula y a quién. No añade reglas de lectura: resume las consecuencias de §22, §24, §29.1, §31 y §39. Describe el formato 2; el formato 1 revela más (al final).

### Visible para quien tenga el `.dkc`, antes y después de la fecha

- `VERSION`, que es el formato, `PUBLIC_HEADER_LEN` y `SEALED_CONTROL_LEN`;
- `capsule_id`, que permite reconocer copias de la misma cápsula;
- la DateKey: el perfil y la ronda, y por tanto el instante de apertura;
- `access_policy`: si hace falta una credencial. Sigue visible para que el paso 9 pueda fallar sin red (`ERR_ACCESS_REQUIRED`);
- las extensiones de `PUBLIC_HEADER`, con su `data`;
- el stanza tlock de `OUTER_TIME_AGE`: la ronda y el chain hash;
- la cabecera de `PAYLOAD_AGE` y su longitud, que da P exactamente: `184 + P + 16·max(1, ⌈P / 65536⌉)`. P acota L: entre 0 y 256 si P = 256; si no, en un intervalo de 256 bytes con `bloque256`, y con `reforzado`, de 256 bytes hasta L = 8192 y de menos de P / 16 por encima;
- la longitud de `CONTROL_CBOR`, que se deduce de `SEALED_CONTROL_LEN` (§62.1): 103 bytes más los de sus extensiones. Revela el tamaño de las extensiones de control, no su contenido. No depende de L, que ocupa siempre 8 bytes, ni del número de credenciales, porque `INNER_ACCESS_AGE` tiene siempre 16 stanzas.

El código de relleno va sellado, pero P lo delata a veces: los dos códigos dan la misma P para L ≤ 8192, y por encima una P que `reforzado` no produce revela `bloque256`.

### Oculto hasta la fecha

- el contenido y su longitud exacta L;
- el código de relleno, salvo lo que delata P;
- `I_PAYLOAD` y todo `CONTROL_CBOR`, incluida la `data` de sus extensiones;
- el número de credenciales y los recipients.

### Después de la fecha

Publicada la ronda, cualquiera puede abrir `OUTER_TIME_AGE`:

- `time_only`: cualquiera lee `CONTROL_CBOR` y el contenido, con L y el código. Nada queda oculto;
- `time_and_key`: cualquiera ve la cabecera de `INNER_ACCESS_AGE`, siempre con 16 stanzas; `CONTROL_CBOR` y el contenido siguen ocultos para quien no tiene una credencial. Quien tiene una lee L, el código, el contenido y las extensiones de control, y sabe cuál es su stanza; no sabe cuáles de los otros 15 son señuelos. Varias credenciales juntas solo saben cuántos stanzas abren entre todas: una cota inferior del número de credenciales.

### Nunca visible

- los recipients: un stanza X25519 no contiene la clave pública de su recipient, y comprobar si un stanza es para una clave pública dada exige su clave privada (§39);
- si un stanza es de un señuelo;
- el número de credenciales, salvo la cota inferior de arriba.

### Fuera del `.dkc`

- una `.dkk` lleva en claro `capsule_id`, `credential_id`, el `capsule_digest` opcional, que la ata a los bytes exactos de un `.dkc`, y la `data` de sus extensiones (§43, §44);
- pedir el release revela a la Release API y a los relays el perfil, la ronda y la dirección de quien pregunta, pero no `capsule_id` (§45, §46);
- el nombre del fichero, sus fechas en el sistema de ficheros y el canal de entrega no pertenecen al protocolo (§6).

### Formato 1

Una cápsula de formato 1 no oculta ni el número de credenciales ni L:

- `SEALED_CONTROL_LEN` crece 98 bytes por stanza de `INNER_ACCESS_AGE`: en los fixtures de la v0.8.2, 646 bytes con una credencial y 842 con tres;
- la longitud de `PAYLOAD_AGE`, `184 + L + 16·max(1, ⌈L / 65536⌉)`, da L exacta.

El protocolo no rellena la `data` de las extensiones. Una extensión registrada que necesite ocultar su longitud define su propio relleno (§72).

§56: add at the end

En formato 2, los L primeros bytes no pueden presentarse como válidos antes de que el paso 17 compruebe la longitud y el relleno del plaintext (§63, paso 18).

§57

L1543 → «PAYLOAD_AGE no tiene trama propia: va hasta EOF, se procesa en streaming y es, como mínimo, una cabecera age bien formada (§22, §28.1). En formato 2 su plaintext mide exactamente P = regla(L) (§29.1).»

After «Los tamaños se validan antes de reservar memoria.», add:

Límites del escritor que el lector no comprueba como trama (§62.1):

- `time_and_key` admite entre 1 y 16 credenciales (§39); un lector de formato 2 solo ve 16 stanzas;
- L es como mucho L_MAX = 2⁵³ − 2⁴⁶ (§29.1); un lector lo comprueba en `CONTROL_CBOR` (§31), con `ERR_NON_CANONICAL_CBOR`.

In the error mapping (L1550), replace «versión de schema (clave 1) no soportada → ERR_UNSUPPORTED_VERSION (§70)» with «versión de schema (clave 1) que el objeto no admite —para CONTROL_CBOR, la que no es el formato de su cápsula (§31)— → ERR_UNSUPPORTED_VERSION (§70)». After «el orden de los extension_id y el máximo de 64 extensiones por array», insert «, y las de las claves 6 y 7 de CONTROL_CBOR (§31)».

§58: after L1586

L (clave 6 de `CONTROL_CBOR`) no es un entero CBOR: es una cadena de 8 bytes con un entero sin signo big-endian de como mucho 2⁵³ − 2⁴⁶ (§31), también exacto como double.

§58.1: add to the «Por tanto:» list

- las claves 6 y 7 de `CONTROL_CBOR` de versión 2 son obligatorias y nunca se omiten; L = 0 es un dato real (`48 0000000000000000`), no una ausencia, y ningún código de relleno representa «sin relleno» (§29.1, §31).

§61: replace the flow (L1664–L1688)

## 61. Flujo de cifrado `time_only`

Un orden válido para escribir una cápsula de formato 2. Un escritor MAY seguir otro que produzca un objeto equivalente y cumpla §62.1.

```text
1. Validar las opciones (§62.1): ninguna credencial; instante pedido
   posterior al reloj del escritor; L conocida y como mucho L_MAX;
   código de relleno 1 o 2; extensiones conformes a §54 y §72.
2. Resolver DateKey localmente (§15).
3. Generar capsule_id (CSPRNG, §21).
4. Generar I_PAYLOAD X25519 nueva (CSPRNG, §29).
5. Construir PUBLIC_HEADER (versión de schema 1).
6. Medir SEALED_CONTROL_LEN (§62.1): la longitud del OUTER_TIME_AGE
   del paso 10 con un CONTROL_CBOR de 103 bytes más sus extensiones.
   Más de 64 MiB → abortar (§57).
7. Construir PRELUDE: VERSION = 2 y las longitudes de los pasos 5 y 6.
8. Calcular header_binding = SHA-256(PRELUDE || PUBLIC_HEADER).
9. Crear CONTROL_CBOR, versión de schema 2:
      header_binding
      I_PAYLOAD
      critical_extensions
      noncritical_extensions
      payload_length = L
      padding = código
10. Crear OUTER_TIME_AGE:
      age genera FK_TIME (16 bytes)
      recipient = tlock(DateKey)
      plaintext = CONTROL_CBOR
    Su longitud MUST ser la del paso 6.
11. SEALED_CONTROL = OUTER_TIME_AGE.
12. Escribir PRELUDE || PUBLIC_HEADER || SEALED_CONTROL.
13. Escribir PAYLOAD_AGE a continuación:
      age genera FK_PAYLOAD (16 bytes)
      recipient = R_PAYLOAD
      plaintext = contenido || 0x00^(P − L), P = regla(L) (§29.1)
    Si el contenido no mide exactamente L bytes → abortar y descartar
    lo escrito.
```

(Keep «File keys utilizadas: FK_PAYLOAD, FK_TIME».)

§62: replace the flow (L1701–L1725)

```text
1. Validar las opciones (§62.1): entre 1 y 16 credenciales —los
   recipients X25519 dados y, si se pide, una clave portable—, sin
   repetir ninguna, canónicas y no de orden bajo (§37); el resto,
   como en §61.
2. Resolver DateKey localmente.
3. Generar capsule_id.
4. Generar I_PAYLOAD X25519 nueva.
5. Si se pide una clave portable: generar I_ACCESS (§38).
6. Completar 16 recipients con señuelos (§39): por cada hueco libre,
   generar una identity X25519 nueva, tomar su recipient y descartar
   la identity. Ordenar los 16 al azar (CSPRNG).
7. Construir PUBLIC_HEADER.
8. Medir SEALED_CONTROL_LEN (§62.1): la del OUTER_TIME_AGE del paso
   13, que contiene un INNER_ACCESS_AGE de 16 stanzas.
9. Construir PRELUDE (VERSION = 2).
10. Calcular header_binding.
11. Crear CONTROL_CBOR, versión de schema 2, con los campos de §61,
    paso 9.
12. Crear INNER_ACCESS_AGE:
      age genera FK_ACCESS
      recipients = los 16 del paso 6, en ese orden
      plaintext = CONTROL_CBOR
13. Crear OUTER_TIME_AGE:
      age genera FK_TIME
      recipient = tlock(DateKey)
      plaintext = exact INNER_ACCESS_AGE bytes
    Su longitud MUST ser la del paso 8.
14. SEALED_CONTROL = OUTER_TIME_AGE.
15. Escribir PRELUDE || PUBLIC_HEADER || SEALED_CONTROL.
16. Escribir PAYLOAD_AGE como en §61, paso 13.
17. Si se generó I_ACCESS: escribir I_ACCESS cruda en una .dkk
    (§40 a §43), con el capsule_digest del .dkc completo si se
    incluye.
```

(Keep «Las tres file keys… MUST ser independientes».)

§62.1 (new)

## 62.1 Reglas del escritor

Un escritor conforme con esta versión cumple estas reglas, siga o no el orden de §61 y §62.

MUST:

1. **Formato.** Escribir el formato 2: `VERSION` = 2 (§22) y `CONTROL_CBOR` de versión 2 (§31). MUST NOT escribir el formato 1.
2. **Instante.** Rechazar un `requested_unlock_at` que no sea posterior al instante actual de su reloj. La ronda se resuelve después, con §15.
3. **Credenciales.** Con `time_only`, rechazar cualquier credencial. Con `time_and_key`, exigir entre 1 y 16 credenciales —recipients X25519 y, si se pide, la clave portable (§38)—, rechazar una clave pública repetida y rechazar todo recipient que no sea X25519, que no sea canónico o que sea de orden bajo (§37). Sin credenciales nadie abriría la cápsula; una repetida daría a su identity dos stanzas, y la cápsula fallaría para ella en el paso 13.
4. **Huecos.** Completar los 16 recipients con señuelos y ordenarlos al azar (§39). La clave privada de un señuelo MUST NOT almacenarse, registrarse ni entregarse.
5. **Aleatoriedad.** Obtener de un CSPRNG, nuevos para cada cápsula, `capsule_id` (§21), `I_PAYLOAD` (§29), `I_ACCESS` (§38), `credential_id` (§42), los señuelos y el orden de los stanzas. `I_PAYLOAD` y los señuelos MUST NOT reutilizarse ni derivarse de otro valor.
6. **Contenido.** Conocer L antes de sellar `CONTROL_CBOR`, con L ≤ L_MAX, y usar el código 1 o 2 (§29.1). Si el contenido no mide exactamente L bytes, abortar: la cápsula fallaría en el paso 17, cuando ya no puede repararse.
7. **Longitud del control.** Escribir en `SEALED_CONTROL_LEN` la longitud exacta de `SEALED_CONTROL`. `header_binding` cubre el PRELUDE y va dentro de `CONTROL_CBOR`, así que esa longitud se conoce antes de calcular `header_binding`. La de un fichero `age` depende solo de la longitud de su plaintext y de la forma de sus stanzas, y la de `CONTROL_CBOR` de versión 2 no depende de `header_binding`, `I_PAYLOAD`, L ni el código, todos de tamaño fijo. El escritor MAY medirla sellando antes un `CONTROL_CBOR` provisional de la misma longitud, con `header_binding` e `I_PAYLOAD` a cero, o calcularla con la nota de abajo. En los dos casos MUST comprobar que el sellado real mide lo mismo.
8. **Límites.** Respetar los límites de §57 y las reglas de extensiones de §54 y §72.
9. **Salida.** Ante cualquier error, no entregar lo escrito como una cápsula: descartarlo.

SHOULD:

10. **Código por defecto.** El SDK oficial SHOULD usar el código 2.
11. **Autocomprobación.** Antes de sellar, el escritor SHOULD decodificar su `PUBLIC_HEADER` y su `CONTROL_CBOR` con las reglas del lector (§63, pasos 4 y 14). Después SHOULD comprobar que `INNER_ACCESS_AGE` tiene 16 stanzas X25519 con shares distintos, que cada identity que genera y conserva, como `I_ACCESS`, abre exactamente uno, y que `I_PAYLOAD` abre la cabecera de `PAYLOAD_AGE`. Una cápsula que el lector rechaza solo se descubre tras la fecha.
12. **Borrado.** El escritor SHOULD borrar de la memoria, en cuanto dejan de necesitarse, `I_PAYLOAD`, `CONTROL_CBOR` y sus copias, las copias de `I_ACCESS` que no entrega en la `.dkk`, las claves privadas de los señuelos y los buffers del contenido.

Nota informativa: longitudes en Quicknet. Con `age` estándar, un stanza X25519 mide 98 bytes, y el stanza tlock, 249 + d, con d el número de dígitos decimales de la ronda. Con c(n) = max(1, ⌈n / 65536⌉) y k stanzas X25519 (16 en formato 2):

```text
PAYLOAD_AGE      = 184 + P + 16·c(P)
INNER_ACCESS_AGE = 86 + 98·k + C + 16·c(C)       ; C = |CONTROL_CBOR|
OUTER_TIME_AGE   = 335 + d + n + 16·c(n)          ; n = C en time_only,
                                                  ; |INNER_ACCESS_AGE| en time_and_key
```

Sin extensiones de control, C = 103: en la ronda 1000, `SEALED_CONTROL_LEN` vale 458 en `time_only` y 2128 en `time_and_key`. Con C = 91 y k = 1 o 3, las fórmulas dan las longitudes de los cinco fixtures de formato 1.

§63: step changes

Step 2 →

2. Validar PRELUDE: version, que es el formato (1 o 2), flags,
   reserved, longitudes y límites (§22, §23). Los pasos siguientes
   aplican las reglas de ese formato.

Step 4: after «tipo y versión de schema (§70)» insert «(versión 1 en los dos formatos)».

Step 12 →

12. Verificar que la estructura resultante coincide con access_policy
    (§36). En formato 2 con time_and_key, la cabecera de
    INNER_ACCESS_AGE MUST tener exactamente 16 stanzas (§39)
       → si no, ERR_POLICY_STRUCTURE_MISMATCH.

Step 13, line «MUST existir uno o más stanzas;» →

       MUST existir uno o más stanzas en formato 1, y exactamente 16
       en formato 2;

Step 14 →

14. Parsear CONTROL_CBOR canónico, con las capas de §69.1. En la
    capa 2, su versión de schema (clave 1) MUST ser el formato de la
    cápsula, el VERSION del paso 2 (§31)
       → si no, ERR_UNSUPPORTED_VERSION.
    En la capa 3, las reglas de las claves 6 y 7 (§31): presentes en
    la versión 2 y ausentes en la 1, L de 8 bytes y como mucho
    2⁵³ − 2⁴⁶, código 1 o 2
       → si no, ERR_NON_CANONICAL_CBOR.
    Validar sus extensiones críticas (clave 4) como en el paso 4.

Step 16 →

16. Recuperar I_PAYLOAD. En formato 2, recuperar también L y el código
    de relleno (claves 6 y 7) y calcular P = regla(L) (§29.1). Este
    paso no falla: el paso 14 ya validó las dos claves.

Step 17: append

    En formato 2, además (§29.1):
       el plaintext MUST medir exactamente P bytes;
       sus bytes L a P − 1 MUST valer 0x00;
           → si no, ERR_INTEGRITY.
    Un lector en streaming conoce P antes de descifrar el primer byte
    (paso 16): MAY fallar en cuanto el plaintext supera P bytes o
    aparece un byte de relleno distinto de 0x00; un plaintext de menos
    de P bytes se detecta al final del STREAM. Todos estos fallos
    tienen el mismo código, así que el momento en que se detectan no
    lo cambia.

Step 18 →

18. Commit del resultado solo si el paso 17 termina sin error, con
    todas sus comprobaciones:
       formato 1 → el plaintext completo de PAYLOAD_AGE;
       formato 2 → sus L primeros bytes, el contenido. El relleno
       nunca se entrega.

Paragraph at L1902: append to its last sentence «…cumple la política DateKeys V1 y, en formato 2, que INNER_ACCESS_AGE tiene 16 stanzas y que el plaintext de PAYLOAD_AGE tiene la longitud y el relleno de su L (§29.1).»

§64

  • L1932: append a sentence: «La lista vale para los dos formatos: el corpus oficial la aplica a fixtures de formato 2 y conserva los de formato 1 como casos de compatibilidad (§70).»
  • L1939: «version cambiada» → «version cambiada a 3».
  • After the point block and its note (after L1975), add:
y, con el código y el paso de §63 en que fallan, las del formato 2 (§22, §29.1, §31, §39), sobre fixtures de formato 2 salvo donde se indica:

```text
VERSION 3                                          ERR_UNSUPPORTED_VERSION, paso 2
time_only de formato 1 con VERSION 2               ERR_UNSUPPORTED_VERSION, paso 14
time_and_key de formato 1, un stanza, con VERSION 2
                                                   ERR_POLICY_STRUCTURE_MISMATCH, paso 12
time_only con VERSION 1                            ERR_UNSUPPORTED_VERSION, paso 14
time_and_key con VERSION 1                         ERR_UNSUPPORTED_VERSION, paso 14
INNER_ACCESS_AGE con 15 stanzas                    ERR_POLICY_STRUCTURE_MISMATCH, paso 12
INNER_ACCESS_AGE con 17 stanzas                    ERR_POLICY_STRUCTURE_MISMATCH, paso 12
16 stanzas, dos para un mismo recipient            ERR_POLICY_STRUCTURE_MISMATCH, paso 13
identity que no es recipient de ninguno de los 16  ERR_ACCESS_INVALID, paso 13
CONTROL_CBOR de versión 2 sin la clave 6           ERR_NON_CANONICAL_CBOR, paso 14
CONTROL_CBOR de versión 2 sin la clave 7           ERR_NON_CANONICAL_CBOR, paso 14
código de relleno 0                                ERR_NON_CANONICAL_CBOR, paso 14
código de relleno 3                                ERR_NON_CANONICAL_CBOR, paso 14
L = 2⁵³ − 2⁴⁶ + 1                                  ERR_NON_CANONICAL_CBOR, paso 14
último byte de relleno distinto de 0x00            ERR_INTEGRITY, paso 17
plaintext de P − 1 bytes                           ERR_INTEGRITY, paso 17
plaintext de P + 256 bytes                         ERR_INTEGRITY, paso 17
plaintext sin relleno, de L bytes                  ERR_INTEGRITY, paso 17
código 2 cambiado a 1, con L = 78000               ERR_INTEGRITY, paso 17
L − 1, con el último byte del contenido distinto de 0
                                                   ERR_INTEGRITY, paso 17
```

Las mutaciones de `VERSION` editan un byte de un fixture. Las de `INNER_ACCESS_AGE` las construye quien conoce `FK_ACCESS`, como el creador. Las de `CONTROL_CBOR` sellan otro control, lo que en `time_only` puede hacer cualquiera (§36.1); las que cambian su longitud recalculan también el PRELUDE y `header_binding`. Las del plaintext las construye quien conoce `I_PAYLOAD`. Todas conservan MAC válidos: solo las reglas de su paso las rechazan.

§67: after «- resultado esperado de cada etapa de verificación.»

- el formato (1 o 2);
- en formato 2: L, el código de relleno y P esperados, y los 16 stanzas de `INNER_ACCESS_AGE` en `time_and_key`, con el que abre cada credencial;
- en formato 2, el plaintext final esperado son los L primeros bytes del plaintext de `PAYLOAD_AGE`.

Los fixtures de la v0.8.2 se conservan como fixtures de compatibilidad de formato 1: un lector MUST abrirlos con la semántica de la v0.8.2 (§70). Los de formato 2 cubren, como mínimo:

- las dos políticas;
- los dos códigos de relleno, con un mismo L ≥ 8193 para el que dan P distintas;
- L = 0 (P = 256);
- una credencial y 15 señuelos;
- varias credenciales, entre ellas una clave portable;
- 16 credenciales, sin señuelos;
- extensiones en `CONTROL_CBOR`.

Los vectores de las reglas de relleno (§29.1) van en un fichero propio con L, código y P.

§68: after «Al menos un vector oficial .dkk MUST incluir una extensión con data.»

Al menos un vector oficial `.dkk` MUST acompañar a una cápsula de formato 2. El formato de la `.dkk` no cambia (§40).

§69: no change

The catalogue stays as it is, so TestCatalogueMatchesSpec stays green.

§69.1

  • Layer 1 (L2095): «magic, prelude completo, versión de framing,» → «magic, prelude completo, versión de trama —el formato, 1 o 2, en el .dkc (§23), y 1 en la .dkk (§40)—,».
  • Layer 2 (L2096): replace the last sentence with:
Solo entonces una versión que el objeto no admite es `ERR_UNSUPPORTED_VERSION`, sea lo que sea lo que la sigue: claves desconocidas, elementos fuera del perfil, truncado o bytes sobrantes. El Provider Profile, `PUBLIC_HEADER` y la `.dkk` admiten la versión 1; `CONTROL_CBOR`, la versión igual al formato de su cápsula (§31). Es la única regla de esta capa que depende de otro objeto.
  • Layer 3 (L2097): after «el orden y la unicidad de extension_id (§54)», insert «, en CONTROL_CBOR las reglas de las claves 6 y 7 —presentes en la versión 2 y ausentes en la 1, L de 8 bytes y como mucho L_MAX, código 1 o 2 (§31)—».
  • Cross-object paragraph (L2106): replace «la estructura frente a access_policy (paso 12) y header_binding (paso 15)» with «la estructura frente a access_policy y, en formato 2, los 16 stanzas de INNER_ACCESS_AGE (paso 12), header_binding (paso 15), y la longitud y el relleno del plaintext de PAYLOAD_AGE frente a L y el código (paso 17)».
  • "Alcance" (L2108), last sentence: append «, ni el momento en que un lector en streaming detecta un fallo del paso 17».
  • Examples table: add rows
| Cápsula de formato 2 con 15 stanzas en `INNER_ACCESS_AGE` y una identity que abre uno | `ERR_POLICY_STRUCTURE_MISMATCH`, paso 12 |
| `CONTROL_CBOR` de versión 1 en una cápsula de formato 2, con una clave desconocida | `ERR_UNSUPPORTED_VERSION`, paso 14 |
| Código de relleno 3 y una extensión crítica desconocida en `CONTROL_CBOR` | `ERR_NON_CANONICAL_CBOR`, paso 14 |
| Código de relleno 3 y el `header_binding` de otra cabecera | `ERR_NON_CANONICAL_CBOR`, paso 14 |
| Extensión crítica desconocida en `CONTROL_CBOR` y un byte de relleno distinto de 0x00 | `ERR_EXTENSION_CRITICAL_UNKNOWN`, paso 14 |
| Byte de relleno distinto de 0x00 y un stanza adicional en `PAYLOAD_AGE` | `ERR_POLICY_STRUCTURE_MISMATCH`, paso 6 |

§70: replace the list

Una implementación V1:

- MUST aceptar `DKC1` con `VERSION` 1 o 2, y `DKK1` con `VERSION` 1;
- MUST rechazar cualquier otra versión de trama (§23, §40) y toda versión de schema que el objeto no admita (§69.1);
- MUST abrir todo objeto válido de formato 1 con la semántica de la v0.8.2: uno o más stanzas en `INNER_ACCESS_AGE` y el plaintext completo de `PAYLOAD_AGE`, sin relleno;
- MUST escribir el formato 2 y MUST NOT escribir el formato 1 (§62.1);
- SHOULD indicar al llamador el formato de una cápsula abierta o inspeccionada: el formato 1 no oculta el número de credenciales ni la longitud exacta del contenido (§55.2);
- MUST rechazar critical extensions desconocidas;
- MAY ignorar noncritical extensions desconocidas;
- MUST mantener inmutable la interpretación de perfiles publicados.

Un lector que solo conoce el formato 1, como los de la v0.8.2, rechaza una cápsula de formato 2 en el paso 2, con `ERR_UNSUPPORTED_VERSION` y sin ninguna petición de red. Si alguien cambia su `VERSION` a 1, falla en el paso 14, también con `ERR_UNSUPPORTED_VERSION`, porque su `CONTROL_CBOR` tiene la versión 2. En ningún caso entrega el contenido con su relleno.

§72: after the bullet about objects and arrays

Una extensión registrada en `CONTROL_CBOR` vale en sus dos versiones de schema, salvo que su registro diga otra cosa.

§73

  • Title → «Decisiones canónicas v0.9».
  • Replace these entries:
DKC framing
= sin PAYLOAD_LEN; VERSION es el formato de la cápsula: 1 (v0.8.2)
  o 2 (v0.9); se escribe solo el 2

payload
= fichero age v1 estándar completo; en formato 2, su plaintext es el
  contenido seguido de ceros hasta P = regla(L)

payload access
= X25519 identity I_PAYLOAD dentro de CONTROL_CBOR, nueva de un CSPRNG
  para cada cápsula

time_and_key
= age(tlock → age(X25519 recipient(s) → CONTROL_CBOR)); en formato 2,
  exactamente 16 recipients: de 1 a 16 credenciales y señuelos, en
  orden aleatorio
  • Add these entries (before «recovery»):
payload padding
= códigos 1 (bloque256) y 2 (reforzado, con Padmé), sin opción sin
  relleno; L y el código en las claves 6 y 7 de CONTROL_CBOR versión 2,
  L en 8 bytes y como mucho 2^53 − 2^46; comprobado en el paso 17
  (ERR_INTEGRITY); el SDK usa el 2 por defecto

compatibility
= un lector v0.9 abre el formato 1 con la semántica de la v0.8.2; un
  lector v0.8.2 rechaza el formato 2 en el paso 2, sin red

privacy
= §55.2: visibles la fecha, access_policy, capsule_id y P; ocultos L,
  el número de credenciales y los recipients

writer rules
= §62.1: MUST para formato, credenciales, señuelos, aleatoriedad, L y
  SEALED_CONTROL_LEN; SHOULD para autocomprobación y borrado

§74

L2313 → «El framing base, la ausencia de PAYLOAD_LEN, el uso de age files estándar, la identity X25519 cruda de .dkk, el formato de extensiones (§54), los 16 huecos de INNER_ACCESS_AGE (§39) y las reglas de relleno 1 y 2 con la codificación de L (§29.1, §31) dejan de considerarse provisionales en este borrador.» Then, after the limits paragraph (L2327), add:

Quedan fuera de la v0.9, como trabajo futuro que esta versión no especifica:

- el formato de un objeto de release y de su fuente de archivo;
- una nueva redacción del modelo de amenazas del proveedor (§7.6);
- una extensión de firma;
- el uso del reloj local en el paso 9.c de §63;
- un tipo de acceso post-cuántico.

The limits table needs no new row. 16 and L_MAX are normative, not implementation limits.

§75

  • 5 → «5. fixtures oficiales .dkc de descifrado/validación, de formato 2 y de compatibilidad de formato 1.»
  • 7 → «7. mutation tests completos, en los dos formatos.»
  • Add «11. vectores de las reglas de relleno (§29.1).»

§76: insert after L2447, before ---

Existing entries and the title stay untouched.

### Cambios normativos de la v0.9

La v0.9 añade el formato 2 de `.dkc`, que oculta hasta la fecha la longitud exacta del contenido y el número de credenciales, y pasa a texto normativo las reglas del escritor. Sus cambios normativos:

1. **Formato de la cápsula** (§22, §23, §24, §31, §63, §69.1, §70). `VERSION` del PRELUDE es el formato: 1, el de la v0.8.2, o 2. `CONTROL_CBOR` gana la versión de schema 2, que MUST coincidir con el formato (`ERR_UNSUPPORTED_VERSION`, paso 14). `PUBLIC_HEADER` y la `.dkk` conservan la versión 1. Un escritor MUST escribir el formato 2, y un lector MUST abrir los dos, el 1 con la semántica de la v0.8.2.
2. **16 huecos en `INNER_ACCESS_AGE`** (§33, §36, §38, §39, §63 pasos 12 y 13). En formato 2, `time_and_key` lleva exactamente 16 stanzas X25519: de 1 a 16 credenciales y, en los huecos libres, señuelos, en orden aleatorio. Otro número es `ERR_POLICY_STRUCTURE_MISMATCH` en el paso 12.
3. **Relleno del payload** (§29, §29.1, §30.1, §31, §63 pasos 14 a 18). En formato 2 el plaintext de `PAYLOAD_AGE` es el contenido seguido de ceros hasta P = regla(L), con los códigos 1 (`bloque256`) y 2 (`reforzado`, con Padmé); no hay regla sin relleno. L y el código van sellados en las claves 6 y 7 de `CONTROL_CBOR`, L en 8 bytes y como mucho 2⁵³ − 2⁴⁶. Un plaintext de otra longitud o con un byte de relleno distinto de 0x00 es `ERR_INTEGRITY` en el paso 17, y el lector entrega solo los L primeros bytes.
4. **Consideraciones de privacidad** (§4, §5, §55.2). Qué revela una cápsula, antes y después de la fecha, y a quién. `access_policy` sigue visible, para que el paso 9 pueda fallar sin red.
5. **Reglas del escritor** (§15, §29, §37, §39, §57, §61, §62, §62.1). Orden de escritura sin circularidad; `I_PAYLOAD` nueva de un CSPRNG; de 1 a 16 credenciales, sin repetir, canónicas y no de orden bajo; instante pedido posterior al reloj del escritor; L conocida antes de sellar; autocomprobación y borrado de secretos (SHOULD).

Ningún código de error es nuevo (§69).

Casos reproducibles que lo justifican, obtenidos con la implementación de referencia v0.8.2, sus fixtures y las librerías `age` de Go (`filippo.io/age` 1.3.2) y TypeScript (`age-encryption` 0.3.1):

1. **Número de credenciales.** `SEALED_CONTROL_LEN` (bytes 12 a 15 del PRELUDE) vale 646 en `time_and_key_portable.dkc`, con una credencial, y 842 = 646 + 2·98 en `time_and_key_recipients.dkc`, con tres: cualquiera cuenta las credenciales antes de la fecha, y tras el release en la cabecera de `INNER_ACCESS_AGE`. En formato 2, con la ronda 1000 y sin extensiones de control, vale siempre 2128 en `time_and_key` y 458 en `time_only`.
2. **Longitud del contenido.** `time_only.dkc` mide 78 799 bytes y su `PAYLOAD_AGE`, 78 216 = 184 + 78 000 + 2·16: cualquiera lee L = 78 000. El `PAYLOAD_AGE` de `empty_payload.dkc`, de 200 bytes, da L = 0. En formato 2 con el código 2, un `PAYLOAD_AGE` de 80 088 bytes corresponde a cualquier L entre 77 825 y 79 872.
3. **L de longitud fija.** Con L como entero CBOR en su forma más corta (§58), su valor ocuparía 1, 2, 3, 5 o 9 bytes, y `SEALED_CONTROL_LEN` lo revelaría: en el hueco P = 256, el de los mensajes cortos, distinguiría L < 24, 24 ≤ L ≤ 255 y L = 256; en P = 65 536, L = 65 536 del resto. Con 8 bytes fijos, un `CONTROL_CBOR` de versión 2 sin extensiones mide siempre 103 bytes.
4. **Versión visible.** La mutación «version changed» de `mutations.json` (`[4,1,"02"]` sobre `time_only.dkc`) da `ERR_UNSUPPORTED_VERSION` en el paso 2, sin red, con la implementación de referencia v0.8.2: un lector v0.8.2 rechaza el formato 2 antes de pedir el release. Sin un cambio visible de versión, lo pediría y fallaría en el paso 14 por las claves 6 y 7, desconocidas en su mapa cerrado (§58). Se cambia `VERSION` y no la versión de `PUBLIC_HEADER` porque es la primera comprobación, porque el cambio es de toda la cápsula y la cabecera no cambia, y porque así siguen valiendo los vectores «schema version 2» de `PUBLIC_HEADER` en `cbor.json`, la mutación «header schema version changed» y el ejemplo de §69.1 «Versión 2, una clave desconocida y una DateKey inválida». Un formato 2 con `VERSION` 1 falla en el paso 14 en los dos lectores, por la versión 2 de su control.
5. **Orden de escritura.** §61 y §62 construían el PRELUDE (paso 6) antes que `SEALED_CONTROL` (pasos 9 a 11), cuya longitud lleva el PRELUDE, y `header_binding`, que cubre el PRELUDE, va dentro del control sellado: el orden era circular. `capsule.Encrypt` lo resuelve sellando antes un control provisional de la misma longitud, algo que el texto no decía.
6. **`I_PAYLOAD` nueva.** Si dos cápsulas compartieran `I_PAYLOAD`, «SEALED_CONTROL_A + PAYLOAD_AGE_B» (§64) se abriría en lugar de fallar en el paso 17. §29 solo decía «aleatoria».
7. **Recipients no canónicos y de orden bajo.** `filippo.io/age` y `age-encryption` aceptan un recipient X25519 con el bit 255 a 1 o con u ≥ p y cifran para él un stanza que ninguna identity abre: el salt de HKDF lleva los 32 bytes recibidos, y la identity usa su clave pública canónica. Con un recipient de orden bajo, las dos librerías fallan al cifrar. El lector no puede detectar ninguno de los dos casos.
8. **Credenciales y reloj del escritor.** La referencia ya rechaza un instante pedido que no es posterior a su reloj, `time_only` con credenciales, `time_and_key` sin ellas y un recipient repetido (`capsule.TestEncryptRejectsInvalidOptions`), sin texto normativo que lo exija. Un recipient repetido daría a su identity dos stanzas, y la cápsula fallaría para ella en el paso 13 (mutación «two INNER_ACCESS_AGE stanzas for one recipient»).
9. **Orden de los stanzas.** `age` escribe los stanzas en el orden de sus recipients, y `capsule.Encrypt` añade `R_ACCESS` al final. Con los señuelos detrás de las credenciales, quien abre el stanza k sabría que hay al menos k credenciales.
10. **L conocida antes de sellar.** `capsule.Encrypt` cifra en streaming una fuente de longitud desconocida. En formato 2, L se sella antes que el payload: si la fuente entrega otra longitud, la cápsula fallaría en el paso 17, cuando ya no puede repararse.
11. **Autocomprobación.** El caso 5 de la v0.8.2: `Encrypt` selló un `CONTROL_CBOR` que el lector rechazaba en el paso 14, con la cápsula ya desbloqueada.

Ningún objeto válido de la v0.8.2 cambia de veredicto. Los cinco fixtures y las mutaciones construidas por el creador, todos de formato 1, quedan como casos de compatibilidad (§70), y ninguno de los 1 825 casos de `inspect_differential.json` produce `VERSION` 2. Cambian estos datos de prueba:

- la mutación «version changed» pasa a `VERSION` 3 (`[4,1,"03"]`): con `VERSION` 2, ese `time_only.dkc` de formato 1 llega al paso 14 (`ERR_UNSUPPORTED_VERSION`), un caso que pasa a la lista nueva de §64;
- los vectores `control_cbor` de `cbor.json` dependen del formato: «schema version 2» es `ERR_UNSUPPORTED_VERSION` en formato 1 y `ERR_NON_CANONICAL_CBOR` en formato 2, donde le faltan las claves 6 y 7;
- `testdata/README.md` cita esa mutación, la «framing version» y el número de mutaciones de §64.

Reproducirán cada caso, cuando la implementación de referencia implemente la v0.9, los tests (TODO, nombres provisionales) `capsule.TestFormatDispatch`, `TestFormatRelabel`, `TestFormat1Compatibility`, `TestInnerHasSixteenStanzas`, `TestDummyRecipients`, `TestCredentialBounds`, `TestPaddingRules`, `TestPaddingChecksAtStep17`, `TestPaddingAcrossChunks`, `TestControlLengthIsConstant`, `TestSealedControlLength`, `TestPayloadIdentityReuse` y `TestEncryptSourceLength`; `capsule.TestEncryptRejectsInvalidOptions`, `TestExportedMutationCorpus`, `TestTrustModel` y `TestDecodeControlRejects`, ampliados; y `agewrap.TestNonCanonicalRecipients`.

§77: add

- K. Nikitin, L. Barman, W. Lueks, M. Underwood, J.-P. Hubaux, B. Ford — «Reducing Metadata Leakage from Encrypted Files and Communication with PURBs», Proceedings on Privacy Enhancing Technologies 2019(4), pp. 6–33: función de relleno Padmé (§29.1)  
  https://doi.org/10.2478/popets-2019-0056

- RFC 7748 — X25519: codificación de u, recorte de escalares y puntos de orden bajo (§37)

9. datekeys.cddl

Replace lines 1–4:

; DateKeys Protocol Specification v0.9 (working draft) - CBOR schemas (RFC
; 8610 CDDL).
;
; Normative companion of spec/DateKeys_Protocol_Specification_v0.9.md. The
; reference implementation g.activething.com/go/DateKeys still implements
; v0.8.2, whose schemas are in tag spec-v0.8.2. These schemas include both
; control versions: 1, of capsule format 1 (v0.8.2), and 2, of format 2.

Add to the rules block, before the "When bytes break several rules" bullet:

;   - The schema version of a control (key 1) is the format of its capsule,
;     the VERSION of the PRELUDE (spec section 22): control-v1 only in format
;     1, control-v2 only in format 2. A decoder picks the rule by the format,
;     and another version is ERR_UNSUPPORTED_VERSION, read before the rest of
;     the schema (spec section 69.1, layer 2). CDDL cannot express that link.
;   - payload-length holds L as an unsigned 64-bit big-endian integer in
;     exactly 8 bytes, whatever its value; the value is at most
;     max-payload-length (spec section 29.1, 31). A larger value is
;     ERR_NON_CANONICAL_CBOR, like any rule of this schema.

In the last bullet: «then type tag and schema version (keys 0 and 1)» → «then type tag and schema version (keys 0 and 1; for a control, the version of its capsule format)».

Replace lines 77–86:

; Spec section 31. Sealed inside OUTER_TIME_AGE (time_only) or inside
; INNER_ACCESS_AGE inside OUTER_TIME_AGE (time_and_key).
control = control-v1 / control-v2

; Capsule format 1 (v0.8.2).
control-v1 = {
  0 => "datekeys-control",
  1 => 1,
  2 => bstr .size 32,      ; header_binding
  3 => bstr .size 32,      ; payload_identity, raw X25519 identity I_PAYLOAD
  ? 4 => extensions,       ; critical_extensions
  ? 5 => extensions,       ; noncritical_extensions
}

; Capsule format 2 (v0.9). 103 bytes without extensions.
control-v2 = {
  0 => "datekeys-control",
  1 => 2,
  2 => bstr .size 32,      ; header_binding
  3 => bstr .size 32,      ; payload_identity, raw X25519 identity I_PAYLOAD
  ? 4 => extensions,       ; critical_extensions
  ? 5 => extensions,       ; noncritical_extensions
  6 => payload-length,     ; L, the length of the content (spec section 29.1)
  7 => padding-scheme,     ; the padding rule of PAYLOAD_AGE (spec section 29.1)
}

; Fixed width, so that the length of CONTROL_CBOR, visible in
; SEALED_CONTROL_LEN, never depends on L (spec section 55.2). Value at most
; max-payload-length (see the rules above).
payload-length = bstr .size 8
padding-scheme = &(bloque256: 1, reforzado: 2)

; 2^53 - 2^46: the largest L whose padded length P stays within
; max-safe-uint under both padding rules (spec section 29.1).
max-payload-length = 8936830510563328

Change the comment at L65–66 to add: «The same header, schema version 1, in both capsule formats.»


10. spec/README.md

Keep the v0.8.2 entry. Add after it:

- `DateKeys_Protocol_Specification_v0.9.md`: working draft v0.9 (29 September
  2026), not frozen and not yet implemented: the reference implementation and
  the fixtures still follow v0.8.2. It adds capsule format 2 (PRELUDE VERSION
  2): exactly 16 recipient slots in INNER_ACCESS_AGE, filled with dummy
  recipients in random order; padding of the PAYLOAD_AGE plaintext (codes 1,
  bloque256, and 2, reforzado with Padmé), with the real length and the code
  sealed in CONTROL_CBOR version 2; privacy considerations; and normative
  writer rules. Readers keep opening format 1 with v0.8.2 semantics. The
  changes and their reproducible cases are in its §76.

Replace the CDDL bullet:

- `datekeys.cddl`: the CBOR schemas of the v0.9 draft, both control versions
  included, with the encoding rules CDDL cannot express. The schemas of
  v0.8.2 as implemented are in tag `spec-v0.8.2`.

The text «draft v0.9 (» is what TestSpecVersionNamesTheSpecification will need once SpecVersion becomes "0.9". The v0.8.2 entry keeps the current test green.


11. Tests and test data (to be generated later by the Go reference)

mutations.json. "version changed" becomes [4,1,"03"] (step 2, network:false). New cases, all spec:true, all with network:true except VERSION 3:

Name Base / built by Code Step
format 1 time_only relabeled format 2 time_only.dkc [4,1,"02"] ERR_UNSUPPORTED_VERSION 14
format 1 time_and_key relabeled format 2 time_and_key_portable.dkc + .dkk, [4,1,"02"] ERR_POLICY_STRUCTURE_MISMATCH 12
format 2 time_only relabeled format 1 format2_time_only.dkc [4,1,"01"] ERR_UNSUPPORTED_VERSION 14
format 2 time_and_key relabeled format 1 format2_time_and_key_portable.dkc + .dkk ERR_UNSUPPORTED_VERSION 14
15 / 17 INNER_ACCESS_AGE stanzas frozen, creator ERR_POLICY_STRUCTURE_MISMATCH 12
two of the 16 stanzas for one recipient frozen, creator ERR_POLICY_STRUCTURE_MISMATCH 13
identity that is not a recipient of the 16 format2_time_and_key_portable.dkc, foreign identity ERR_ACCESS_INVALID 13
control v2 without key 6 / key 7 frozen, resealed, PRELUDE recomputed ERR_NON_CANONICAL_CBOR 14
padding code 0 / 3 frozen, resealed ERR_NON_CANONICAL_CBOR 14
payload_length 2^53 − 2^46 + 1 frozen, resealed ERR_NON_CANONICAL_CBOR 14
non-zero last padding byte frozen, I_PAYLOAD ERR_INTEGRITY 17
payload plaintext of P − 1 / P + 256 bytes / without padding frozen, I_PAYLOAD ERR_INTEGRITY 17
padding code 2 changed to 1, L = 78000 frozen, resealed ERR_INTEGRITY 17
payload_length L − 1, last content byte non-zero frozen, resealed ERR_INTEGRITY 17

Also add the §69.1 precedence examples as reference tests (TestPrecedenceAcrossSteps, extended).

cbor.json:

  • control_cbor vectors gain "format" (default 1).
  • New format-2 vectors:
    • accepted: minimal (103 bytes, hex in §2), both extension arrays, L = 0, L = L_MAX;
    • rejected with ERR_NON_CANONICAL_CBOR: L = L_MAX + 1, L = 2⁶⁴ − 1, payload_length of 7 or 9 bytes, payload_length as a uint, padding 0, padding 3, padding as a bstr, missing key 6, missing key 7, and "padding 3 and an unknown critical extension" (layer 3 before layer 4);
    • rejected with ERR_UNSUPPORTED_VERSION: "format 2: schema version 1 with keys 6 and 7", "format 2: version 3", "format 1: version 2 with keys 6 and 7".
  • Also "format 1: version 1 with keys 6 and 7" (ERR_NON_CANONICAL_CBOR).
  • "unknown key 6" keeps its verdict in format 1.

testdata/vectors/padding.json (new): {L, bloque256, reforzado, payload_age_length} for every row of §3.4, plus l_max, and informational E, S and lastBits.

Fixtures (format 2, round 1000):

  • format2_time_only.dkc: code 2, L = 78000, P = 79872.
  • format2_time_only_bloque256.dkc: same content, code 1, P = 78080.
  • format2_empty_payload.dkc: L = 0, P = 256.
  • format2_time_only_extensions.dkc: header and control extensions.
  • format2_time_and_key_portable.dkc and .dkk: 1 credential and 15 dummies.
  • format2_time_and_key_recipients.dkc and .dkk: 3 recipients plus a portable key, 12 dummies.
  • format2_time_and_key_sixteen.dkc: 16 recipients, no dummies.

Sidecars gain format, payload_length, padding, padded_length, and the stanza index each credential opens. Padding that spans chunks (L = 5 000 000, code 2 → P = 5 111 808, a full chunk of zeros) is better as a test generated at run time than as a 5 MB fixture.

Unchanged: the 5 format-1 fixtures and the crafted format-1 mutations, and inspect_differential.json. None of its 1825 cases yields VERSION 2 (its byte-4 edits give a7, 05, e8 and 20). Format-2 bases, if wanted, go in a new block.


12. Points for the author to confirm

  1. Encoding of L: bstr .size 8 (recommended) instead of a CBOR uint, which would add the width leak to §55.2.
  2. Reader and format: readers SHOULD report the format (format 1 leaks more). Alternatively MAY.
  3. §4 and §5: goal 8 and the new non-goal are optional but short.
  4. Test builders: "MUST NOT write format 1" binds conforming writers. New format-1 test objects, if any, come from a test-only builder.
  5. Fixture names: the format2_ prefix is a proposal.

Powered by TurnKey Linux.