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.
DateKeys/docs/traceability.md

175 lines
24 KiB

# Traceability: DateKeys Protocol Specification v0.8.2 ↔ datekeys-go
This table maps every normative section of the specification to the code that
implements it and to the tests that exercise it. It is updated in the same
change as any normative code, and it is the document handed to the external
reviewer together with the specification, the fixtures and the mutation corpus
(plan §10).
Paths are relative to the repository root. `§` numbers refer to
`spec/DateKeys_Protocol_Specification_v0.8.2.md`.
## Section map
| § | Topic | Implementation | Tests |
|---|---|---|---|
| 3 | Guiding principle: verify locally | `profile.Registry`, `datekey.Resolve`, `provider.Verify`, `capsule.Inspect` | `capsule.TestMutationCorpus` |
| 4 | Security goals | whole module | whole suite |
| 7 | Threat model | creator model in `internal/testkit.Build`, `RewriteAge`; third-party edits in the mutation corpus | `agewrap.TestTimeIdentityStrictness`, `TestPayloadIdentityStrictness`, `TestAccessIdentityStrictness`, `capsule.TestMutationCorpus` |
| 9 | Provider abstraction | `provider.Condition`, `provider.Release`, `provider.ReleaseSource` | `provider/*` |
| 10 | Provider Profile | `profile.Profile`, `Profile.Validate` | `profile.TestValidateRejectsTamperedProfiles` |
| 11 | Canonical profile encoding, `profile_hash`; `period` in 1..2^53−1, `genesis_time` in 0..2^53−1 | `Profile.CanonicalCBOR`, `Profile.Hash`, `profile.Decode` (hand-written `wire` encode and decode: keys 0 to 10, all required, in order; unsigned `genesis_time`, `codec.MaxSafeUint`; `period` limited to 1..86400 s, an implementation limit marked in `spec/datekeys.cddl`, `ERR_NON_CANONICAL_CBOR`) | `profile.TestQuicknetMatchesGoldenVector`, `TestQuicknetCBORLayout`, `TestDecodeRoundTrip`, `TestDecodeStructure`, `TestIntegerRanges`, `FuzzDecode`; `testdata/vectors/profile_quicknet.json`; the `provider_profile` block of `testdata/vectors/cbor.json` (*period of one day, the implementation limit*, *… above the implementation limit*) |
| 12 | Quicknet Provider Profile V1 | `profile.Quicknet`, `profile.Quicknet*` constants | `profile.TestQuicknetMatchesGoldenVector` |
| 13 | Root of trust | `profile.NewRegistry`, `profile.Pin`, `profile.Default`, `QuicknetProfileHash`; chain-hash self-check in `Profile.Validate` (the drand chain-info hash, formula in `testdata/README.md`) | `profile.TestRegistry`; mutations *unknown profile*, *empty registry*; the `provider_profile` block of `testdata/vectors/cbor.json` |
| 14 | DateKey | `datekey.DateKey` | `datekey/*` |
| 15 | Date → round resolution | `datekey.Resolve`, `datekey.RoundTime` | `datekey.TestGoldenRoundVectors`, `TestRoundNeverOpensEarly`, `TestResolveProperty`, `TestTimezoneIndependence` |
| 16 | Normative round vector | — | `datekey.TestNormativeRoundVector`; `testdata/vectors/quicknet_rounds.json` |
| 17 | Past-round attack | `provider.Verify` (round equality), `capsule.Encrypt` (round time ≥ requested), `agewrap.CheckTimeStanzas` | `provider.TestVerifyRejects`; mutations *DateKey A + release of round B*, *tlock stanza round differs from DateKey.round* |
| 18 | `dk1_` representation | `DateKey.CanonicalJSON`, `DateKey.Compact` | `datekey.TestGoldenDK1Vectors`, `TestNormativeRoundVector` |
| 19 | `dk1_` canonicality | `datekey.Parse` | `datekey.TestGoldenDK1Vectors`, `TestNumberSpellings`, `FuzzParse`; mutation *non-canonical dk1_ JSON*; `testdata/vectors/dk1.json` |
| 20 | File extensions and magic | magic checks in `capsule.ParsePrelude`, `accesskey.Decode` | mutation *a .dkk offered as a .dkc*; `accesskey.TestDecodeRejects` *a .dkc* |
| 21 | `capsule_id` | `capsule.Encrypt` (16 bytes from `crypto/rand`), `capsule.DecodeHeader` | `capsule.TestPortableKeysAreNeverReused` |
| 22 | `.dkc` framing | `capsule.Prelude`, `capsule.ParsePrelude` | mutations *version changed*, *flags != 0*, *reserved != 0*, *magic*, length limits; `capsule.FuzzParsePrelude` |
| 23 | PRELUDE | `Prelude.Bytes` | `capsule.TestConformanceFixtures` |
| 24 | PUBLIC_HEADER; keys 5 and 6 optional, 1 to 64 extensions each | `capsule.Header`, `EncodeHeader`, `DecodeHeader` (hand-written `headerWire` encode and decode; CDDL checked before the DateKey) | `capsule.TestConformanceFixtures` (exact extension data), `TestDecodeHeaderRejects`, `TestDecodeMapStructure`, `TestDecodeHeaderReportsTheCDDLFirst`, `FuzzDecodeHeader`, `FuzzEncodeImpliesDecode`; mutations *header schema version changed*, *unknown key in PUBLIC_HEADER* |
| 25 | Declared access policy | `capsule.Policy`; `capsule.DecodeHeader` (the value read, up to 2^53−1, must be 0 or 1 before any narrowing); `capsule.Open` step 12 | `capsule.TestDecodeMapStructure` (2, 255, 256, 257, 2^32, 2^53−256 and others), `FuzzDecodeHeader` (seeds 256, 257, 2^32); mutations *access_policy=… with … structure* (four cases), *undefined access_policy*, *access_policy 256 / 257 with a consistent header_binding* |
| 26 | Header binding | `capsule.HeaderBinding`; `capsule.Open` step 15 | `capsule.TestConformanceFixtures`; mutation *PUBLIC_HEADER_A + SEALED_CONTROL_B* |
| 27 | Pre-unlock validation | `capsule.Inspect` (steps 1–8), `agewrap.Stanzas` probe | `capsule.TestMutationCorpus` (no release request for any pre-unlock failure), `FuzzInspect` |
| 28 | Three age files | `capsule.Encrypt`, `capsule.Open` | `capsule.TestEncryptRoundTripBothPolicies` |
| 29 | PAYLOAD_AGE | `capsule.Encrypt` step 4; `agewrap.PayloadIdentity`, `agewrap.CheckPayloadStanzas` | `agewrap.TestPayloadIdentityStrictness`; mutation *extra stanza in PAYLOAD_AGE* |
| 30 | PAYLOAD_AGE is a complete age file | `filippo.io/age` public API only | `capsule.TestInteropAgeOpensPayload` (`-tags interop`, official `age` CLI) |
| 30.1 | CONTROL_CBOR ↔ PAYLOAD_AGE binding | `agewrap.PayloadIdentity` | mutation *SEALED_CONTROL_A + PAYLOAD_AGE_B*; `agewrap.TestPayloadIdentityStrictness` |
| 31 | CONTROL_CBOR; keys 4 and 5 optional; extension entry rules | `capsule.Control`, `EncodeControl`, `DecodeControl` (hand-written `controlWire` encode and decode); `extension` | `capsule.TestConformanceFixtures` (exact extension data), `TestDecodeControlRejects`, `TestDecodeMapStructure`, `FuzzDecodeControl`, `FuzzEncodeImpliesDecode`; mutation *unknown critical CONTROL_CBOR extension* |
| 32 | `time_only` | `capsule.Encrypt`; `agewrap.TimeRecipient` | fixtures `time_only*`, `empty_payload`; `capsule.TestInteropTleOpensSealedControl` (`-tags interop`, official `tle` CLI) |
| 33 | `time_and_key` | `capsule.Encrypt` (`seal`); `agewrap.AccessIdentity` | fixtures `time_and_key_*`; `capsule.TestEncryptRoundTripBothPolicies` |
| 34 | SEALED_CONTROL | `capsule.Encrypt`; `capsule.Open` step 11 | `capsule.TestConformanceFixtures` |
| 35 | tlock strict mode | `agewrap.TimeRecipient`, `agewrap.TimeIdentity` (pinned parameters only, exact stanza arguments) | `agewrap.TestTimeIdentityStrictness`, `TestInteroperabilityWithTlockLibrary`, `TestTimeIdentityRelease` |
| 36 | Policy ↔ structure | `capsule.Open` step 12, `agewrap.CheckAccessStanzas` | mutations *access_policy=…* (four cases), *non-X25519 stanza in INNER_ACCESS_AGE* |
| 36.1 | Authenticity semantics | documented in `README.md`, `SECURITY.md` | — (a property the protocol does not provide) |
| 37 | X25519 recipient V1 | `age.X25519Recipient`; `agewrap.X25519IdentityFromRaw` | `agewrap.TestRawKeys` |
| 38 | Portable Access Key | `EncryptOptions.NewPortableKey` (fresh `I_ACCESS` per capsule; no API accepts an existing one); `accesskey.AccessKey` | `capsule.TestPortableKeysAreNeverReused` |
| 39 | Multiple recipients | `capsule.Encrypt`; `agewrap.AccessIdentity` | `capsule.TestFixtureRecipients`, `TestEncryptRoundTripBothPolicies` |
| 40 | `.dkk` framing | `accesskey.Encode`, `accesskey.Decode` (the body buffer grows with the data read; every buffer holding the body is wiped) | `accesskey.TestDecodeRejects`, `TestDecodeShortBodyAllocatesLittle`, `TestEncodeAndDecodeLeaveNoStaleMaterial`, `FuzzDecode` |
| 41 | `.dkk` BODY_CBOR | `AccessKey.MarshalBody`, `accesskey.DecodeBody` (hand-written `bodyWire` encode and decode) | `accesskey.TestFixtures`, `TestDecodeBodyStructure` |
| 42 | `credential_id` | `capsule.Encrypt` (16 bytes from `crypto/rand`) | `capsule.TestPortableKeysAreNeverReused` |
| 43 | `verification_metadata` | `accesskey.Verification`, `decodeVerification` (the closed map `{0: capsule_digest}`); `capsule.Open` (`checkCapsuleDigest`, seekable readers) | `accesskey.TestDecodeRejects` *empty verification map*, `TestDecodeBodyStructure`; mutation *capsule_digest of the .dkk does not match* |
| 44 | Application extensions in `.dkk` | `AccessKey.Critical/Noncritical`; `capsule.Open` (`checkAccessKey`, `Opened.UnusableAccessKeyExtensions`) | `accesskey.TestEncodeRejectsAbsenceAsEmptyMap`, `TestDecodeBodyExtensionRules`, `TestFixtureWithExtension`; `capsule.TestAccessKeyFixtureWithExtension`; mutation *known critical .dkk extension with invalid data* |
| 45 | Release API | `provider.ReleaseSource` interface only (server out of scope, plan §2) | — |
| 46 | Release Queue | out of scope (server) | — |
| 47 | Release Cache | every release is verified again: `capsule.Open` step 10 and `agewrap.TimeIdentity` | mutations *release of another round* |
| 48 | Multi-relay | `provider/drand.Client` (race, first *verified* release wins) | `drand.TestRaceWaitsForAValidSignature` |
| 49 | Direct recovery from the provider | `provider/drand` | `drand.TestLiveRelays`, `capsule.TestLiveLifecycle` (`-tags integration`) |
| 50 | Historical release dependency | documented in `README.md` | — |
| 51 | Quicknet release verification | `provider.Verify` | `provider.TestVerifyPublishedReleases`, `TestVerifyRejects`, `TestVerifyUsesThePinnedKeyOnly` |
| 52 | DNS / MITM | `provider/drand` (no redirects, bounded responses, BLS) | `drand.TestRedirectsAreNotFollowed`, `TestRejectMalformedRelayResponses`, `TestRandomnessMustMatchWhenPresent` |
| 53 | Harvest now, decrypt later | `cmd/datekeys` warning beyond one year | `cmd/datekeys.TestLongHorizonWarning` |
| 54 | Extensions: data absent or a non-empty opaque byte string, never decoded; 1 to 64 per array; `extension_version` ≤ 2^32−1; one `extension_id` per object | `extension.New`, `Canonical`, `EncodeArray` (refuses, through `codec.Encoder.Fail`, an array that `DecodeArray` rejects), `DecodeArray` (64 entries checked on the array head, explicit key 2 check), `CheckDisjoint` (linear merge), `CheckCritical`, `CheckNoncritical`, `Unusable` | `extension.TestNew`, `TestData`, `TestCanonicalSorts`, `TestCanonicalRejects`, `TestEncodeArrayRejects`, `TestDecodeArrayRejects`, `TestCheckDisjoint`, `TestCheckDisjointIsLinear`, `TestCheckCritical`, `TestCheckNoncritical`, `FuzzDecodeArray`; `capsule.TestKnownCriticalExtensions`, `TestUnusableNoncriticalExtensions`; mutations *unknown critical … extension*, *known critical … extension with invalid data*, *extension_version above 2^32-1*, *null extension data* |
| 55 | Auxiliary integrity | `capsule_digest` treated as UX only | — |
| 56 | Atomic plaintext output | `capsule.Open` contract; `cmd/datekeys.writeAtomic` | `cmd/datekeys.TestOutputNotPublishedOnFailureOrOverwrite`, `TestDecryptFailuresLeaveNothing` |
| 57 | Parser limits, MUST for encoders and decoders; frame lengths and objects above their frame → `ERR_INTEGRITY` on encode and decode, CDDL violations → `ERR_NON_CANONICAL_CBOR`, Provider Profile names and public key → `ERR_UNKNOWN_PROFILE`; implementation limits not normative | `capsule.MaxPublicHeaderLen` (`ParsePrelude`, `EncodeHeader`, `DecodeHeader`), `MaxSealedControlLen` (`ParsePrelude`, `Encrypt`), `accesskey.MaxBodyLen` (`Decode`, `DecodeBody`, `MarshalBody`), `extension.MaxExtensions`, `MaxDataLen`; the bounds each schema passes to `codec.Decoder` (`Map`, `Array`, `Uint`, `Bstr`, `Text`), with lengths checked against the remaining input before any copy; `profile.Validate` | mutations *…_LEN above the limit*, *65 extensions in one array*; `capsule.TestHeaderLimit`, `TestHugeExtensionArraysAreRejected`; `accesskey.TestDecodeRejects` *body length above the limit*, `TestBodyLimit`; `profile.TestValidateRejectsTamperedProfiles`, `TestIntegerRanges`; `codec.TestDecoderRejects` |
| 58 | Canonical CBOR and the protocol's CBOR profile (major types 0, 2, 3, 4, 5; unsigned integer keys; integers ≤ 2^53−1) | `codec`, without reflection or dependencies: `Encoder` (shortest heads, valid UTF-8, nil byte strings as empty, never `null`; a sticky first error, which `Fail` lets a schema encoder record), `Decoder` (strict cursor: profile major types only, shortest heads, definite lengths, strictly ascending unsigned keys per map, valid UTF-8, no trailing bytes), `Unmarshal` (re-encoding comparison), `Walk` (the profile only, for vectors, fuzzing and diagnostics); `codec.MaxSafeUint`; the profile covers the head of extension data only | `codec.TestDecoderAccepts`, `TestDecoderRejects` (negative integer, tag, float, simple values, indefinite lengths, non-shortest heads, text key, key order, UTF-8), `TestUnmarshalRejectsNonCanonical`, `TestWalk`, `TestEncoderAndWalkAgreeWithAReference` (against `internal/cbortest`), `TestSharedVectors`, `FuzzDecoder`, `FuzzUnmarshal`, `FuzzWalk`, `FuzzEncodeImpliesWalk`; `extension.TestData`; `internal/testkit.TestSchemaVectors`; `testdata/vectors/cbor.json` (generic vectors walked with `codec.Walk`, and one block per schema: Provider Profile, PUBLIC_HEADER, CONTROL_CBOR, `.dkk` body, `verification_metadata`, extension), generated by `internal/testkit.CBORVectors` |
| 58.1 | Absent optional fields are omitted; `h''` and `null` never stand for absence | `extension.Canonical` (nil for empty), `extension.DecodeArray` (empty array, empty data), re-encoding check, `accesskey` verification map | `codec` *empty optional array present*; `accesskey` *empty extension array*, *empty verification map*, *null verification*, *empty data*, *null data*; mutation *empty extension data (h'')* |
| 59 | Supply-chain security | pinned `go.mod`/`go.sum`, `.gitea/workflows`, `scripts/check.sh`, `.goreleaser.yaml`, `SECURITY.md` | CI jobs `vuln`, `sbom`, `verify` |
| 60 | Conceptual Go interfaces | `provider.ReleaseSource`, `provider.Verify`, `datekey.Resolve`, `datekey.RoundTime` | — |
| 61 | `time_only` encryption flow | `capsule.Encrypt` (steps numbered in comments) | `capsule.TestEncryptRoundTripBothPolicies` |
| 62 | `time_and_key` encryption flow | `capsule.Encrypt` | `capsule.TestEncryptRoundTripBothPolicies`, `TestPortableKeysAreNeverReused` |
| 63 | Decryption flow; steps 4 and 14 validate critical extensions (unknown, invalid data) | `capsule.Inspect` (steps 1–8), `capsule.Open` (steps 9–18), MUST rules inside `agewrap` identities; `datekeys inspect -json` rendered by `internal/inspectview` | `capsule.TestConformanceFixtures` (stage by stage), `TestMutationCorpus`, `TestInspectDifferentialCorpus` (`testdata/vectors/inspect_differential.json`: 1825 deterministic mutations of the fixtures with the verdict of steps 1–8, generated by `internal/testkit.InspectDifferential`); `cmd/datekeys.TestInspectJSONGoldens` (`testdata/fixtures/*.inspect.json`) |
| 64 | Mandatory mutation tests | `internal/testkit.Mutations` (the corpus), `internal/testkit.MutationCorpus` (its export) | `capsule.TestMutationCorpus`: the 23 listed mutations plus 32 more, built afresh; `capsule.TestExportedMutationCorpus`: `testdata/vectors/mutations.json`, the same 55 cases as frozen data (capsule, `.dkk`, identities, recorded release, clock, registry, known extensions), replayed with the recorded error and step |
| 65 | Quicknet vectors | `internal/testkit.RoundVectors` | `datekey.TestGoldenRoundVectors` |
| 66 | `dk1_` vectors | `internal/testkit.DK1Vectors` | `datekey.TestGoldenDK1Vectors` |
| 67 | `.dkc` vectors | `testdata/fixtures/*.dkc` + `*.json`, `internal/testkit/genfixtures`; the frozen `datekeys inspect -json` output of each, `*.inspect.json`; formats in `testdata/README.md` | `capsule.TestConformanceFixtures`; `cmd/datekeys.TestInspectJSONGoldens` |
| 68 | `.dkk` vectors, with the exact extension data; one carries an extension with data | `testdata/fixtures/*.dkk` + `*.dkk.json`; `time_and_key_portable_extension.dkk` derived by `genfixtures` | `accesskey.TestFixtures`, `TestFixtureWithExtension`; `capsule.TestAccessKeyFixtureWithExtension` |
| 69 | Normative errors, including `ERR_EXTENSION_DATA_INVALID` | `errors.go` | `datekeys.TestCatalogueMatchesSpec`, `TestCode` |
| 70 | Compatibility | magic and version checks; `codec.Peek` and `codec.CheckSchema` read keys 0 and 1 only, before strict decoding, with a type tag of at most `codec.MaxTypeTagLen` bytes | mutations; `codec.TestPeek`, `TestCheckSchema`, `TestCheckSchemaVersionForms`, `FuzzPeek`; `capsule.TestDecodeSchemaVersion` |
| 71 | Profile registry | `profile.Decode` + `profile.NewRegistry` with pinned hashes | `profile.TestRegistry` |
| 72 | Extension registry and registration rules; the encoder decodes its own output before sealing | `extension.Registry`, `extension.Set`, `extension.DataValidator`; self-checks in `capsule.Encrypt` and `accesskey.MarshalBody` | `capsule.TestKnownCriticalExtensions`, `TestUnusableNoncriticalExtensions`, `TestNestedDataSealsAndOpens`, `FuzzEncodeImpliesDecode` |
| 75 | Blocking requirements before v1.0 | items 1–9 above; item 10 (external review) pending | — |
| 76 | Change policy; the v0.8.2 extension change and its reproducible cases | `extension`, `codec`, fixture `time_only_extensions` regenerated | case 2: `extension.TestNew`; case 3: `capsule.TestNaNKeyedDataHasOneVerdict`; case 4: `capsule.TestExtensionFixtureData`; case 5: `capsule.TestNestedDataSealsAndOpens`; case 6: `capsule.TestHugeExtensionArraysAreRejected`, `extension.TestCheckDisjointIsLinear` |
## Error mapping
Where the specification does not name the error of a failure, the reference
implementation uses the following mapping. Each entry is a reproducible case
under the change policy of §76.
| Failure | Error |
|---|---|
| Bytes that are not the deterministic encoding of a valid schema instance: malformed CBOR, non-canonical encoding (including a map head or type tag head not in its shortest form before the schema version), unknown key, missing key, wrong type, `null`, wrong type tag (key 0, or one longer than `codec.MaxTypeTagLen` bytes), a schema version that is missing, not the second key, not an unsigned integer, not in its shortest form or above 2^53−1, wrong field length, undefined `access_policy` (any value other than 0 and 1), empty optional array or map, extension rules (named by §54 and §57 since v0.8.2) | `ERR_NON_CANONICAL_CBOR` |
| Schema version other than 1, read as the second key, after a type tag within the profile, as an unsigned integer in its shortest form of at most 2^53−1; whatever follows it | `ERR_UNSUPPORTED_VERSION` |
| Truncated framing, length fields of 0 or beyond the §57 limits, an object above its §57 frame on encode or decode, data after BODY_CBOR, malformed or unauthenticated age data (an age header without stanzas, or beyond the parser limits of `filippo.io/age`: 1024 stanzas, 128 arguments, 2 MiB), truncated or modified STREAM, trailing data after PAYLOAD_AGE, a PAYLOAD_AGE that I_PAYLOAD cannot open | `ERR_INTEGRITY` |
| Stanza count or type violations in OUTER_TIME_AGE, PAYLOAD_AGE or INNER_ACCESS_AGE, including two stanzas for one recipient, in a header that parses; a tlock stanza without exactly two arguments | `ERR_POLICY_STRUCTURE_MISMATCH` |
| tlock stanza round argument not exactly the canonical decimal DateKey round | `ERR_ROUND_MISMATCH` |
| tlock stanza chain hash not exactly the lowercase hex chain hash of the pinned profile; profile whose parameters do not hash to its chain hash | `ERR_PROFILE_MISMATCH` |
| Instant before the profile genesis or after 9999-12-31T23:59:59Z; round outside the profile range, including a round time after 9999-12-31T23:59:59Z (step 7) | `ERR_DATEKEY_INVALID` |
| Provider Profile that cannot be pinned, beyond the names and public key size that §57 maps: `genesis_time` outside 1..253402300798, `provider` other than `drand`, a scheme tlock does not support, a public key that is not a point of the scheme's key group or is the identity | `ERR_UNKNOWN_PROFILE` |
| Unknown `access_type`, wrong material length, `.dkk` for another `capsule_id`, `capsule_digest` mismatch, no supplied identity is a recipient | `ERR_ACCESS_INVALID` |
| Round time not reached yet (no request is made), no source delivered the release | `ERR_RELEASE_UNAVAILABLE` |
## Implementation decisions to confirm in the specification
These are choices the reference implementation had to make where v0.8.2 is
silent or provisional (§74). None changes the protocol semantics; each is a
candidate clarification under §76.
1. **Pre-genesis instants.** §15 defines the candidate formula relative to
`genesis_time`; instants before it are rejected with `ERR_DATEKEY_INVALID`
instead of resolving to round 1.
2. **Round bounds.** `dk1_` accepts rounds in 1..2^53−1 so that JSON parsers
based on IEEE 754 doubles read the same integer; a profile further limits
rounds to round times up to 9999-12-31T23:59:59Z (Quicknet: 83 903 165 811).
3. **`profile_id` alphabet.** `[a-z0-9][a-z0-9:._-]{0,127}`, which keeps the
canonical `dk1_` JSON free of escapes and makes its re-emission trivial.
4. **Number spellings in `dk1_`.** JSON numbers are compared by value, so
`1e3` or `1000.0` for 1000 are `ERR_DATEKEY_NON_CANONICAL`, while
non-integers, negatives and out-of-range values are `ERR_DATEKEY_INVALID`.
Padded or standard-alphabet Base64 and non-zero trailing bits are
non-canonical.
5. **Closed maps.** Unknown keys in core maps are rejected; applications use
extensions (§1, §54).
6. **Extension data.** v0.8.2 settles the format (§54, §76): key 2 is omitted
when absent and otherwise a non-empty byte string that the base protocol
never decodes. What remains an implementation choice: `extension_id` is 1 to
256 bytes of UTF-8; `extension.MaxDataLen` is 64 MiB, the largest frame, the
container frame being the effective bound; an application validates the
data of the extensions it knows through the optional
`extension.DataValidator` of its `Registry`, and an unknown critical
extension is reported before a known one with invalid data.
7. **One stanza per recipient.** Enforced as far as a recipient can observe it:
no repeated X25519 ephemeral share, and no identity that unwraps more than
one stanza.
8. **Strict tlock stanza arguments.** Exact string comparison, as the tlock
library itself does for the chain hash; a round with leading zeros is a
mismatch.
9. **`capsule_digest`.** Written by `Encrypt` for every portable key and
checked before any request when the capsule reader is seekable; it remains
a UX shortcut (§43).
10. **Creation in the past.** `Encrypt` requires the unlock time to be strictly
after the injected clock.
11. **Clock injection.** No library package reads the wall clock; `Encrypt` and
`Open` require a `Now` function, and `Open` never requests a release for a
round whose time has not been reached.
12. **Order of checks within an object.** The type tag and the schema version
are read first, from keys 0 and 1 only (§70). The decoder of each schema
then reads the whole map with every CDDL rule whose violation is
`ERR_NON_CANONICAL_CBOR`, and only then checks the fields that have codes
of their own (§57): the DateKey of PUBLIC_HEADER, `access_type` and
`access_material` of a `.dkk`, the names and public key of a Provider
Profile. An object with faults of both kinds reports
`ERR_NON_CANONICAL_CBOR`.
13. **Profile `period`.** At most 86400 s (one day), an implementation limit
marked in `spec/datekeys.cddl` (§57, §74); §11 allows up to 2^53−1.
`genesis_time` must be in 1..253402300798 for a profile to be pinned
(`ERR_UNKNOWN_PROFILE`): a positive time before the last second that
item 2 allows, so that the profile has at least one round.
14. **Frame lengths.** `PUBLIC_HEADER_LEN` and `SEALED_CONTROL_LEN` of 0 are
out of range (`ERR_INTEGRITY`, step 2): §57 gives only upper bounds, and
no empty frame holds a valid object.
15. **Malformed age headers.** Steps 5 and 6 parse the age header with
`filippo.io/age`; a header that does not parse, including one without
stanzas or beyond the parser limits (1024 stanzas, 128 arguments, 2 MiB),
is `ERR_INTEGRITY`, and only a header that parses is judged by the stanza
rules (`ERR_POLICY_STRUCTURE_MISMATCH`).
All of them, with the other rules of steps 1 to 8 and the access pre-checks,
are written out for a second implementation in `testdata/README.md`.

Powered by TurnKey Linux.