Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
|
# 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.
|
|
|
|
|
|
|
|
|
|
|
|
1. **Pre-genesis instants.** §15 defines the candidate formula relative to
|
|
|
|
|
|
`genesis_time`; instants before it are rejected with `ERR_DATEKEY_INVALID`
|
|
|
|
|
|
instead of resolving to round 1.
|
|
|
|
|
|
2. **Round bounds.** `dk1_` accepts rounds in 1..2^53−1 so that JSON parsers
|
|
|
|
|
|
based on IEEE 754 doubles read the same integer; a profile further limits
|
|
|
|
|
|
rounds to round times up to 9999-12-31T23:59:59Z (Quicknet: 83 903 165 811).
|
|
|
|
|
|
3. **`profile_id` alphabet.** `[a-z0-9][a-z0-9:._-]{0,127}`, which keeps the
|
|
|
|
|
|
canonical `dk1_` JSON free of escapes and makes its re-emission trivial.
|
|
|
|
|
|
4. **Number spellings in `dk1_`.** JSON numbers are compared by value, so
|
|
|
|
|
|
`1e3` or `1000.0` for 1000 are `ERR_DATEKEY_NON_CANONICAL`, while
|
|
|
|
|
|
non-integers, negatives and out-of-range values are `ERR_DATEKEY_INVALID`.
|
|
|
|
|
|
Padded or standard-alphabet Base64 and non-zero trailing bits are
|
|
|
|
|
|
non-canonical.
|
|
|
|
|
|
5. **Closed maps.** Unknown keys in core maps are rejected; applications use
|
|
|
|
|
|
extensions (§1, §54).
|
|
|
|
|
|
6. **Extension data.** Key 2 is optional and omitted when absent; data must be
|
|
|
|
|
|
deterministic CBOR without tags. `extension_id` is 1 to 256 bytes of UTF-8.
|
|
|
|
|
|
No V1 schema allows repeating an `extension_id`.
|
|
|
|
|
|
7. **One stanza per recipient.** Enforced as far as a recipient can observe it:
|
|
|
|
|
|
no repeated X25519 ephemeral share, and no identity that unwraps more than
|
|
|
|
|
|
one stanza.
|
|
|
|
|
|
8. **Strict tlock stanza arguments.** Exact string comparison, as the tlock
|
|
|
|
|
|
library itself does for the chain hash; a round with leading zeros is a
|
|
|
|
|
|
mismatch.
|
|
|
|
|
|
9. **`capsule_digest`.** Written by `Encrypt` for every portable key and
|
|
|
|
|
|
checked before any request when the capsule reader is seekable; it remains
|
|
|
|
|
|
a UX shortcut (§43).
|
|
|
|
|
|
10. **Creation in the past.** `Encrypt` requires the unlock time to be strictly
|
|
|
|
|
|
after the injected clock.
|
|
|
|
|
|
11. **Clock injection.** No library package reads the wall clock; `Encrypt` and
|
|
|
|
|
|
`Open` require a `Now` function, and `Open` never requests a release for a
|
|
|
|
|
|
round whose time has not been reached.
|