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

44 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
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.

Powered by TurnKey Linux.