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

144 lines
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.
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.

Powered by TurnKey Linux.