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
|
|
|
|
# Changelog
|
|
|
|
|
|
|
|
|
|
|
|
All notable changes to this module are documented here. The project follows
|
|
|
|
|
|
semantic versioning; `v0.x` versions make no API stability promise.
|
|
|
|
|
|
|
|
|
|
|
|
## Unreleased — specification v0.8.2
|
|
|
|
|
|
|
|
|
|
|
|
Moves the module to the DateKeys Protocol Specification v0.8.2, whose one
|
|
|
|
|
|
normative change closes the extension format (spec §76). Framing and schema
|
|
|
|
|
|
versions do not change.
|
|
|
|
|
|
|
|
|
|
|
|
The CBOR library is replaced by a codec of the module's own, without
|
|
|
|
|
|
reflection or dependencies. Every valid object encodes to the same bytes as
|
|
|
|
|
|
before: the official vectors and fixtures are unchanged, and every error code
|
|
|
|
|
|
and inspection step of the test suite and the mutation corpus is the same.
|
|
|
|
|
|
|
|
|
|
|
|
### Breaking changes
|
|
|
|
|
|
|
|
|
|
|
|
- `extension.New(id, version, data []byte)` takes the opaque data bytes instead
|
|
|
|
|
|
of a value that it encoded as CBOR, and rejects nil or empty data. An
|
|
|
|
|
|
extension without data is the literal `extension.Extension{ID, Version}`,
|
|
|
|
|
|
which omits key 2.
|
|
|
|
|
|
- Extension data (key 2) must be a byte string of at least one byte. Any other
|
|
|
|
|
|
CBOR type, `null` or `h''` at key 2 is now `ERR_NON_CANONICAL_CBOR`, so a
|
|
|
|
|
|
v0.8.1 object with such data no longer decodes. The base protocol never
|
|
|
|
|
|
decodes the content (§54).
|
|
|
|
|
|
- `codec.Valid` and its fuzz target `codec.FuzzValid` are removed: nothing
|
|
|
|
|
|
decodes extension data any more.
|
|
|
|
|
|
- Package `codec` is rewritten without reflection, struct tags or dependencies
|
|
|
|
|
|
(§58). Removed: `Marshal`, the reflection-based `Unmarshal(data, v)` and
|
|
|
|
|
|
`Peek(data, v)`, and `MaxNestedLevels`, `MaxArrayElements` and
|
|
|
|
|
|
`MaxMapPairs`. Each schema now writes its encoding with a `codec.Encoder`
|
|
|
|
|
|
(`Map`, `Array`, `Uint`, `Bstr`, `Text`, `Out`, with a sticky first error,
|
|
|
|
|
|
and `Fail`, which records an error of the schema so that `Out` never returns
|
|
|
|
|
|
bytes its decoder rejects) and reads it with a strict `codec.Decoder`
|
|
|
|
|
|
(`NewDecoder`, `Map`, `Key`, `EndMap`, `Array`, `Uint`, `Bstr`, `Text`,
|
|
|
|
|
|
`Done`). `codec.Unmarshal(in, decode, encode)` runs the decoder of a schema
|
|
|
|
|
|
and requires that its re-encoding reproduces the input; `codec.Peek(in)`
|
|
|
|
|
|
returns the type tag, of at most `codec.MaxTypeTagLen` (64) bytes, and the
|
|
|
|
|
|
schema version; `codec.Walk(in, maxDepth, maxLen)` checks that bytes are one
|
|
|
|
|
|
item of the §58 profile, for vectors, fuzzing and diagnostics.
|
|
|
|
|
|
`CheckSchema` and `MaxSafeUint` keep their names.
|
|
|
|
|
|
- `extension.Wire` and its `UnmarshalCBOR` are removed. `extension.Encode`
|
|
|
|
|
|
becomes `extension.Canonical`, which returns the validated array in
|
|
|
|
|
|
canonical order as `[]Extension`; `extension.Decode([]Wire)` becomes
|
|
|
|
|
|
`extension.DecodeArray(*codec.Decoder)`, which reads and validates one array
|
|
|
|
|
|
and rejects more than 64 entries from the array head, before reading any;
|
|
|
|
|
|
`extension.EncodeArray(*codec.Encoder, []Extension)` writes one, and
|
|
|
|
|
|
records in the Encoder, instead of writing it, an array that `DecodeArray`
|
|
|
|
|
|
would reject.
|
|
|
|
|
|
- `codec.CheckSchema` reads keys 0 and 1 only, and nothing after them (§70):
|
|
|
|
|
|
the map head, key 0, a type tag of at most 64 bytes, key 1 and the version
|
|
|
|
|
|
must be in the profile, each head in its shortest form, and the version at
|
|
|
|
|
|
most 2^53−1. A schema version other than the expected one read that way is
|
|
|
|
|
|
`ERR_UNSUPPORTED_VERSION` whatever follows it; it was
|
|
|
|
|
|
`ERR_NON_CANONICAL_CBOR` when the rest of the object was malformed. Every
|
|
|
|
|
|
other form of the version is now `ERR_NON_CANONICAL_CBOR`, where it was
|
|
|
|
|
|
`ERR_UNSUPPORTED_VERSION` whenever the value read was not the expected one:
|
|
|
|
|
|
a missing version, `null` or `undefined`, a version not in its shortest
|
|
|
|
|
|
form, a version above 2^53−1 (up to 2^64−1), a version that is not the
|
|
|
|
|
|
second key (placed before key 0 or after another key), and a version
|
|
|
|
|
|
behind a map head or a type tag head not in its shortest form. `true` and
|
|
|
|
|
|
`false` were already `ERR_NON_CANONICAL_CBOR`.
|
|
|
|
|
|
- `capsule.DecodeHeader` checks every CDDL rule of PUBLIC_HEADER, including
|
|
|
|
|
|
`access_policy`, the extension arrays and the cross-array rule, before it
|
|
|
|
|
|
parses the DateKey (§57, §63 step 4). A header that breaks both reports
|
|
|
|
|
|
`ERR_NON_CANONICAL_CBOR` where it reported `ERR_DATEKEY_INVALID` or
|
|
|
|
|
|
`ERR_DATEKEY_NON_CANONICAL`; a header with one fault keeps its code.
|
|
|
|
|
|
- `profile.Profile.CanonicalCBOR` and `Hash` refuse a `profile_id`,
|
|
|
|
|
|
`provider`, `network` or `scheme` that is not valid UTF-8, with
|
|
|
|
|
|
`ERR_NON_CANONICAL_CBOR`: they wrote it as an invalid text string. Every
|
|
|
|
|
|
encoder refuses such text.
|
|
|
|
|
|
- At most 64 extensions per array and `extension_version` at most 2^32−1, on
|
|
|
|
|
|
encode and decode (`ERR_NON_CANONICAL_CBOR`). An `extension_id` appears at
|
|
|
|
|
|
most once per object, and arrays are ordered by `extension_id` only.
|
|
|
|
|
|
- Provider Profile: `genesis_time` is an unsigned integer, and `period` and
|
|
|
|
|
|
`genesis_time` are at most 2^53−1; a negative or larger value is
|
|
|
|
|
|
`ERR_NON_CANONICAL_CBOR`.
|
|
|
|
|
|
- `null` in a byte-string field is `ERR_NON_CANONICAL_CBOR` (for example a
|
|
|
|
|
|
`null` `access_material` was `ERR_ACCESS_INVALID`): nil byte strings, arrays
|
|
|
|
|
|
and maps now encode as empty ones, never as `null`.
|
|
|
|
|
|
- `capsule.EncodeHeader` and `capsule.DecodeHeader` enforce the 1 MiB
|
|
|
|
|
|
PUBLIC_HEADER limit and `accesskey.DecodeBody` the 16 MiB BODY limit. Every
|
|
|
|
|
|
frame-limit refusal, including those of `accesskey.MarshalBody` and of
|
|
|
|
|
|
`capsule.Encrypt` for SEALED_CONTROL, now wraps `ERR_INTEGRITY` (§57).
|
|
|
|
|
|
- `profile.Profile.CanonicalCBOR` refuses a `period` or `genesis_time` outside
|
|
|
|
|
|
the schema with `ERR_NON_CANONICAL_CBOR`, as `profile.Decode` does.
|
|
|
|
|
|
- The official fixture `time_only_extensions` is regenerated: its header data
|
|
|
|
|
|
is the raw UTF-8 bytes of "public label" and its control data is
|
|
|
|
|
|
`{0: 7, 1: "sealed"}` (`a2000701667365616c6564`). Every other `.dkc` and
|
|
|
|
|
|
`.dkk` keeps its bytes; the fixture and vector metadata name spec 0.8.2.
|
|
|
|
|
|
|
|
|
|
|
|
### Added
|
|
|
|
|
|
|
|
|
|
|
|
- `ErrExtensionDataInvalid` (`ERR_EXTENSION_DATA_INVALID`, §69).
|
|
|
|
|
|
- `extension.DataValidator`, an optional interface of a `Registry` that
|
|
|
|
|
|
validates the data of the extensions it knows: a known critical extension
|
|
|
|
|
|
with invalid data fails with `ErrExtensionDataInvalid` (§63 steps 4 and 14,
|
|
|
|
|
|
and the `.dkk` check); a known noncritical one is reported in
|
|
|
|
|
|
`capsule.Inspection.UnusableExtensions`, `capsule.Opened.UnusableControlExtensions`
|
|
|
|
|
|
or `capsule.Opened.UnusableAccessKeyExtensions` (`extension.CheckNoncritical`,
|
|
|
|
|
|
`extension.Unusable`) and does not fail.
|
|
|
|
|
|
- Encoder self-checks: `capsule.Encrypt` decodes its PUBLIC_HEADER and
|
|
|
|
|
|
CONTROL_CBOR, and `accesskey.MarshalBody` its body, with the readers'
|
|
|
|
|
|
decoders before sealing or writing (§72).
|
|
|
|
|
|
- `extension.MaxExtensions`, `MaxVersion`, `MaxDataLen`, `codec.MaxSafeUint`
|
|
|
|
|
|
and `codec.MaxTypeTagLen`.
|
|
|
|
|
|
- The `.dkk` fixture `time_and_key_portable_extension`, which carries a
|
|
|
|
|
|
noncritical extension with data (§68).
|
|
|
|
|
|
- `genfixtures -only NAME[,NAME...]` regenerates the named fixtures only.
|
|
|
|
|
|
- Tests: the three new §64 mutations (data that is not a byte string, `h''`
|
|
|
|
|
|
data, 65 extensions), regression tests for the cases of §76, conformance
|
|
|
|
|
|
checks on the exact data bytes, and the fuzz target
|
|
|
|
|
|
`capsule.FuzzEncodeImpliesDecode` (header, control and `.dkk`).
|
|
|
|
|
|
- The mutation corpus moves from `capsule/mutation_test.go` to
|
|
|
|
|
|
`internal/testkit.Mutations`, shared by the test and by `genfixtures`. Its
|
|
|
|
|
|
third-party X25519 identity is now fixed (`testkit.Stranger`), and every
|
|
|
|
|
|
release source answers with one recorded release, as the exported corpus
|
|
|
|
|
|
describes it. The CLI's inspect view moves to `internal/inspectview`, which
|
|
|
|
|
|
`genfixtures` uses to freeze the outputs.
|
|
|
|
|
|
- `internal/cbortest`, an encoder and decoder of generic CBOR values written
|
|
|
|
|
|
independently of `codec`: tests build with it inputs outside the profile
|
|
|
|
|
|
and check `codec` against it.
|
|
|
|
|
|
- Tests of the map structure of every schema (key order, required and unknown
|
|
|
|
|
|
keys, entry counts) and of the codec, whose statement coverage is 100 %.
|
|
|
|
|
|
- `access_policy` values whose low byte is 0 or 1 (256, 257, 65536, 2^32,
|
|
|
|
|
|
2^53−256…) are tested as undefined, as `FuzzDecodeHeader` seeds and as two
|
|
|
|
|
|
mutations with a consistent `header_binding` (`testkit.Build.RawPolicy`).
|
|
|
|
|
|
- Tests `extension.TestEncodeArrayRejects`,
|
|
|
|
|
|
`accesskey.TestEncodeAndDecodeLeaveNoStaleMaterial`,
|
|
|
|
|
|
`accesskey.TestDecodeShortBodyAllocatesLittle` and `capsule.TestDecryptAll`.
|
|
|
|
|
|
- Shared test data for a second implementation, generated by `genfixtures`,
|
|
|
|
|
|
regenerated by the gate and documented in `testdata/README.md`:
|
|
|
|
|
|
- `testdata/vectors/cbor.json`: 36 accepted and 67 rejected items of the
|
|
|
|
|
|
§58 profile, walked with `codec.Walk` (integers above 2^53−1 as decimal
|
|
|
|
|
|
strings), and 135 schema vectors: minimal valid object, unknown key,
|
|
|
|
|
|
missing key, wrong type, size and range for the Provider Profile,
|
|
|
|
|
|
PUBLIC_HEADER, CONTROL_CBOR, the `.dkk` body, `verification_metadata` and
|
|
|
|
|
|
extensions (data `40` and `5801xx`, data of every other type, 64 and 65
|
|
|
|
|
|
extensions, a leading BOM, the U+FF61/U+10000 order, `extension_version`
|
|
|
|
|
|
2^32−1 and 2^32); schema versions 2^53 and 2^64−1 and the order of type
|
|
|
|
|
|
tag and version; and the Provider Profile validation (names, public key,
|
|
|
|
|
|
`genesis_time`, drand scheme, the chain-hash self-check with its formula,
|
|
|
|
|
|
the `period` limit), each vector keeping the chain hash consistent unless
|
|
|
|
|
|
it tests the self-check.
|
|
|
|
|
|
- `testdata/vectors/mutations.json`: the mutation corpus as frozen data, 55
|
|
|
|
|
|
cases (the 23 of §64 first), each a `.dkc` given as edits of a fixture and
|
|
|
|
|
|
what the reader is given (`.dkk`, identities, the recorded release, clock,
|
|
|
|
|
|
registry, known extensions), with the expected error and step. The 16
|
|
|
|
|
|
capsules built with age randomness are kept from the committed file;
|
|
|
|
|
|
`genfixtures -only mutations` rebuilds them.
|
|
|
|
|
|
- `testdata/vectors/inspect_differential.json`: 1825 deterministic mutations
|
|
|
|
|
|
of the five `.dkc` fixtures (bit flips, byte changes, truncations,
|
|
|
|
|
|
insertions, deletions, length fields, CBOR-aware header edits, DateKey
|
|
|
|
|
|
edits, age header edits) with the verdict of steps 1 to 8.
|
|
|
|
|
|
- `testdata/fixtures/<name>.inspect.json`: the exact output of
|
|
|
|
|
|
`datekeys inspect -json -in <name>.dkc` for each official capsule.
|
|
|
|
|
|
- `testdata/README.md` also states the rules of the reference that the spec
|
|
|
|
|
|
leaves open and the corpora depend on: the order of codes within an
|
|
|
|
|
|
object, the checks of steps 1 to 8 (frame lengths of at least 1, the age
|
|
|
|
|
|
header grammar and parser limits, the round-time ceiling of
|
|
|
|
|
|
9999-12-31T23:59:59Z, the exact tlock stanza arguments), the access
|
|
|
|
|
|
pre-checks and the implementation limits.
|
|
|
|
|
|
- `spec/datekeys.cddl` marks the one-day `period` limit of the Provider
|
|
|
|
|
|
Profile, which `profile.Decode` already applied, as an implementation limit
|
|
|
|
|
|
of the reference (§57, §74); spec §11 allows up to 2^53−1.
|
|
|
|
|
|
- Tests that replay them: `codec.TestSharedVectors`,
|
|
|
|
|
|
`internal/testkit.TestSchemaVectors`, `capsule.TestExportedMutationCorpus`,
|
|
|
|
|
|
`capsule.TestInspectDifferentialCorpus` and
|
|
|
|
|
|
`cmd/datekeys.TestInspectJSONGoldens`.
|
|
|
|
|
|
- Fuzz targets `codec.FuzzDecoder` (the Decoder primitives),
|
|
|
|
|
|
`codec.FuzzWalk` (against the independent decoder), `codec.FuzzPeek`,
|
|
|
|
|
|
`codec.FuzzEncodeImpliesWalk` and `extension.FuzzDecodeArray`, run by
|
|
|
|
|
|
`scripts/fuzz.sh`; `codec.FuzzUnmarshal` now fuzzes a hand-written schema.
|
|
|
|
|
|
|
|
|
|
|
|
### Removed
|
|
|
|
|
|
|
|
|
|
|
|
- The dependencies `github.com/fxamacker/cbor/v2` and
|
|
|
|
|
|
`github.com/x448/float16`. `go.mod` requires nothing new.
|
|
|
|
|
|
|
|
|
|
|
|
### Fixed
|
|
|
|
|
|
|
|
|
|
|
|
- `extension.CheckDisjoint` is a linear merge of the two sorted arrays; a
|
|
|
|
|
|
PUBLIC_HEADER with 40 000 + 40 000 extensions took 8.3 s in the pairwise
|
|
|
|
|
|
check (§76, case 6).
|
|
|
|
|
|
- Control data made of 14 or 15 nested arrays was sealed by `Encrypt` and
|
|
|
|
|
|
rejected by `Open` at step 14, after the unlock (§76, case 5).
|
|
|
|
|
|
- Header data `{NaN: 0, NaN: 1}` gave a nondeterministic verdict (§76, case 3).
|
|
|
|
|
|
- `extension.New(id, v, nil)` wrote `null` as data (§76, case 2).
|
|
|
|
|
|
- `codec.Unmarshal` wipes its re-encoding, which after the new self-checks
|
|
|
|
|
|
held a copy of I_PAYLOAD or `access_material`, and `accesskey.DecodeBody`
|
|
|
|
|
|
wipes the material on its error paths. The `codec.Encoder` also wipes every
|
|
|
|
|
|
buffer it outgrows, and `codec.Unmarshal` sizes its re-encoding for the
|
|
|
|
|
|
input; the former library's internal buffers could keep a copy.
|
|
|
|
|
|
- `accesskey.Encode` wipes the body it wrote, and `accesskey.Decode` reads the
|
|
|
|
|
|
body into a buffer that grows with the data read, wiping every buffer it
|
|
|
|
|
|
outgrows, and wipes the body once decoded or on error: both left copies of
|
|
|
|
|
|
`access_material` behind. The in-memory age decryption of SEALED_CONTROL and
|
|
|
|
|
|
INNER_ACCESS_AGE in `capsule.Open` reads the plaintext into one buffer of
|
|
|
|
|
|
the ciphertext's size instead of a growing one, so that no outgrown buffer
|
|
|
|
|
|
keeps a copy of I_PAYLOAD. Buffers internal to `filippo.io/age` and copies
|
|
|
|
|
|
made by the Go runtime stay out of reach (SECURITY.md).
|
|
|
|
|
|
|
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
|
|
|
|
## Unreleased — v0.1.0
|
|
|
|
|
|
|
|
|
|
|
|
First implementation of the DateKeys Protocol Specification v0.8.1.
|
|
|
|
|
|
|
|
|
|
|
|
### Added
|
|
|
|
|
|
|
|
|
|
|
|
- `datekey`: local date → round resolution at full precision (§15), canonical
|
|
|
|
|
|
`dk1_` encoding and strict parsing (§18, §19).
|
|
|
|
|
|
- `profile`: Provider Profile Deterministic CBOR and `profile_hash` (§11), the
|
|
|
|
|
|
pinned Quicknet profile with its chain-hash self-check (§12), and pinned
|
|
|
|
|
|
registries (§13).
|
|
|
|
|
|
- `provider`: release sources and local BLS verification (§51);
|
|
|
|
|
|
`provider/drand`: racing public relays, verifying every answer (§48, §49, §52).
|
|
|
|
|
|
- `codec`: Deterministic CBOR with a re-encoding canonicality check (§58, §58.1).
|
|
|
|
|
|
- `extension`: the generic extension mechanism (§54).
|
|
|
|
|
|
- `agewrap`: strict tlock and X25519 age identities that enforce the stanza
|
|
|
|
|
|
rules (§27, §29, §32, §33, §35), and a secret-free header probe.
|
|
|
|
|
|
- `capsule`: `.dkc` framing, `Encrypt` for `time_only` and `time_and_key`
|
|
|
|
|
|
(§61, §62), `Inspect` (§63 steps 1–8) and `Open` (§63 steps 9–18).
|
|
|
|
|
|
- `accesskey`: `.dkk` encoding and decoding (§40–§44).
|
|
|
|
|
|
- `cmd/datekeys`: `encrypt`, `decrypt`, `inspect`, `datekey resolve`,
|
|
|
|
|
|
`profile hash`, with atomic, non-overwriting outputs.
|
|
|
|
|
|
- Official vectors (§65, §66), `.dkc`/`.dkk` fixtures (§67, §68), the mutation
|
|
|
|
|
|
corpus (§64), fuzz targets for every parser, interoperability tests with the
|
|
|
|
|
|
official `age` and `tle` CLIs, and live Quicknet integration tests.
|
|
|
|
|
|
- `spec/datekeys.cddl` and `docs/traceability.md`.
|