# 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` | | 12.1 | Provider Profile validation: CDDL, field rules (`ERR_UNKNOWN_PROFILE`: the name alphabets, normative, and their lengths and the `public_key` size, implementation limits; `period` at most 2^32−1, implied by the 86400 s limit; `genesis_time` in 1..253402300798, provider `drand`, the three unchained tlock schemes, a public key that is the canonical encoding of a point of the key group (§12.2) other than the identity), then the chain-hash self-check (`ERR_PROFILE_MISMATCH`), then the pinned `profile_hash`; the same codes on the pin path and the decode path | `profile.Decode`, `Profile.Validate` (rules 1 to 3 for a value), `validateDrand` (drand `chain.Info.Hash`), `profile.NewRegistry` (encode, `Decode`, then the pinned hash) | `profile.TestDecodePrecedence` (with a G1 point outside the subgroup), `TestPinPathMatchesDecode`, `TestChainHashFormula` (the formula computed without drand), `TestPublicKeyEncodingIsCanonical`, `TestValidateRejectsTamperedProfiles`, `TestRegistry`; the `provider_profile` block of `testdata/vectors/cbor.json` | | 12.2 | Canonical encoding of a BLS12-381 point: compressed, 48 bytes in G1 and 96 in G2 (c1 then c0); compression flag set, infinity flag only for the point at infinity with every other bit zero, sort flag for the lexicographically largest y; coordinates below p; the prime-order subgroup; every other string rejected (x + p, c0 + p, c1 + p, an identity with a payload or the sort flag, no compression flag, uncompressed forms, other lengths) | the decoder of `kilic/bls12-381` through drand's `kyber-bls12381` (`KyberG1` and `KyberG2` `UnmarshalBinary`), which the reference runs for the public key (`profile.validateDrand`), the release signature (drand `Scheme.VerifyBeacon` in `provider.Verify`) and U (`tlock.BytesToCiphertext` in `agewrap.TimeIdentity.Unwrap`); the point at infinity refused explicitly for the public key and U, and for the signature by the BLS verification | `profile.TestDrandPointDecodersAreCanonical` (fails if a dependency update makes the decoders lenient), `TestPublicKeyEncodingIsCanonical`; `provider.TestVerifyRejects`; `agewrap.TestTimeIdentityStrictness`, `TestTimeIdentityRelease`; `internal/testkit.TestPointReencodings`; `capsule.TestPointMutationsChangeOnlyTheEncoding`; the ten point mutations of §64 | | 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 spec §12.1) | `profile.TestRegistry`, `TestPinPathMatchesDecode`; mutations *unknown profile*, *empty registry*; the `provider_profile` block of `testdata/vectors/cbor.json` | | 14 | DateKey | `datekey.DateKey` | `datekey/*` | | 15 | Date → round resolution; round time at most 9999-12-31T23:59:59Z, instants before `genesis_time` rejected (`ERR_DATEKEY_INVALID`) | `datekey.Resolve`, `datekey.RoundTime`, `DateKey.Validate`, `Profile.MaxRound`, `profile.MaxUnixTime`; `capsule.Inspect` step 7 | `datekey.TestGoldenRoundVectors` (*genesis - 1s*, *after the last representable round*), `TestRoundNeverOpensEarly`, `TestResolveProperty`, `TestTimezoneIndependence`, `TestValidate`; `capsule.TestPrecedenceAcrossSteps` (step 7) | | 16 | Normative round vector | — | `datekey.TestNormativeRoundVector`; `testdata/vectors/quicknet_rounds.json` | | 17 | Past-round attack; at step 10 the release round is compared before the signature, `ERR_ROUND_MISMATCH` for a release supplied directly, a network source discarding one of another round at step 9 (`ERR_RELEASE_UNAVAILABLE`) | `provider.Verify` (round equality first), `capsule.Encrypt` (round time ≥ requested), `agewrap.CheckTimeStanzas`, `provider/drand.Client` | `provider.TestVerifyRejects` (*another round and a short signature*); `capsule.TestReleaseFromANetworkSource`; mutations *DateKey A + release of round B*, *tlock stanza round differs from DateKey.round* | | 18 | `dk1_` representation; the canonical JSON has no escapes, the `profile_id` alphabet needs none | `DateKey.CanonicalJSON`, `DateKey.Compact` | `datekey.TestGoldenDK1Vectors`, `TestNormativeRoundVector` | | 19 | `dk1_` canonicality; steps 1 to 3 `ERR_DATEKEY_INVALID`, step 6 `ERR_DATEKEY_NON_CANONICAL`; step 1 accepts either Base64 alphabet, padding and non-zero trailing bits but no other character (CR and LF included); step 2 one RFC 8259 JSON object, no byte order mark; JSON numbers by their exact decimal value; round in 1..2^53−1 without the profile | `datekey.Parse` (`decodeBase64`, which rejects CR and LF before the Go decoders, `parseJSON`, which rejects invalid UTF-8 before `encoding/json` can replace it, `jsonUint`) | `datekey.TestGoldenDK1Vectors`, `TestReadingRules`, `TestNumberSpellings`, `FuzzParse`; mutation *non-canonical dk1_ JSON*; `testdata/vectors/dk1.json` (*byte order mark*, *line feed inside the Base64*, *carriage return and line feed after the Base64*, *version 1.0000000000000001: its exact value, not a double*, *invalid UTF-8 in a member a repeated name overwrites*) | | 20 | File extensions and magic | magic checks in `capsule.ParsePrelude`, `accesskey.Decode` | mutation *a .dkk offered as a .dkc*; `accesskey.TestDecodeRejects` *a .dkc* | | 21 | `capsule_id`: exactly 16 bytes from a CSPRNG (MUST) | `capsule.Encrypt` (16 bytes from `crypto/rand`), `capsule.DecodeHeader` | `capsule.TestPortableKeysAreNeverReused` | | 22 | `.dkc` framing; `PUBLIC_HEADER_LEN` in 1..1 MiB, `SEALED_CONTROL_LEN` in 1..64 MiB; PAYLOAD_AGE to EOF, at least an age header | `capsule.Prelude`, `capsule.ParsePrelude` | mutations *version changed*, *flags != 0*, *reserved != 0*, *magic*, length limits; `capsule.TestFrameLengthLowerBounds`, `FuzzParsePrelude` | | 23 | PRELUDE; order of the checks of steps 1 and 2; section bytes present at steps 3 and 5 | `Prelude.Bytes`, `capsule.ParsePrelude`, `capsule.Inspect` | `capsule.TestConformanceFixtures`, `TestFrameLengthLowerBounds`, `TestPrecedenceAcrossSteps`; `testdata/vectors/inspect_differential.json` | | 24 | PUBLIC_HEADER; keys 5 and 6 optional, 1 to 64 extensions each | `capsule.Header`, `EncodeHeader`, `DecodeHeader` (hand-written `headerWire` encode and decode; CDDL checked before the DateKey) | `capsule.TestConformanceFixtures` (exact extension data), `TestDecodeHeaderRejects`, `TestDecodeMapStructure`, `TestDecodeHeaderReportsTheCDDLFirst`, `FuzzDecodeHeader`, `FuzzEncodeImpliesDecode`; mutations *header schema version changed*, *unknown key in PUBLIC_HEADER* | | 25 | Declared access policy | `capsule.Policy`; `capsule.DecodeHeader` (the value read, up to 2^53−1, must be 0 or 1 before any narrowing); `capsule.Open` step 12 | `capsule.TestDecodeMapStructure` (2, 255, 256, 257, 2^32, 2^53−256 and others), `FuzzDecodeHeader` (seeds 256, 257, 2^32); mutations *access_policy=… with … structure* (four cases), *undefined access_policy*, *access_policy 256 / 257 with a consistent header_binding* | | 26 | Header binding; a mismatch at step 15 is `ERR_HEADER_BINDING` | `capsule.HeaderBinding`; `capsule.Open` step 15 | `capsule.TestConformanceFixtures`; mutation *PUBLIC_HEADER_A + SEALED_CONTROL_B* | | 27 | Pre-unlock validation; the age header MAC authenticates only against whoever does not know the file key (anyone recomputes that of OUTER_TIME_AGE once the round is published), and `header_binding` gives internal coherence, not authorship or a date (§55.1) | `capsule.Inspect` (steps 1–8), `agewrap.Stanzas` probe | `capsule.TestMutationCorpus` (no release request for any pre-unlock failure), `FuzzInspect`, `TestTrustModel`; the U and stanza body mutations of §64, whose header MAC is recomputed | | 28 | Three age files | `capsule.Encrypt`, `capsule.Open` | `capsule.TestEncryptRoundTripBothPolicies` | | 28.1 | Age file format: the C2SP header grammar (at least one stanza); malformed OUTER_TIME_AGE and PAYLOAD_AGE headers `ERR_INTEGRITY` (steps 5 and 6, or 11 and 17), a malformed INNER_ACCESS_AGE `ERR_POLICY_STRUCTURE_MISMATCH` (step 12); wrong stanza count or type `ERR_POLICY_STRUCTURE_MISMATCH` | `agewrap.Stanzas` (the header parser of `filippo.io/age`), `capsule.classify`, `capsule.Open` step 12 | `capsule.TestMalformedAgeHeaders`, `agewrap.TestStanzasProbe`, `FuzzStanzas`; the 10 header-without-stanzas cases of `testdata/vectors/inspect_differential.json` (5 at step 5, 5 at step 6) | | 29 | PAYLOAD_AGE | `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 (`extension_id` valid UTF-8 of at least 1 byte), elements in strictly ascending bytewise order of `extension_id` (§54) | `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; exactly two stanza arguments compared as strings (§63 step 8) | `agewrap.TimeRecipient`, `agewrap.TimeIdentity`, `agewrap.CheckTimeStanzas` (pinned parameters only, exact stanza arguments) | `agewrap.TestTimeIdentityStrictness`, `TestInteroperabilityWithTlockLibrary`, `TestTimeIdentityRelease`; `capsule.TestTlockStanzaArgumentComparison` | | 36 | Policy ↔ structure; `time_only`: a plaintext that starts with the age intro line is a mismatch, any other is read as CONTROL_CBOR at step 14; `time_and_key`: a malformed age header, or two stanzas with one argument after the type and the same argument (a repeated X25519 ephemeral share), is a mismatch | `capsule.Open` step 12 (`looksLikeAge`, `agewrap.Stanzas`), `agewrap.CheckAccessStanzas` | mutations *access_policy=…* (four cases), *non-X25519 stanza in INNER_ACCESS_AGE*; `capsule.TestMalformedAgeHeaders`; `agewrap.TestMalformedX25519Stanzas` (*repeated stanza*) | | 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; `BODY_LEN` in 1..16 MiB (0 is `ERR_INTEGRITY`); order of the frame checks | `accesskey.Encode`, `accesskey.Decode` (the body buffer grows with the data read; every buffer holding the body is wiped) | `accesskey.TestDecodeRejects`, `TestDecodePrecedence`, `TestDecodeShortBodyAllocatesLittle`, `TestEncodeAndDecodeLeaveNoStaleMaterial`, `FuzzDecode` | | 41 | `.dkk` BODY_CBOR | `AccessKey.MarshalBody`, `accesskey.DecodeBody` (hand-written `bodyWire` encode and decode) | `accesskey.TestFixtures`, `TestDecodeBodyStructure` | | 42 | `credential_id` | `capsule.Encrypt` (16 bytes from `crypto/rand`) | `capsule.TestPortableKeysAreNeverReused` | | 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; a network source verifies every response with the rules of §63 step 10 and discards the invalid ones: none valid is `ERR_RELEASE_UNAVAILABLE` at step 9, and so is any other failure of a source, with no other code | `provider/drand.Client` (race, first *verified* release wins; the failure of each relay kept as text only, a context that ended detectable with `errors.Is`), the `provider.ReleaseSource` contract, `capsule.Open` (step 9 keeps only the text of a source error with another code or none) | `drand.TestRaceWaitsForAValidSignature`, `TestRejectMalformedRelayResponses`, `TestFetchErrorHasOneCode`, `TestUnavailabilityAndCancellation`; `capsule.TestReleaseFromANetworkSource`, `TestReleaseSourceErrorsAtStep9` | | 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; order and codes of §63 step 10: the round (`ERR_ROUND_MISMATCH`), then the signature, the canonical encoding of a point of G1 other than the identity (§12.2) that verifies as the BLS signature of the round (`ERR_RELEASE_INVALID`); those codes for a release supplied directly, a network source discarding an invalid one at step 9 (`ERR_RELEASE_UNAVAILABLE`) | `provider.Verify` | `provider.TestVerifyPublishedReleases`, `TestVerifyRejects` (x + p, the point at infinity alone, with a payload or with the sort flag, no compression flag, the negated signature), `TestVerifyUsesThePinnedKeyOnly`; `capsule.TestReleaseFromANetworkSource`; mutations *DateKey A + release of round B*, *release of another round*, *release signature …* | | 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_id` of at least 1 byte; `extension_version` ≤ 2^32−1; elements in strictly ascending unsigned bytewise order of the UTF-8 bytes of `extension_id` (a proper prefix first, never UTF-16 code units or a collation), so one `extension_id` per array, and none in both arrays; a known extension in an object or array it is not registered for is unknown there, its data never interpreted | `extension.New`, `Canonical`, `EncodeArray` (refuses, through `codec.Encoder.Fail`, an array that `DecodeArray` rejects), `DecodeArray` (64 entries checked on the array head, explicit key 2 check), `CheckDisjoint` (linear merge), `CheckCritical`, `CheckNoncritical`, `Unusable`; `Object`, `Array`, the optional `Placement` of a `Registry`, `KnownIn`, and `CheckCriticalIn` and `CheckNoncriticalIn`, which `capsule` runs at steps 4, 9.a and 14 | `extension.TestNew`, `TestData`, `TestCanonicalSorts`, `TestOrderIsUnsignedBytewise`, `TestCanonicalRejects`, `TestEncodeArrayRejects`, `TestDecodeArrayRejects`, `TestCheckDisjoint`, `TestCheckDisjointIsLinear`, `TestCheckCritical`, `TestCheckNoncritical`, `TestPlacement`, `FuzzDecodeArray`; `capsule.TestKnownCriticalExtensions`, `TestUnusableNoncriticalExtensions`, `TestExtensionPlacement`; mutations *unknown critical … extension*, *known critical … extension with invalid data*, *extension_version above 2^32-1*, *null extension data* | | 55 | Auxiliary integrity | `capsule_digest` treated as UX only | — | | 55.1 | Trust model: who writes each section, from which step and by what it is bound, what it never proves | no code of its own: `header_binding` (step 15), the age header MACs (steps 11, 13 and 17), `capsule_id` and `capsule_digest` (step 9.a) | `capsule.TestTrustModel` (a capsule forged from the public bytes of `time_only.dkc` opens; edited PUBLIC_HEADER data passes steps 1 to 8 and fails step 15; other `.dkk` extension data opens the capsule) | | 56 | Atomic plaintext output | `capsule.Open` contract; `cmd/datekeys.writeAtomic` | `cmd/datekeys.TestOutputNotPublishedOnFailureOrOverwrite`, `TestDecryptFailuresLeaveNothing` | | 57 | Parser limits, MUST for encoders and decoders; frame lengths of 0 or above the limits and objects above their frame → `ERR_INTEGRITY` on encode and decode, a field of the wrong CBOR type → `ERR_NON_CANONICAL_CBOR` before its own code, CDDL violations → `ERR_NON_CANONICAL_CBOR`, Provider Profile names and public key → `ERR_UNKNOWN_PROFILE`; implementation limits not normative | `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, then invalid data); step 5 reads SEALED_CONTROL, a MUST (`ERR_INTEGRITY`), and SHOULD inspect its age header; step 8 argument rules; step 9 order: the `.dkk` as an object (decoded there when still encoded), its `capsule_id` and `capsule_digest`, credentials (nil identities are none) before the clock, round time, request, and nothing of the credentials under `time_only`; a network source verifies each response with the rules of step 10 and discards the invalid ones (none valid: `ERR_RELEASE_UNAVAILABLE`, step 9), and any failure of a source is `ERR_RELEASE_UNAVAILABLE` alone, whatever code its error carries; step 10: round, then signature, a canonical point other than the identity (§12.2), the codes of a release supplied directly; step 11: the tlock stanza body `U \|\| V \|\| W` of \|U\| + 32 bytes (128 in Quicknet), U canonical and not the identity, the IBE check r·G == U, every failure `ERR_INTEGRITY`, H2, H3 and H4 those of drand/kyber `encrypt/ibe`, H2 over the element of GT serialized in the order of kilic/bls12-381 (c1 before c0 at every level of the tower), with the frozen vector H2(e(G1, G2)) = `cb87319f24560b5231579a09ad79f12e`; the codes of the identities at steps 11, 13 (malformed X25519 stanza `ERR_INTEGRITY`, an identity that unwraps two stanzas `ERR_POLICY_STRUCTURE_MISMATCH` whatever the order, none `ERR_ACCESS_INVALID`) and 17; step 15 `ERR_HEADER_BINDING` | `capsule.Inspect` (steps 1–8), `capsule.Open` (steps 9–18; `OpenOptions.AccessKeyFile`, `checkAccessKey`, `checkCapsuleDigest`), the `provider.ReleaseSource` contract, `provider/drand.Client` and `capsule.sourceFailure` (step 9), `tlock.TimeUnlock` with the kyber-bls12381 pairing (step 11), MUST rules inside `agewrap` identities (`AccessIdentity` tries every identity on every stanza; `TimeIdentity` checks the length of the tlock stanza body and U before `tlock.TimeUnlock`); no error copies the text of an error of age, tlock, kyber or drand (`agewrap`, `capsule.classify`), since kyber's IBE error carries the candidate plaintext and r; `cmd/datekeys` hands the `.dkk` over encoded; `datekeys inspect -json` rendered by `internal/inspectview` | `capsule.TestConformanceFixtures` (stage by stage), `TestTlockFailureDiagnosticsCarryNoSecrets`, `TestPlaintextWriterFailureKeepsItsText`, `TestAccessKeyCheckOrder`, `TestAccessKeyFileAtStep9`, `TestPrecedenceAcrossSteps`, `TestControlCriticalBeforeHeaderBinding`, `TestReleaseFromANetworkSource`, `TestReleaseSourceErrorsAtStep9`, `agewrap.TestAccessIdentityStrictness`, `TestMalformedX25519Stanzas`, `TestTlockH2Vector` (`testdata/vectors/tlock_ibe.json`, generated by `internal/testkit.IBEVectors`, and step 11 recomputed with H2 and H4 against the file key tlock unwraps), `cmd/datekeys.TestDecryptAccessKeyOrder`, `TestMutationCorpus`, `TestInspectDifferentialCorpus` (`testdata/vectors/inspect_differential.json`: 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 33 listed mutations plus 32 more, built afresh; `capsule.TestExportedMutationCorpus`: `testdata/vectors/mutations.json`, the same 65 cases as frozen data (capsule, `.dkk`, identities, recorded release, clock, registry, known extensions), replayed with the recorded error and step; `capsule.TestPointMutationsChangeOnlyTheEncoding`: the ten point mutations keep a valid header MAC, and a decoder that reduces coordinates modulo p opens the c0 + p and x + p cases | | 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`; every error of the module wraps exactly one | `errors.go`; `provider/drand.Client` and step 9 of `capsule.Open` keep another code of a failure as text only | `datekeys.TestCatalogueMatchesSpec`, `TestCode`; `drand.TestFetchErrorHasOneCode`; `capsule.TestReleaseSourceErrorsAtStep9` | | 69.1 | Error precedence: the first failing layer of each object (frame, a truncated prelude before the version; type tag and schema version; CBOR profile and CDDL, except the rules with codes of their own; fields with codes of their own in ascending key order, an extension unknown in its object or array before invalid data), the step order of §63 across objects and steps; only the optional inspection of steps 5, 6 and 8 and the `capsule_digest` check can change the code; the codes of step 10 are those of a release supplied directly, one from a network source being discarded at step 9 | `capsule.ParsePrelude`, `capsule.DecodeHeader`, `capsule.DecodeControl`, `accesskey.Decode`, `accesskey.DecodeBody`, `profile.Decode`, `codec.CheckSchema`, `codec.Unmarshal`, `extension.CheckCriticalIn`, `capsule.checkAccessKey`, `OpenOptions.AccessKeyFile`, `agewrap.AccessIdentity`, `provider/drand.Client` | `capsule.TestPrecedenceWithinPublicHeader`, `TestPrecedenceAcrossSteps`, `TestDecodeHeaderReportsTheCDDLFirst`, `TestAccessKeyCheckOrder`, `TestAccessKeyFileAtStep9`, `TestControlCriticalBeforeHeaderBinding`, `TestReleaseFromANetworkSource`, `TestExtensionPlacement`; `accesskey.TestDecodePrecedence`; `agewrap.TestAccessIdentityStrictness`; `profile.TestDecodePrecedence`, `TestPinPathMatchesDecode`; `cmd/datekeys.TestDecryptAccessKeyOrder`; `extension.TestCheckCritical`, `TestPlacement`; `codec.TestCheckSchemaVersionForms`; `testdata/vectors/cbor.json`, `inspect_differential.json` | | 70 | Compatibility | 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, among them the objects and arrays where each extension may appear, and an encoder never writes one elsewhere; the encoder decodes its own output before sealing; security-relevant claims in CONTROL_CBOR or under a signature extension, `.dkk` extension data advisory | `extension.Registry`, `extension.Set`, `extension.DataValidator`, `extension.Placement` (optional: a `Registry` without it knows its extensions in every object and array); self-checks in `capsule.Encrypt` and `accesskey.MarshalBody`, which take no `Registry`: the application writes each extension only where it is registered | `capsule.TestKnownCriticalExtensions`, `TestUnusableNoncriticalExtensions`, `TestExtensionPlacement`, `TestNestedDataSealsAndOpens`, `FuzzEncodeImpliesDecode`; `extension.TestPlacement` | | 74 | Provisional aspects; the implementation limits of the reference (name lengths, `public_key`, `period`, maximum `extension_id` length, `dk1_` length, age parser limits, `ERR_POLICY_STRUCTURE_MISMATCH` for INNER_ACCESS_AGE) | `profile.ValidID`, `validName`, `maxPublicKeyLen`, `maxPeriod`; `extension.MaxIDLen`; `datekey.MaxEncodedLen`; `filippo.io/age` | `profile.TestValidateRejectsTamperedProfiles`, `TestIntegerRanges`; `extension.TestNew`; the vectors of `cbor.json` named after the implementation limit | | 75 | Blocking requirements before v1.0 | items 1–9 above; item 10 (external review) pending | — | | 76 | Change policy; the normative changes of v0.8.2: the extension change and its reproducible cases; the refinements and theirs; the amendment on point canonicality and its case (a second implementation on `tlock-js` and `@noble/curves` 1.9.7 accepted U with c0 + p and a signature with x + p); the corrections of the formal review (an invalid release from a network source, the objects and arrays of each extension, the serialization of GT in H2) and their cases, and those of its second round (the encoder rule of §72, the codes of step 10 in §17 and §51 for a release supplied directly, one code for any failure of a source at step 9) | `extension`, `codec`, fixture `time_only_extensions` regenerated; refinements: the order of `capsule.checkAccessKey`, `BODY_LEN` 0 in `accesskey.Decode`, CR and LF and invalid UTF-8 in `datekey.Parse`, the `.dkk` decoded at step 9.a (`OpenOptions.AccessKeyFile`, the CLI), nil identities in `capsule.Open`, every identity tried in `agewrap.AccessIdentity`, `profile.NewRegistry` through `Decode`, `Profile.Validate` rule 1 first; four new `dk1.json` vectors; corrections: `extension.Placement` and the object-aware checks, the `provider.ReleaseSource` contract, `testdata/vectors/tlock_ibe.json`; second round: the error of `provider/drand.Client` and of step 9 in `capsule.Open` | case 2: `extension.TestNew`; case 3: `capsule.TestNaNKeyedDataHasOneVerdict`; case 4: `capsule.TestExtensionFixtureData`; case 5: `capsule.TestNestedDataSealsAndOpens`; case 6: `capsule.TestHugeExtensionArraysAreRejected`, `extension.TestCheckDisjointIsLinear`; refinements: the tests of rows 12.1, 15, 17, 19, 22, 28.1, 35, 36, 40, 51, 55.1, 63 and 69.1, and `extension.TestOrderIsUnsignedBytewise`; amendment: the tests of rows 12.2 and 64; corrections: `capsule.TestReleaseFromANetworkSource`, `TestExtensionPlacement`, `extension.TestPlacement`, `agewrap.TestTlockH2Vector`; second round: `capsule.TestReleaseSourceErrorsAtStep9`, `TestExtensionPlacement` (the noncritical array of a `.dkk`), `drand.TestFetchErrorHasOneCode`, `TestUnavailabilityAndCancellation`, `datekeys.TestCode` | ## Error mapping The error of each failure, with the section of the specification that names it. Until the v0.8.2 refinements (§76) several of these were choices of the reference; they are now normative. When bytes break several rules, §69.1 decides which code is reported. | Failure | Error | Spec | |---|---|---| | Bytes that are not the deterministic encoding of a valid schema instance: malformed CBOR, non-canonical encoding (including a map head or type tag head not in its shortest form before the schema version), unknown key, missing key, wrong type (also for a field with a code of its own), `null`, wrong type tag (key 0, or one longer than `codec.MaxTypeTagLen` bytes), a schema version that is missing, not the second key, not an unsigned integer, not in its shortest form or above 2^53−1, wrong field length, undefined `access_policy` (any value other than 0 and 1), empty optional array or map, extension rules including the order of `extension_id` | `ERR_NON_CANONICAL_CBOR` | §54, §57, §58, §69.1 | | Schema version other than 1, read as the second key, after a type tag within the profile, as an unsigned integer in its shortest form of at most 2^53−1; whatever follows it | `ERR_UNSUPPORTED_VERSION` | §69.1, §70 | | Truncated framing, length fields of 0 or beyond the §57 limits, an object above its §57 frame on encode or decode, data after BODY_CBOR, a malformed OUTER_TIME_AGE or PAYLOAD_AGE (an age header against the C2SP grammar, without stanzas, or beyond the parser limits of `filippo.io/age`: 1024 stanzas, 128 arguments, 2 MiB), a tlock stanza body of a length other than \|U\| + 32, with a U that is not the canonical encoding of a point of the key group (§12.2) or is the identity, or that fails the IBE check r·G == U (step 11), a malformed X25519 stanza (steps 13 and 17), a failed header MAC, a truncated or modified STREAM, trailing data after PAYLOAD_AGE, a PAYLOAD_AGE that I_PAYLOAD cannot open (step 17) | `ERR_INTEGRITY` | §22, §23, §28.1, §40, §57, §63 steps 11, 13 and 17, §74 | | Stanza count or type violations in OUTER_TIME_AGE, PAYLOAD_AGE or INNER_ACCESS_AGE in a header that parses; a repeated X25519 ephemeral share in INNER_ACCESS_AGE (step 12); an offered identity that unwraps more than one INNER_ACCESS_AGE stanza, whatever the order of the identities (step 13); a malformed INNER_ACCESS_AGE, including one beyond the parser limits, or none, under `time_and_key`; an age intro line under `time_only`; a tlock stanza without exactly two arguments | `ERR_POLICY_STRUCTURE_MISMATCH` | §28.1, §36, §63 steps 8, 12 and 13 | | tlock stanza round argument not exactly the canonical decimal DateKey round (steps 8 and 11); a release for another round supplied directly, checked before its signature (step 10) | `ERR_ROUND_MISMATCH` | §17, §63 steps 8, 10 and 11 | | A release supplied directly whose signature is not the canonical encoding of a point of the signature group of the scheme (§12.2), is the identity, or does not verify under the pinned key for the DateKey round | `ERR_RELEASE_INVALID` | §12.2, §51, §63 step 10 | | 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; a pinned profile with another `profile_hash` | `ERR_PROFILE_MISMATCH` | §12.1, §63 step 8 | | Instant before the profile genesis or whose round time would be after 9999-12-31T23:59:59Z; `dk1_` round outside 1..2^53−1; round time after 9999-12-31T23:59:59Z under the pinned profile (step 7) | `ERR_DATEKEY_INVALID` | §15, §19 | | Provider Profile that cannot be pinned: name alphabets (§12.1) and lengths (§74), public key size (§74), `period` above 2^32−1 (preceded in the reference by its 86400 s limit, `ERR_NON_CANONICAL_CBOR`), `genesis_time` outside 1..253402300798, `provider` other than `drand`, a scheme tlock does not support, a public key that is not the canonical encoding of a point of the scheme's key group (§12.2) or is the identity | `ERR_UNKNOWN_PROFILE` | §12.1, §12.2 | | Unknown `access_type`, wrong material length, `.dkk` for another `capsule_id`, `capsule_digest` mismatch (step 9.a); no offered identity is a recipient of INNER_ACCESS_AGE (step 13) | `ERR_ACCESS_INVALID` | §57, §63 steps 9 and 13 | | `time_and_key` and no credential offered (nil identities are none), before the clock is consulted | `ERR_ACCESS_REQUIRED` | §63 step 9 | | Round time not reached yet (no request is made), or no source delivered a release, whatever the cause: a network source discarded every response for breaking the rules of step 10, or the error of a source carries another code or none, of which only the text is kept | `ERR_RELEASE_UNAVAILABLE` | §63 step 9 | | An unknown critical extension, or a known one in an object or array it is not registered for, before any invalid data (steps 4, 9.a and 14) | `ERR_EXTENSION_CRITICAL_UNKNOWN` | §54, §63, §69.1, §72 | ## Implementation decisions Choices the reference implementation made where v0.8.2 was silent or provisional (§74). None changes the protocol semantics. ### Settled in the specification by the refinements, the amendment and the corrections of v0.8.2 1. **Pre-genesis instants** are `ERR_DATEKEY_INVALID` instead of resolving to round 1: §15. 2. **Round bounds.** `dk1_` accepts rounds in 1..2^53−1; a profile further limits rounds to round times up to 9999-12-31T23:59:59Z (Quicknet: 83 903 165 811), checked at step 7: §15, §19. 3. **Name alphabets.** `[a-z0-9][a-z0-9:._-]*` for `profile_id`, also as the `network` of `dk1_`, and `[a-z0-9][a-z0-9._-]*` for the other names: normative, §12.1, §19. Their maximum lengths, 128 and 64 bytes, are implementation limits listed in §74. 4. **Reading `dk1_`.** JSON numbers are compared by their exact decimal value, and the Base64 decoding accepts padding, the standard alphabet and non-zero trailing bits, which the final comparison rejects as non-canonical; CR and LF, which the Go decoders skip, a byte order mark and invalid UTF-8, which `encoding/json` replaces with U+FFFD, are invalid: §19. Until the refinements CR and LF were `ERR_DATEKEY_NON_CANONICAL`, and so was invalid UTF-8 in a member that a repeated name overwrites until 692cf87. 5. **Strict tlock stanza arguments.** Exact string comparison; a round with leading zeros or a sign, or an uppercase chain hash, is a mismatch: §63 step 8. 6. **Order of checks.** The first failing layer of each object, and the step order of §63 across objects: §69.1. The decoder of each schema reads the type tag and version first (keys 0 and 1), then the whole map with every CDDL rule, and only then the fields with codes of their own. At step 9 an offered `.dkk` is checked as an object (`access_type`, `access_material`, critical extensions) before its `capsule_id` and `capsule_digest`; until the refinements the reference checked the `capsule_id` first. 7. **Profile `period` and `genesis_time`.** `period` at most 86400 s, an implementation limit (§74), below the normative 2^32−1 of §12.1; `genesis_time` in 1..253402300798 to be pinned (§12.1). `NewRegistry` encodes and decodes each profile, so a profile gets the same code pinned or decoded; until the refinements it applied the field rules first. 8. **Frame lengths.** `PUBLIC_HEADER_LEN`, `SEALED_CONTROL_LEN` and `BODY_LEN` of 0 are out of range (`ERR_INTEGRITY`): §22, §40, §57. Until the refinements a `.dkk` with `BODY_LEN` 0 was `ERR_NON_CANONICAL_CBOR`. 9. **Malformed age headers.** A header against the C2SP grammar, including one without stanzas or beyond the parser limits, is `ERR_INTEGRITY` for OUTER_TIME_AGE and PAYLOAD_AGE and `ERR_POLICY_STRUCTURE_MISMATCH` for INNER_ACCESS_AGE; only a header that parses is judged by the stanza rules: §28.1, §36. 10. **Release verification order.** The release round before its signature: §63 step 10. 11. **One stanza per recipient.** No repeated X25519 ephemeral share (step 12), and no offered identity that unwraps more than one stanza, whatever the order of the identities (step 13): §36, §63. Until the refinements the first identity that unwrapped exactly one stanza opened the file. 12. **Decoding a `.dkk`.** Its errors come at step 9.a, and never under `time_only`, even when it is decoded earlier: §63. The CLI hands the file to `capsule.Open` encoded (`OpenOptions.AccessKeyFile`); until the refinements it decoded the `.dkk` before step 1. A caller of the API that decodes a `.dkk` itself, with `accesskey.Decode`, gets its decoding errors first. 13. **Point encodings.** Settled by the v0.8.2 amendment on point canonicality: the public key, the release signature and U accept only the canonical compressed encoding of drand, never the point at infinity, and the tlock stanza body is `U || V || W`: §12.1, §12.2, §63 steps 10 and 11. The reference already rejected every other encoding through the decoder of `kilic/bls12-381`; a U at infinity, which only the IBE check used to reject, is now refused before decryption, with the same code. 14. **Invalid releases from the network.** Settled by the corrections of the formal review: `provider/drand.Client` verifies every relay response and discards the invalid ones, so a relay that returns no valid release gives `ERR_RELEASE_UNAVAILABLE` at step 9, and only a release supplied directly gets the codes of step 10: §63 steps 9 and 10, §69.1. Since the second round of the review, the error of `Fetch` keeps the failure of each relay as text only, and step 9 reports any failure of a source with `ERR_RELEASE_UNAVAILABLE` alone, keeping only the text of an error with another code or none. Until then, a relay whose only answer broke a rule of step 10 left that rule's code in the error too (`errors.Is`), and a caller's source that failed with `ERR_RELEASE_INVALID` got that code at step 9. 15. **GT in H2.** Settled by the corrections of the formal review: H2 hashes the element of GT as `kilic/bls12-381` serializes it, through tlock and drand/kyber, with the frozen vector of `testdata/vectors/tlock_ibe.json`: §63 step 11. ### Still implementation decisions 1. **Closed maps.** Unknown keys in core maps are rejected; applications use extensions (§1, §54, §58). 2. **Extension data.** `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 declares the objects and arrays each one is registered for (§72) through the optional `extension.Placement`; a `Registry` without it knows its extensions in every object and array, as before the formal review. The writers, `capsule.Encrypt` and `accesskey.Encode`, take no `Registry`: they write the extensions they are given, and the application writes each one only where it is registered, as §72 requires of an encoder. 3. **`capsule_digest`.** Written by `Encrypt` for every portable key and checked at step 9 when the capsule reader is seekable; it remains a UX shortcut and an optional check (§43, §69.1). 4. **Creation in the past.** `Encrypt` requires the unlock time to be strictly after the injected clock. 5. **Clock injection.** No library package reads the wall clock; `Encrypt` and `Open` require a `Now` function, and `Open` never requests a release for a round whose time has not been reached (§63 step 9). `testdata/README.md` documents the formats of the vectors and corpora and points to these sections for the rules that decide each verdict.