|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
### Borrador normativo v0.9
|
|
|
|
|
|
|
|
|
|
|
|
**Estado:** Draft / pre-estándar
|
|
|
|
|
|
**Fecha:** 29 septiembre 2026
|
|
|
|
|
|
````
|
|
|
|
|
|
|
|
|
|
|
|
### §1: add to the list
|
|
|
|
|
|
|
|
|
|
|
|
After «- recipient X25519 V1;»:
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
- formatos 1 y 2 de `.dkc` y su compatibilidad;
|
|
|
|
|
|
- huecos fijos de `INNER_ACCESS_AGE` y relleno del payload;
|
|
|
|
|
|
````
|
|
|
|
|
|
|
|
|
|
|
|
After «- verificación;»:
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
- reglas del escritor;
|
|
|
|
|
|
````
|
|
|
|
|
|
|
|
|
|
|
|
Change «- extensiones genéricas.» to «- extensiones genéricas;» and add:
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
- consideraciones de privacidad.
|
|
|
|
|
|
````
|
|
|
|
|
|
|
|
|
|
|
|
### §4: add goal 8
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
- 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.»
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
`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:
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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…»
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
`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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
## 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)
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
## 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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
El relleno de §29.1 forma parte del plaintext de `age`: no existe un subformato DateKeys para él.
|
|
|
|
|
|
````
|
|
|
|
|
|
|
|
|
|
|
|
### §30.1: add at the end
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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.»)
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
`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:
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
Los señuelos de §39 no añaden ninguna barrera de acceso ni ninguna autenticidad.
|
|
|
|
|
|
````
|
|
|
|
|
|
|
|
|
|
|
|
### §37: add after «No se define un KEM propio.»
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
## 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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
## 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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
- 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)
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
## 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)
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
```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)
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
## 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 →
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
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 →
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
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;» →
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
MUST existir uno o más stanzas en formato 1, y exactamente 16
|
|
|
|
|
|
en formato 2;
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Step 14 →
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
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 →
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
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 →
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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.»
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
- 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`.»
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
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»):
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
### 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
|
|
|
|
|
|
|
|
|
|
|
|
````markdown
|
|
|
|
|
|
- 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:
|
|
|
|
|
|
|
|
|
|
|
|
```markdown
|
|
|
|
|
|
- `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:
|
|
|
|
|
|
|
|
|
|
|
|
```markdown
|
|
|
|
|
|
- `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.
|