15 KiB
Traceability: DateKeys Protocol Specification v0.8.1 ↔ 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.1.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 |
Profile.CanonicalCBOR, Profile.Hash, profile.Decode |
profile.TestQuicknetMatchesGoldenVector, TestQuicknetCBORLayout, TestDecodeRoundTrip, 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 | capsule.Header, EncodeHeader, DecodeHeader |
capsule.TestConformanceFixtures, FuzzDecodeHeader; 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 | capsule.Control, EncodeControl, DecodeControl |
capsule.TestConformanceFixtures, FuzzDecodeControl; 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) |
accesskey.TestEncodeRejectsAbsenceAsEmptyMap |
| 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 | extension |
extension/*; mutations unknown critical … extension; capsule.TestKnownCriticalExtensions |
| 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 | capsule.MaxPublicHeaderLen, MaxSealedControlLen, accesskey.MaxBodyLen, codec limits |
mutations …_LEN above the limit; accesskey.TestDecodeRejects body length above the limit |
| 58 | Canonical CBOR | codec.Marshal, codec.Unmarshal (re-encoding comparison), codec.Valid |
codec.TestUnmarshalRejectsNonCanonical, TestValid, TestRoundTripProperty, FuzzValid |
| 58.1 | Absent optional fields are omitted | extension.Encode (nil for empty), re-encoding check, accesskey verification map |
codec empty optional array present; accesskey empty extension array, empty verification map, null verification |
| 59 | Supply-chain security | pinned go.mod/go.sum, .github/workflows, .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 | 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 20 listed mutations plus 25 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 |
testdata/fixtures/*.dkk + *.dkk.json |
accesskey.TestFixtures |
| 69 | Normative errors | 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 | extension.Registry, extension.Set |
capsule.TestKnownCriticalExtensions |
| 75 | Blocking requirements before v1.0 | items 1–9 above; item 10 (external review) pending | — |
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, wrong type tag (key 0), wrong field length, undefined access_policy, empty optional array or map, extension rules |
ERR_NON_CANONICAL_CBOR |
| Schema version (key 1) other than 1 | ERR_UNSUPPORTED_VERSION |
| Truncated framing, length fields beyond the §57 limits, 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.1 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. Key 2 is optional and omitted when absent; data must be
deterministic CBOR without tags.
extension_idis 1 to 256 bytes of UTF-8. No V1 schema allows repeating anextension_id. - 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.