20 KiB
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), refined before release (see "Specification refinements"). 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.
Specification refinements
v0.8.2 is unreleased, so these refinements amend it without a version change;
spec §76 records each with its reproducible cases. No valid object changes,
nor the verdict of any existing official vector or fixture; dk1.json gains
three vectors for the reading rules of §19.
- Layered error precedence (spec §69.1, with §57 and §63): within one object
the code of the first failing layer is reported: frame, then type tag and
schema version (keys 0 and 1), then the CBOR profile and the CDDL, then the
fields with codes of their own in ascending key order; the rules with
codes of their own belong to the last layer, not to the CDDL layer. Across
objects and steps, the step order of §63 decides; step 9 now spells out its
order (the
.dkkas an object, then its binding to the capsule, then credentials, then the round time, then the request), and the errors of a.dkkcome at step 9.a even when it is decoded earlier, and never undertime_only. Only the optional steps 5, 6 and 8 and thecapsule_digestcheck can change the code. Steps 10, 11, 13 and 17 give the codes of the release verification (round, then signature) and of each identity. - Trust model (spec §55.1): who can write PUBLIC_HEADER, CONTROL_CBOR,
PAYLOAD_AGE and the
.dkkbody, from which step each is bound and by what, and what none of them proves. Spec §72: an extension with security-relevant claims lives in CONTROL_CBOR or is signed by a signature extension; the data of.dkkextensions is advisory for its holder only. - One ordering rule for extension arrays (spec §31, §54, CDDL): strictly
ascending unsigned bytewise order of the UTF-8 bytes of
extension_id, a proper prefix first, never UTF-16 code units or a locale collation. - Rules that only
testdata/README.mdstated are now normative text: the age header grammar of C2SP and its codes (§28.1, §36), the round-time bound of 9999-12-31T23:59:59Z and pre-genesis instants (§15), the reading rules ofdk1_(§19: either Base64 alphabet but no CR or LF, one JSON object without a byte order mark, numbers by their exact decimal value), lengths of at least 1 (§22, §23, §40, §57), the comparison of the tlock stanza arguments (§35, §63 step 8), the Provider Profile rules and the chain-hash formula (§12.1, withperiodat most 2^32 − 1 and the name alphabets normative; the name lengths stay implementation limits),extension_idof at least one byte (§31), a repeated X25519 ephemeral share in INNER_ACCESS_AGE (§36), and the implementation limits of the reference (§74).
These error codes of the reference change, for inputs that no existing official vector holds:
capsule.Openchecks an offered.dkkas an object before binding it to the capsule:access_typeandaccess_material, then its critical extensions, thencapsule_id, thencapsule_digest. A.dkkfor another capsule with an unknown critical extension is nowERR_EXTENSION_CRITICAL_UNKNOWN(wasERR_ACCESS_INVALID), and one with an unsupportedaccess_typeand an unknown critical extension isERR_ACCESS_INVALID(wasERR_EXTENSION_CRITICAL_UNKNOWN).accesskey.Decoderejects aBODY_LENof 0 withERR_INTEGRITY, like aPUBLIC_HEADER_LENof 0; it wasERR_NON_CANONICAL_CBOR, the empty body failing to decode.datekey.Parserejects CR and LF in adk1_string withERR_DATEKEY_INVALID; the Go Base64 decoders skipped them, and the result wasERR_DATEKEY_NON_CANONICAL.datekeys decrypt -dkkhands the.dkktocapsule.Openstill encoded, through the newOpenOptions.AccessKeyFile, so its decoding errors come at step 9 of atime_and_keycapsule, after any failure of steps 1 to 8, and atime_onlycapsule ignores it; the CLI used to fail on it before reading the capsule.capsule.Openignores nil entries ofOpenOptions.Identities: with no other credential, the result isERR_ACCESS_REQUIREDat step 9, before the clock is consulted, instead ofERR_RELEASE_UNAVAILABLEor a release request.agewrap.AccessIdentitytries every identity on every stanza: an identity that unwraps two INNER_ACCESS_AGE stanzas isERR_POLICY_STRUCTURE_MISMATCHeven when another identity unwraps exactly one, whatever their order; the file used to open when the other came first.profile.NewRegistryencodes and decodes each profile before comparing itsprofile_hash, so a profile gets the codeprofile.Decodereports: aperiodof 86401 s isERR_NON_CANONICAL_CBOR, as inDecode, notERR_UNKNOWN_PROFILE.Profile.Validatechecks the schema rules a value can break (a period that is not a whole number of seconds in 1..86400, a genesis time outside 0..2^53−1, a name that is not valid UTF-8) first, withERR_NON_CANONICAL_CBOR.
Tests: capsule.TestPrecedenceWithinPublicHeader, TestPrecedenceAcrossSteps,
TestFrameLengthLowerBounds, TestMalformedAgeHeaders,
TestTlockStanzaArgumentComparison, TestAccessKeyCheckOrder,
TestAccessKeyFileAtStep9, TestControlCriticalBeforeHeaderBinding and
TestTrustModel; accesskey.TestDecodePrecedence;
agewrap.TestAccessIdentityStrictness; datekey.TestReadingRules;
profile.TestDecodePrecedence (a G1 point outside the prime-order
subgroup), TestPinPathMatchesDecode and TestChainHashFormula;
provider.TestVerifyRejects; extension.TestOrderIsUnsignedBytewise;
cmd/datekeys.TestDecryptAccessKeyOrder.
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 literalextension.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,
nullorh''at key 2 is nowERR_NON_CANONICAL_CBOR, so a v0.8.1 object with such data no longer decodes. The base protocol never decodes the content (§54). codec.Validand its fuzz targetcodec.FuzzValidare removed: nothing decodes extension data any more.- Package
codecis rewritten without reflection, struct tags or dependencies (§58). Removed:Marshal, the reflection-basedUnmarshal(data, v)andPeek(data, v), andMaxNestedLevels,MaxArrayElementsandMaxMapPairs. Each schema now writes its encoding with acodec.Encoder(Map,Array,Uint,Bstr,Text,Out, with a sticky first error, andFail, which records an error of the schema so thatOutnever returns bytes its decoder rejects) and reads it with a strictcodec.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 mostcodec.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.CheckSchemaandMaxSafeUintkeep their names. extension.Wireand itsUnmarshalCBORare removed.extension.Encodebecomesextension.Canonical, which returns the validated array in canonical order as[]Extension;extension.Decode([]Wire)becomesextension.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 thatDecodeArraywould reject.codec.CheckSchemareads 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 isERR_UNSUPPORTED_VERSIONwhatever follows it; it wasERR_NON_CANONICAL_CBORwhen the rest of the object was malformed. Every other form of the version is nowERR_NON_CANONICAL_CBOR, where it wasERR_UNSUPPORTED_VERSIONwhenever the value read was not the expected one: a missing version,nullorundefined, 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.trueandfalsewere alreadyERR_NON_CANONICAL_CBOR.capsule.DecodeHeaderchecks every CDDL rule of PUBLIC_HEADER, includingaccess_policy, the extension arrays and the cross-array rule, before it parses the DateKey (§57, §63 step 4). A header that breaks both reportsERR_NON_CANONICAL_CBORwhere it reportedERR_DATEKEY_INVALIDorERR_DATEKEY_NON_CANONICAL; a header with one fault keeps its code.profile.Profile.CanonicalCBORandHashrefuse aprofile_id,provider,networkorschemethat is not valid UTF-8, withERR_NON_CANONICAL_CBOR: they wrote it as an invalid text string. Every encoder refuses such text.- At most 64 extensions per array and
extension_versionat most 2^32−1, on encode and decode (ERR_NON_CANONICAL_CBOR). Anextension_idappears at most once per object, and arrays are ordered byextension_idonly. - Provider Profile:
genesis_timeis an unsigned integer, andperiodandgenesis_timeare at most 2^53−1; a negative or larger value isERR_NON_CANONICAL_CBOR. nullin a byte-string field isERR_NON_CANONICAL_CBOR(for example anullaccess_materialwasERR_ACCESS_INVALID): nil byte strings, arrays and maps now encode as empty ones, never asnull.capsule.EncodeHeaderandcapsule.DecodeHeaderenforce the 1 MiB PUBLIC_HEADER limit andaccesskey.DecodeBodythe 16 MiB BODY limit. Every frame-limit refusal, including those ofaccesskey.MarshalBodyand ofcapsule.Encryptfor SEALED_CONTROL, now wrapsERR_INTEGRITY(§57).profile.Profile.CanonicalCBORrefuses aperiodorgenesis_timeoutside the schema withERR_NON_CANONICAL_CBOR, asprofile.Decodedoes.- The official fixture
time_only_extensionsis 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.dkcand.dkkkeeps its bytes; the fixture and vector metadata name spec 0.8.2.
Added
capsule.OpenOptions.AccessKeyFile, a.dkkstill encoded, whichOpendecodes at step 9.a and only for atime_and_keycapsule (§63, §69.1).ErrExtensionDataInvalid(ERR_EXTENSION_DATA_INVALID, §69).extension.DataValidator, an optional interface of aRegistrythat validates the data of the extensions it knows: a known critical extension with invalid data fails withErrExtensionDataInvalid(§63 steps 4 and 14, and the.dkkcheck); a known noncritical one is reported incapsule.Inspection.UnusableExtensions,capsule.Opened.UnusableControlExtensionsorcapsule.Opened.UnusableAccessKeyExtensions(extension.CheckNoncritical,extension.Unusable) and does not fail.- Encoder self-checks:
capsule.Encryptdecodes its PUBLIC_HEADER and CONTROL_CBOR, andaccesskey.MarshalBodyits body, with the readers' decoders before sealing or writing (§72). extension.MaxExtensions,MaxVersion,MaxDataLen,codec.MaxSafeUintandcodec.MaxTypeTagLen.- The
.dkkfixturetime_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 targetcapsule.FuzzEncodeImpliesDecode(header, control and.dkk). - The mutation corpus moves from
capsule/mutation_test.gotointernal/testkit.Mutations, shared by the test and bygenfixtures. 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 tointernal/inspectview, whichgenfixturesuses to freeze the outputs. internal/cbortest, an encoder and decoder of generic CBOR values written independently ofcodec: tests build with it inputs outside the profile and checkcodecagainst 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_policyvalues whose low byte is 0 or 1 (256, 257, 65536, 2^32, 2^53−256…) are tested as undefined, asFuzzDecodeHeaderseeds and as two mutations with a consistentheader_binding(testkit.Build.RawPolicy).- Tests
extension.TestEncodeArrayRejects,accesskey.TestEncodeAndDecodeLeaveNoStaleMaterial,accesskey.TestDecodeShortBodyAllocatesLittleandcapsule.TestDecryptAll. - Shared test data for a second implementation, generated by
genfixtures, regenerated by the gate and documented intestdata/README.md:testdata/vectors/cbor.json: 36 accepted and 67 rejected items of the §58 profile, walked withcodec.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.dkkbody,verification_metadataand extensions (data40and5801xx, data of every other type, 64 and 65 extensions, a leading BOM, the U+FF61/U+10000 order,extension_version2^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, theperiodlimit), 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.dkcgiven 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 mutationsrebuilds them.testdata/vectors/inspect_differential.json: 1825 deterministic mutations of the five.dkcfixtures (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 ofdatekeys inspect -json -in <name>.dkcfor each official capsule.testdata/README.mdpoints to the sections of the specification that decide each verdict (§12.1, §15, §19, §22, §23, §28.1, §63, §69.1, §74), which the refinements above moved into normative text.
spec/datekeys.cddlmarks the one-dayperiodlimit of the Provider Profile, whichprofile.Decodealready 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.TestInspectDifferentialCorpusandcmd/datekeys.TestInspectJSONGoldens. - Fuzz targets
codec.FuzzDecoder(the Decoder primitives),codec.FuzzWalk(against the independent decoder),codec.FuzzPeek,codec.FuzzEncodeImpliesWalkandextension.FuzzDecodeArray, run byscripts/fuzz.sh;codec.FuzzUnmarshalnow fuzzes a hand-written schema.
Removed
- The dependencies
github.com/fxamacker/cbor/v2andgithub.com/x448/float16.go.modrequires nothing new.
Fixed
datekey.Parserejects invalid UTF-8 in thedk1_JSON at step 2, as §19 requires.encoding/jsonreplaced it with U+FFFD, so a member that a repeated name overwrites passed steps 2 and 3 and ended asERR_DATEKEY_NON_CANONICALinstead ofERR_DATEKEY_INVALID. Found by the second implementation's differential; newdk1.jsonvector.extension.CheckDisjointis 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
Encryptand rejected byOpenat 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)wrotenullas data (§76, case 2).codec.Unmarshalwipes its re-encoding, which after the new self-checks held a copy of I_PAYLOAD oraccess_material, andaccesskey.DecodeBodywipes the material on its error paths. Thecodec.Encoderalso wipes every buffer it outgrows, andcodec.Unmarshalsizes its re-encoding for the input; the former library's internal buffers could keep a copy.accesskey.Encodewipes the body it wrote, andaccesskey.Decodereads 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 ofaccess_materialbehind. The in-memory age decryption of SEALED_CONTROL and INNER_ACCESS_AGE incapsule.Openreads 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 tofilippo.io/ageand copies made by the Go runtime stay out of reach (SECURITY.md).
Unreleased — v0.1.0
First implementation of the DateKeys Protocol Specification v0.8.1.
Added
datekey: local date → round resolution at full precision (§15), canonicaldk1_encoding and strict parsing (§18, §19).profile: Provider Profile Deterministic CBOR andprofile_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:.dkcframing,Encryptfortime_onlyandtime_and_key(§61, §62),Inspect(§63 steps 1–8) andOpen(§63 steps 9–18).accesskey:.dkkencoding and decoding (§40–§44).cmd/datekeys:encrypt,decrypt,inspect,datekey resolve,profile hash, with atomic, non-overwriting outputs.- Official vectors (§65, §66),
.dkc/.dkkfixtures (§67, §68), the mutation corpus (§64), fuzz targets for every parser, interoperability tests with the officialageandtleCLIs, and live Quicknet integration tests. spec/datekeys.cddlanddocs/traceability.md.