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/CHANGELOG.md

44 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.11

Moves the module to the DateKeys Protocol Specification v0.11, approved by its author on 1 October 2026 and tagged spec-v0.11, which adds to format 3 what the v0.10 reserved, without changing any format: a reader of v0.10 opens these capsules. SpecVersion is 0.11.

  • Area of 32 KiB. capsule.AreaLen is 32768, and LargeAreaLen, 65536, which EncryptOptions.LargeArea lets the writer use, after the signatures are made, only when what they produced does not fit in 32 KiB. The fixtures of v0.10 keep their 512 bytes (AreaUnit), which a reader accepts.
  • Author signature, alg 1. internal/ed25519strict verifies with the strict profile of §29.9 and authorkey keeps the keys dkauthor1…. EncryptOptions.AuthorKey signs, inside the writer and after its checks; OpenOptions.AuthorKeys are the keys the person saved; Verdicts gives F2, F3 and F4. capsule.PayloadCommit, ControlCommit, HeadDigest, AuthorMessage and SealSubject compute what is signed.
  • Signature with certificates, alg 2. internal/cms and internal/der read the CMS signature and the RFC 3161 token of §29.10 and §29.11 with the standard library only, a closed table of algorithms, and DER checked byte by byte. EncryptOptions.CMSSigner gets AUTHOR_MESSAGE and returns what the person signed outside; the writer checks it and writes nothing unless it is F6. The reader gives F1, F2, F5 and F6 and names the signers.
  • Time seal, seal_type 2. EncryptOptions.Sealer asks for the token over SEAL_SUBJECT; the reader gives S1 to S5 and the authority of a valid seal.
  • Public note. EncryptOptions.PublicNote writes datekeys.note; Header.PublicNote reads it; extension.Standard registers it.
  • Locator and envelope. Package locator: the data of datekeys.capsule, the locator sealed with tlock, and the envelope split into a header and a rest that can hide inside another file.
  • Test data. format3_signed and the vectors of its signature, and the fixtures format3_signature_unsupported and format3_seal_unsupported remade with alg 4294967295.
  • Review. Three independent reviews, of the CMS reader, of the signing logic and writer, and of the locator and the CLI, fixed the form of the signature and of the TSTInfo field by field, the area decided after the signatures, the text of an issuer on screen and the rules of the addresses; spec §76 item 8 records them. Under a valid seal, decrypt shows an mtime later than the seal as an inconsistency (§29.7).
  • CLI. author keygen and author public, encrypt -sign, -note and -large-area, and decrypt -expect-author. Passphrases come from a file or from the standard input.

Unreleased — specification v0.10

Moves the module to the DateKeys Protocol Specification v0.10, which adds capsule format 3, delivery 1 of its design: a capsule holds files, with their paths, sizes, SHA-256 and modification times, a comment and a declared author, and a security area that later versions fill with an author signature and a time seal. EncryptFiles writes format 3; Open and Inspect read the three formats, and capsules of formats 1 and 2 keep their verdicts.

Format 3

  • VERSION 3 in the PRELUDE and CONTROL_CBOR of schema version 3, with the keys of version 2: L is the length of BODY (spec §22, §31).
  • BODY (spec §29.2): a frame of three uint32, AREA_LEN, SECURITY_LEN and HEAD_LEN, the security area, 512 bytes when written and from 512 to 65536 when read, the head and the files. A violation of the frame, L under 12 included, or a byte of the area after SECURITY_CBOR that is not zero, is ERR_INTEGRITY at step 17. capsule.BodyFrame, ParseBodyFrame, CheckArea.
  • Security (spec §29.3, §29.7): a map of version 1 whose keys 2 and 3 are byte strings holding the author signature and the seal, encoded apart. It never fails and never decides the opening: capsule.EvaluateSecurity gives the verdicts X, F0, F1, S0, S1 and S2, and Verdicts.Lines the texts of the table, in Spanish. This version implements no alg and no seal_type.
  • The head (spec §29.4): its type tag and version 1, then the CDDL with R1 and R8, then, in key order, the comment and the declared author, each file with R2 to R6c, R10 and its layout, R7 and R9 over the tree, and the critical extensions. A violation of a rule of layer 4 is the new ERR_HEAD_INVALID. capsule.Head, File, EncodeHead, DecodeHead, and extension.Head for its extensions.
  • Paths and texts (spec §29.5, §29.6, §29.5.1): internal/pathrule applies the rules with tables generated from 19 pinned data files, Unicode 18.0.0 and the 15 WindowsBestFit tables, never with the Unicode functions of the platform; TablesDigest pins them. Its errors name the rule and the character, never echo the text, and read the same in every implementation. R4b and the invisibles rule of §29.6 keep ZWJ, ZWNJ, VS15 and VS16 to their emoji and script uses.

Reader (spec §63 step 17, §56, §57)

  • capsule.Sink receives the files: Begin with the validated head, Create for each file, Commit at step 18 only, and Abort once after any failure that follows Begin. A format 3 capsule without OpenOptions.Sink fails right after step 2 with capsule.ErrSinkRequired, a caller error without a code, before any request; a capsule of format 1 or 2 without dst fails there too.
  • Step 17 in its substeps: the frame and the area, security, the head, the files filling CONTENT, the SHA-256 of each file and the padding. A failure of age, or a plaintext whose length is not P, prevails; otherwise the first substep that fails decides, and a code other than ERR_INTEGRITY is reported only after reading PAYLOAD_AGE to its end.
  • Reads of BODY grow with the bytes received, never with AREA_LEN, HEAD_LEN or a declared size (spec §57).
  • Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions.

Writer (spec §61, §62, §62.1)

  • capsule.EncryptFiles writes format 3 from a list of capsule.Source, each read twice: first to check the paths and the texts with the rules of the reader, measure L with a head whose salt and SHA-256 are zero, and hash each file; then to write it, failing if its size or SHA-256 changed (rules 14 to 18). Files go in the byte order of their paths; the comment has its CR LF turned into LF; the mtime is kept from 1970 to 9999 and omitted otherwise. The head, the control and security are decoded with the rules of the reader before anything is written (rule 17), and the area is always 512 bytes with the empty security (rule 13).
  • EncryptOptions gains Comment, Author, HeadCritical, HeadNoncritical and TestVectors. Encrypt writes format 2 only with TestVectors, for generators of test vectors (rule 1). Result.Head is the head written.

CLI

  • datekeys encrypt takes -in several times, files and folders; a folder gives its name as the first segment and is walked with Lstat, following no link, taking regular files only and leaving out .DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX, which it reports. New -comment, -author and -no-mtime. A pipe is no longer accepted.
  • datekeys decrypt writes the files of a format 3 capsule to the new folder -out: os.Mkdir claims it, only when there are files; the tree is staged in -out/.datekeys-* through an os.Root, with O_EXCL and mode 0600; the mtimes are set and each entry of the first level is moved into place at step 18, and any failure removes the folder. Formats 1 and 2 still write a file.
  • The presentation of spec §29.7 goes to stdout: the verdicts, the declared author and the comment box with their labels, the paths, and the verdicts again. Each line of the creator goes in pieces of at most W − 3 columns behind │ , counting 2 for anything but printable ASCII, with its TABs expanded; W is the width of the terminal, asked through syscall on Unix and Windows, or 80. Shortcuts, desktop.ini, .git, programs and a leading dash get a warning.

Errors

  • ERR_HEAD_INVALID joins the catalogue, last (spec §69): 18 codes.

Test data

  • datekeys.SpecVersion is 0.10, and every test data file says so.
  • Nine fixtures of format 3 (spec §67): format3_single, format3_tree, format3_comment_only, format3_bloque256 and format3_time_and_key_portable, written with EncryptFiles, and format3_area_1024, format3_security_v2, format3_signature_unsupported and format3_seal_unsupported, which only a generator of test vectors writes. Their records add the area, security, the head, the salt, the comment, the declared author, each file and the verdicts, and their plaintext file is BODY. The .dkk of formats 2 and 3 join the .dkk tests (spec §68).
  • New vectors: paths.json, path_fold.json, head_schema.json and security.json; cbor.json gains the control of version 3.
  • The mutation corpus gains the 33 mutations of the first two lists of spec §64 on format 3 and the 47 of the list of format 3, three of which open with their verdicts; format 2 gains "format 2 time_only relabeled format 3", and "version changed" sets VERSION 4. 209 cases, 169 of the spec. testkit.Splice gives an edit for each run of changed bytes.
  • The differential corpus gains two bases of format 3, one per policy: 5110 cases, the earlier ones unchanged.
  • The tests of the paths and of format 3 hold their invisible and combining characters as Go escapes.

Unreleased — specification v0.9

Moves the module to the DateKeys Protocol Specification v0.9, which adds capsule format 2 (spec §76, "Cambios normativos de la v0.9"): until the unlock date it hides the exact length of the content and the number of credentials. Encrypt writes format 2 only; Open and Inspect read both formats, and a format 1 capsule, as v0.8.2 wrote it, keeps its verdict.

Format 2

  • VERSION in the PRELUDE is the capsule format, 1 or 2 (spec §22, §23): capsule.Format, Format1, Format2 and Prelude.Format. Any other value is ERR_UNSUPPORTED_VERSION at step 2, before any request, so a v0.8.2 reader rejects a format 2 capsule without a network request.
  • CONTROL_CBOR has the schema version of its format (spec §31). Version 2 adds key 6, payload_length, L in exactly 8 bytes, big-endian, at most L_MAX = 2^53 − 2^46, and key 7, padding, 1 (bloque256) or 2 (reforzado). A control of the other version is ERR_UNSUPPORTED_VERSION at step 14; a violation of keys 6 and 7 is ERR_NON_CANONICAL_CBOR. Without extensions a version 2 control is 103 bytes, whatever L and the code. EncodeControl and DecodeControl take the format.
  • The payload is padded (spec §29.1): its plaintext is the content followed by zeros up to P = rule(L). capsule.PaddedLength computes both rules on 64-bit integers, and capsule.PayloadAgeLength the length of PAYLOAD_AGE. Step 17 checks that the plaintext is exactly P bytes and its padding zero, or ERR_INTEGRITY. Open writes only the first L bytes to dst, never the padding (spec §56), records step 17, and reports the format, L, the rule and P in Opened.
  • In format 2 INNER_ACCESS_AGE holds exactly 16 X25519 stanzas (spec §39), agewrap.AccessSlots: agewrap.CheckAccessStanzas and agewrap.NewAccessIdentity take the number of slots, 0 for format 1. Another number is ERR_POLICY_STRUCTURE_MISMATCH at step 12, and again at step 13.

Writer rules (spec §62.1)

  • EncryptOptions.Length, L, is required: the control is sealed before the content is read, and a source that delivers another number of bytes is an error. EncryptOptions.Padding chooses the rule; zero means reforzado.
  • time_and_key takes from 1 to 16 credentials, the recipients and the portable key together. Each free slot gets a dummy, a fresh X25519 public key whose private key is dropped at once, and the 16 recipients are shuffled with an unbiased Fisher–Yates over crypto/rand.
  • A recipient that is not canonical (bit 255 set, or u ≥ p) or of low order is rejected: agewrap.CheckX25519Recipient.
  • Self-checks: INNER_ACCESS_AGE has 16 X25519 stanzas with distinct shares and the portable key opens exactly one; PAYLOAD_AGE has the length P gives and I_PAYLOAD opens its header.
  • Result reports the format, L, the rule and P.

CLI

  • datekeys encrypt measures its input, copying a file that is not regular to a temporary file first, and takes -padding reforzado|bloque256. Its report and that of decrypt give the format and the lengths, and decrypt warns that format 1 hides neither the number of credentials nor the exact length. datekeys inspect reports the format, "format" in -json.

Test data

  • Seven format 2 fixtures (spec §67), whose records give L, the rule, P and the INNER_ACCESS_AGE stanza each credential opens. The five format 1 fixtures of v0.8.2 keep their bytes as compatibility fixtures, and genfixtures never regenerates them; their records name spec 0.9, the format and stage 17.
  • vectors/padding.json, new: both rules for the rows of spec §29.1, checked against a Padmé computed with math/big.
  • vectors/cbor.json: the control vectors carry format, absent meaning 1, and gain the cases of version 2; "unknown key 6" is now "key 6, defined only in schema version 2".
  • vectors/mutations.json: "version changed" writes VERSION 3; the 33 mutations of the first two lists of spec §64 also run on format 2 fixtures ("format 2: …"), followed by the 22 of its third list and their companions with a .dkk. The new cases derive from fixtures without randomness: testkit seals them again with their known file keys and nonces (reseal.go), and reproduces each fixture byte for byte when nothing is edited.
  • vectors/inspect_differential.json keeps its 1 825 cases and adds a block for each format 2 fixture: 4 380 cases.

Unreleased — specification v0.8.2

Moves the module to the DateKeys Protocol Specification v0.8.2, whose normative change closes the extension format (spec §76), refined, amended and corrected before release (see "Specification refinements", "Specification amendment: point canonicality" and "Specification corrections: formal review"). 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 four 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 .dkk as an object, then its binding to the capsule, then credentials, then the round time, then the request), and the errors of a .dkk come at step 9.a even when it is decoded earlier, and never under time_only. The optional inspection of steps 5, 6 and 8 and the capsule_digest check can change the code, and so could, until the corrections of the formal review, whether a network source verified the release. 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 .dkk body, 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 .dkk extensions 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.md stated 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 of dk1_ (§19: either Base64 alphabet but no CR or LF, one JSON object in valid UTF-8 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, with period at most 2^32 − 1 and the name alphabets normative; the name lengths stay implementation limits), extension_id of 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.Open checks an offered .dkk as an object before binding it to the capsule: access_type and access_material, then its critical extensions, then capsule_id, then capsule_digest. A .dkk for another capsule with an unknown critical extension is now ERR_EXTENSION_CRITICAL_UNKNOWN (was ERR_ACCESS_INVALID), and one with an unsupported access_type and an unknown critical extension is ERR_ACCESS_INVALID (was ERR_EXTENSION_CRITICAL_UNKNOWN).
  • accesskey.Decode rejects a BODY_LEN of 0 with ERR_INTEGRITY, like a PUBLIC_HEADER_LEN of 0; it was ERR_NON_CANONICAL_CBOR, the empty body failing to decode.
  • datekey.Parse rejects CR and LF in a dk1_ string with ERR_DATEKEY_INVALID; the Go Base64 decoders skipped them, and the result was ERR_DATEKEY_NON_CANONICAL. It also rejects invalid UTF-8 in the JSON at step 2 (see "Fixed").
  • datekeys decrypt -dkk hands the .dkk to capsule.Open still encoded, through the new OpenOptions.AccessKeyFile, so its decoding errors come at step 9 of a time_and_key capsule, after any failure of steps 1 to 8, and a time_only capsule ignores it; the CLI used to fail on it before reading the capsule.
  • capsule.Open ignores nil entries of OpenOptions.Identities: with no other credential, the result is ERR_ACCESS_REQUIRED at step 9, before the clock is consulted, instead of ERR_RELEASE_UNAVAILABLE or a release request.
  • agewrap.AccessIdentity tries every identity on every stanza: an identity that unwraps two INNER_ACCESS_AGE stanzas is ERR_POLICY_STRUCTURE_MISMATCH even when another identity unwraps exactly one, whatever their order; the file used to open when the other came first.
  • profile.NewRegistry encodes and decodes each profile before comparing its profile_hash, so a profile gets the code profile.Decode reports: a period of 86401 s is ERR_NON_CANONICAL_CBOR, as in Decode, not ERR_UNKNOWN_PROFILE. Profile.Validate checks 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, with ERR_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.

Specification amendment: point canonicality

An amendment of the unreleased v0.8.2, recorded with its case in spec §76. The new §12.2 defines the canonical encoding of a BLS12-381 point, the compressed form of drand: the compression flag set, the infinity flag only for the point at infinity with every other bit zero, the sort flag for the lexicographically largest y, big-endian coordinates below p (c1 then c0 in G2) and a point of the prime-order subgroup. A decoder rejects every other string, among them x + p and an identity with a payload. The Provider Profile public key (§12.1), the release signature (§63 step 10) and the U of the tlock stanza (§63 step 11) are canonical and never the point at infinity, and step 11 defines the stanza body, U || V || W with |V| = |W| = 16 (128 bytes for Quicknet), and the IBE check r·G == U. The case, from the second implementation: tlock-js on @noble/curves 1.9.7 accepted U re-encoded as c0 + p and a signature re-encoded as x + p and returned the same file key, where the reference rejects both.

No error code or step of the reference changes: its decoder, kilic/bls12-381 through drand, already rejected those encodings. A U at infinity, which only the IBE check used to reject, is refused before decryption, with the same ERR_INTEGRITY. Spec §64 gains ten mutations, exported in mutations.json: U with c0 + p, U at infinity, U with the infinity flag and a payload, and tlock stanza bodies of 127 and 129 bytes, each with a valid header MAC (ERR_INTEGRITY, step 11); a release signature with x + p, at infinity, with the infinity flag and a payload, negated, and negated together with U with c0 + p (ERR_RELEASE_INVALID, step 10). The x + p case is a capsule for round 1004, whose published signature testkit now knows (testkit.XPlusPRound): no fixture round has an x below 2^381 − p. No fixture and no other vector changes.

Tests: profile.TestDrandPointDecodersAreCanonical, which fails if a dependency update makes the decoders of drand lenient, and TestPublicKeyEncodingIsCanonical; new cases in provider.TestVerifyRejects, agewrap.TestTimeIdentityStrictness and TestTimeIdentityRelease; capsule.TestPointMutationsChangeOnlyTheEncoding, which checks that each point mutation differs from a capsule that opens only in one encoding; internal/testkit.TestPointReencodings.

Specification corrections: formal review

Corrections of the unreleased v0.8.2 from the formal review of the specification, recorded with their cases in spec §76 ("Correcciones de la revisión formal"). Three change normative rules:

  • A source that fetches releases over a network (a relay, the Release API or a cache) must verify every response with the rules of step 10 and discard the one that fails; when none passes, the code is ERR_RELEASE_UNAVAILABLE at step 9 (spec §63). The codes of step 10 are those of a release supplied directly, as in the official vectors. provider/drand.Client already did this; the contract of provider.ReleaseSource and the documentation of capsule.OpenOptions.Source now say so.
  • Each registered extension declares the objects (PUBLIC_HEADER, CONTROL_CBOR, .dkk) and the arrays where it may appear, and a known extension elsewhere is treated as unknown there (spec §31, §54, §72): a critical one is ERR_EXTENSION_CRITICAL_UNKNOWN, a noncritical one is ignored. The new optional interface extension.Placement of a Registry tells where each extension is registered; capsule.Inspect and capsule.Open check the arrays of PUBLIC_HEADER (step 4), of the .dkk (step 9.a) and of CONTROL_CBOR (step 14) with the new extension.CheckCriticalIn and extension.CheckNoncriticalIn. A Registry that does not implement it behaves as before.
  • H2, H3 and H4 of step 11 are those of drand/kyber encrypt/ibe, and H2 hashes the element of GT in the order of kilic/bls12-381: c1 before c0 at every level of the tower, each coordinate of Fp in 48 bytes big-endian. The new vector file testdata/vectors/tlock_ibe.json freezes H2(e(G1, G2)) = cb87319f24560b5231579a09ad79f12e; the order of Fp12.toBytes of noble, c0 first, gives 0118eea9d5971745f71e3c94926f1717 and another file key.

The other findings are editorial: §27 defers the authenticity of PUBLIC_HEADER to the trust model (§55.1) and says that the age header MAC protects only against whoever does not know the file key; step 5 separates the mandatory read of SEALED_CONTROL from the optional inspection of its age header; step 15 names ERR_HEADER_BINDING; §21 makes the 16 CSPRNG bytes of capsule_id a MUST; §77 gains drand/kyber, RFC 8259, RFC 8610 and RFC 4648; §76 retitles its section "Cambios normativos de la v0.8.2" and corrects its record: dk1.json gained four vectors, not three, and two cases of the refinements quoted texts that no earlier version contained.

No error code of the reference changes, nor any fixture or existing vector. github.com/drand/kyber-bls12381, already an indirect dependency through drand and tlock, becomes a direct requirement: internal/testkit and a test compute the pairing with it.

Tests: capsule.TestReleaseFromANetworkSource (a relay whose only answer is a release of another round, or a negated signature, gives ERR_RELEASE_UNAVAILABLE at step 9 through provider/drand.Client, and the same release supplied directly the code of step 10); capsule.TestExtensionPlacement (an extension registered for CONTROL_CBOR only is unknown in PUBLIC_HEADER and in a .dkk, one registered as noncritical only is unknown in a critical array, and a noncritical copy outside its registration is ignored); extension.TestPlacement; agewrap.TestTlockH2Vector (the frozen vector, and step 11 recomputed with H2 and H4 against the file key that tlock unwraps).

A second round of the same review confirmed these corrections and asked for three more, recorded in spec §76 as corrections 4 to 6:

  • An encoder must not write a registered extension in an object or array it is not registered for, and a reader that ignores a noncritical one for that reason must not interpret its data (spec §54, §72). A writer could seal in the critical extensions of CONTROL_CBOR one registered there as noncritical only, which a reader that knows the registration rejects at step 14, after the unlock. capsule.Encrypt and accesskey.Encode take no Registry: they write the extensions they are given, and the application, which knows the registration, applies the rule.
  • §17 and §51 give the codes of step 10 only for a release supplied directly, as step 10 does; one that a network source fetches and that breaks those rules is discarded at step 9.
  • Whatever the failure of the release source, step 9 reports ERR_RELEASE_UNAVAILABLE and no other code (spec §63).

The other findings are editorial: §28.1 says that steps 5 and 6 parse only the age header, since step 5 reads all of SEALED_CONTROL; step 9 has a single arrow to ERR_RELEASE_UNAVAILABLE; §76 rewords two introductions. testdata/README.md says where the extensions of the mutation corpus are registered: in both arrays of every object.

These error results of the reference change, for inputs that no official vector holds:

  • capsule.Open reports any error of OpenOptions.Source at step 9 with ERR_RELEASE_UNAVAILABLE as its only code. An error that carries another normative code, or none, keeps only its text: a caller's source that failed with ERR_RELEASE_INVALID gave ERR_RELEASE_INVALID at step 9. An error that wraps ERR_RELEASE_UNAVAILABLE alone is returned as it is, with its other causes, such as a context error.
  • The error of provider/drand.Client.Fetch wraps ERR_RELEASE_UNAVAILABLE and no other normative error, as errors.go promises for every error of the module. The failure of each relay is kept in its text only, where errors.Join wrapped it: a relay that answered with a release of another round also made the error match ERR_ROUND_MISMATCH. A context that ended is still wrapped, whether Fetch sees it end before or after the relays fail because of it.

Tests: capsule.TestReleaseSourceErrorsAtStep9 (a source error with another code, two codes or none gives ERR_RELEASE_UNAVAILABLE alone at step 9 and keeps its text); capsule.TestExtensionPlacement, which now also covers the noncritical array of a .dkk, so that checking it with the object-blind extension.CheckNoncritical fails; provider/drand.TestFetchErrorHasOneCode and TestUnavailabilityAndCancellation (a canceled context and a deadline during the request); datekeys.TestCode.

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

  • datekeys.SpecVersion (0.8.2), the specification the module implements, and datekeys.Version(), the version of the module as the go command recorded it: a tag, the pseudo-version of the commit of a checkout build, or (devel). datekeys version prints both and the Go toolchain.
  • capsule.OpenOptions.AccessKeyFile, a .dkk still encoded, which Open decodes at step 9.a and only for a time_and_key capsule (§63, §69.1).
  • ErrExtensionDataInvalid (ERR_EXTENSION_DATA_INVALID, §69).
  • extension.Placement, an optional interface of a Registry that tells in which objects and arrays each extension is registered, with extension.Object (PublicHeader, Control, AccessKey), extension.Array (Critical, Noncritical), extension.KnownIn, extension.CheckCriticalIn and extension.CheckNoncriticalIn (§54, §72). CheckCritical and CheckNoncritical, which do not know the object, keep their behaviour and consult no Placement.
  • testdata/vectors/tlock_ibe.json, the H2 vector of §63 step 11, generated by internal/testkit.IBEVectors and documented in testdata/README.md.
  • 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, 65 cases (the 33 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 17 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 points 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.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

  • Error messages no longer copy the text of an error of age, tlock, kyber, drand or kyber-bls12381. When the IBE check of a tlock stanza failed, kyber's error carried the candidate plaintext and r, and agewrap.TimeIdentity copied it into its error, and so into the error of capsule.Open and the details of Inspection.Checks: a third party who edited W learned FK_TIME from the message. Each such failure now has a fixed message with its normative error and a reason of its own: the length of the tlock stanza body, the encoding of U, U at infinity or the IBE check; the header or the STREAM of an age file; a malformed X25519 stanza. A failure of the writer of the plaintext at step 17 keeps its own text. capsule.TestTlockFailureDiagnosticsCarryNoSecrets, TestPlaintextWriterFailureKeepsItsText.
  • datekey.Parse rejects invalid UTF-8 in the dk1_ JSON at step 2, as §19 requires. encoding/json replaced it with U+FFFD, so a member that a repeated name overwrites passed steps 2 and 3 and ended as ERR_DATEKEY_NON_CANONICAL instead of ERR_DATEKEY_INVALID. Found by the second implementation's differential; new dk1.json vector.
  • 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).

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.

Powered by TurnKey Linux.