18 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 (unsigned genesis_time, codec.MaxSafeUint) |
profile.TestQuicknetMatchesGoldenVector, TestQuicknetCBORLayout, TestDecodeRoundTrip, TestIntegerRanges, FuzzDecode; testdata/vectors/profile_quicknet.json |
| 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 |
profile.TestRegistry; mutations unknown profile, empty registry |
| 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 |
capsule.TestConformanceFixtures (exact extension data), TestDecodeHeaderRejects, FuzzDecodeHeader, FuzzEncodeImpliesDecode; mutations header schema version changed, unknown key in PUBLIC_HEADER |
| 25 | Declared access policy | capsule.Policy; capsule.Open step 12 |
mutations access_policy=… with … structure (four cases), undefined access_policy |
| 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; extension |
capsule.TestConformanceFixtures (exact extension data), TestDecodeControlRejects, 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 |
accesskey.TestDecodeRejects, FuzzDecode |
| 41 | .dkk BODY_CBOR |
AccessKey.MarshalBody, accesskey.DecodeBody |
accesskey.TestFixtures |
| 42 | credential_id |
capsule.Encrypt (16 bytes from crypto/rand) |
capsule.TestPortableKeysAreNeverReused |
| 43 | verification_metadata |
accesskey.Verification; capsule.Open (checkCapsuleDigest, seekable readers) |
accesskey.TestDecodeRejects empty verification map; 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, Wire.UnmarshalCBOR (explicit key 2 check), Encode, Decode, CheckDisjoint (linear merge), CheckCritical, CheckNoncritical, Unusable |
extension.TestNew, TestWireData, TestEncodeRejects, TestDecodeRejects, TestCheckDisjoint, TestCheckDisjointIsLinear, TestCheckCritical, TestCheckNoncritical; 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, codec limits; 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 |
| 58 | Canonical CBOR and the protocol's CBOR profile (major types 0, 2, 3, 4, 5; unsigned integer keys; integers ≤ 2^53−1) | codec.Marshal (nil containers as empty, never null), codec.Unmarshal (re-encoding comparison) into typed schemas; codec.MaxSafeUint; the profile covers the head of extension data only |
codec.TestUnmarshalRejectsNonCanonical (negative integer, float, true, null, text key), TestRoundTripProperty, FuzzUnmarshal; extension.TestWireData |
| 58.1 | Absent optional fields are omitted; h'' and null never stand for absence |
extension.Encode (nil for empty), extension.Wire.UnmarshalCBOR and Decode (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 |
capsule.TestConformanceFixtures (stage by stage), TestMutationCorpus |
| 64 | Mandatory mutation tests | capsule/mutation_test.go |
capsule.TestMutationCorpus: the 23 listed mutations plus 30 more |
| 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 |
capsule.TestConformanceFixtures |
| 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.CheckSchema |
mutations; codec.TestCheckSchema |
| 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, unknown key, missing key, wrong type, null, wrong type tag (key 0), wrong field length, undefined access_policy, empty optional array or map, extension rules (named by §54 and §57 since v0.8.2) |
ERR_NON_CANONICAL_CBOR |
| Schema version (key 1) other than 1 | ERR_UNSUPPORTED_VERSION |
| Truncated framing, length fields beyond the §57 limits, an object above its §57 frame on encode or decode, data after BODY_CBOR, malformed or unauthenticated age data, 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 | 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 | ERR_DATEKEY_INVALID |
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.
- Pre-genesis instants. §15 defines the candidate formula relative to
genesis_time; instants before it are rejected withERR_DATEKEY_INVALIDinstead of resolving to round 1. - 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). profile_idalphabet.[a-z0-9][a-z0-9:._-]{0,127}, which keeps the canonicaldk1_JSON free of escapes and makes its re-emission trivial.- Number spellings in
dk1_. JSON numbers are compared by value, so1e3or1000.0for 1000 areERR_DATEKEY_NON_CANONICAL, while non-integers, negatives and out-of-range values areERR_DATEKEY_INVALID. Padded or standard-alphabet Base64 and non-zero trailing bits are non-canonical. - Closed maps. Unknown keys in core maps are rejected; applications use extensions (§1, §54).
- 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_idis 1 to 256 bytes of UTF-8;extension.MaxDataLenis 64 MiB, the largest frame, the container frame being the effective bound; an application validates the data of the extensions it knows through the optionalextension.DataValidatorof itsRegistry, and an unknown critical extension is reported before a known one with invalid data. - 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.
- 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.
capsule_digest. Written byEncryptfor every portable key and checked before any request when the capsule reader is seekable; it remains a UX shortcut (§43).- Creation in the past.
Encryptrequires the unlock time to be strictly after the injected clock. - Clock injection. No library package reads the wall clock;
EncryptandOpenrequire aNowfunction, andOpennever requests a release for a round whose time has not been reached.