Spec §38.1: the official SDK SHOULD warn not to reuse a password, since
once the date has come the capsule lets whoever holds it test guesses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SHOULDs of the official SDK that the CLI did not follow yet. encrypt
writes next to the .dkc the recovery annex, FILE.dkc.recuperacion.txt
(spec §62.1, rule 27): datekeys.RecoveryAnnex, annex/recovery.md, which is
§79 of the specification under a title with its version and SHA-256, the
same for every capsule; TestRecoveryAnnex checks it against the text of
SpecVersion. -no-recovery leaves it out. encrypt also says what opening the
capsule years later will take (rule 26): the .dkc, a credential of a
time_and_key capsule, and the release of its round, which an archive of
releases or a cache service must keep if drand no longer serves it; and
beyond one year it recommends time_and_key to a time_only capsule (§7.6).
profile.Status and StatusOf give the state of a pinned profile in the
registry of §71, which DateKeys does not publish yet: Quicknet is active.
encrypt writes no capsule with a profile that is not active, and decrypt
and inspect warn when the profile of a capsule is compromised.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For whoever does not trust the random numbers of a computer, as the author
decided: five dice for each word, read in a fixed order, give a number
from 11111 to 66666, the position of the word in a list of 7776 words, the
first die the most significant. wordkey.DiceNumber and DiceWord number the
words and give the word of a number; DiceWords takes several numbers, at
least 6 and never the same word twice, which is rolled again; DiceList is
the list numbered for dice, a line for each word with its dice and a tab,
as the EFF publishes its own: for en it is the file of the EFF, byte for
byte.
encrypt -dice TEXT and -dice-file FILE take the words from the dice of the
list -dic and show them: the words open the capsule, the numbers give them
only with that list. datekeys wordlist [-dic LIST] writes the numbered
list, to print it, and its SHA-256, which wordkey/lists/README.md records.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wordkey/lists/en.txt is the large wordlist for passphrases of the EFF,
7776 words, CC BY 4.0 under its copyright policy, without the dice number
of each line and in its order, so that the position of a word still gives
its dice, 11111 for the first. encrypt -new-words takes it by default, as
the author decided; -dic es takes the Spanish one. The alphabet of en is a
to z and the ASCII hyphen of its four compound words, such as t-shirt,
kept so that no word loses its dice. lists/README.md records its source,
the SHA-256 of the download, the change and the license.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wordkey.CheckList takes the language of the list and refuses a word with a
character that is not a letter of its alphabet, which only the code gives:
for es, a to z, the five vowels with an acute accent, u with diaeresis and
n with tilde, in lower case and NFC. A Cyrillic letter that looks like a
Latin one, a capital, a digit or the carriage return of a file with CRLF
lines would be written down and typed again with another character, and
the capsule would not open. The Spanish list passes as it is; its SHA-256
does not change.
wordkey.Bits is the strength of the words that Generate draws, log2 of the
number of draws in order, and encrypt -new-words prints it: 90 bits for 7
words of 7776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wordkey.Generate draws words uniformly with crypto/rand from a built-in
list, and encrypt -new-words FILE writes them to a new file (-dic, default
es; -word-count, default 7). wordkey.List and CheckList refuse a list of
fewer than 2048 words or with two words that are one once normalized
(spec 38.1). The Spanish list, 7776 words, is a draft not yet reviewed,
licensed CC BY-SA 4.0 as an adaptation of FrequencyWords; its source,
method and SHA-256 are in wordkey/lists/README.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The author's decision of 7 October 2026. A release saved next to a capsule
cannot exist when the capsule is made, and once the date comes the capsule
can be opened: such a file only opens it again and does not cover the real
case, someone opening it decades later when drand is gone and nobody saved
anything. Long-term recovery rests instead on archives and cache services
that keep the releases of all rounds; a reader asks for its round and
verifies the signature against the pinned key.
Spec: the .dkr extension (section 20, back to v0.14), sections 1, 4, 8, 45,
47.1, 49, 50 (rewritten), 53, 62.1 (rule 28 removed, rule 26 reworded),
63, 70, 73, 74 (the datekeys.release .dkk extension dropped too), 76
(the v0.15 block, with the discarded design) and the annex 79. The
release object, the chain hash at step 10, step 9.c option B and the
archive format stay.
Code: decrypt -save-release and the command datekeys release are gone,
with writeRelease and their tests; decrypt -release FILE stays. The
release objects of testdata/releases are now <round>.cbor, and
TestVectorFilesAreCurrent fails on a file the generator no longer writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release of a round becomes a file, .dkr: a release object in
deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round,
4: signature}, which provider.EncodeRelease writes and DecodeRelease reads
with its layers (size, type and version, schema). provider.ParseRelease
also reads drand's JSON as the input of the caller. Verify checks the chain
hash a release names before its round and its signature, with
ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the
informative format of the draft.
capsule.OpenOptions.Release takes a release in hand, a provider.Supplier,
exclusive with Source: Open does not compare it with the clock (step 9.c,
option B) and reports a clock behind it in Opened.ClockBehind; a network
source is still never asked before the round time. The CLI gains
decrypt -release FILE (.dkr, drand's JSON or a local archive),
decrypt -save-release FILE.dkr and the command release, which fetches,
verifies and saves the .dkr without opening the capsule.
Test data: vectors/release.json, releases/<round>.dkr for rounds 1000,
1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In
mutations.json every case says its source, "supplied" or "network"; the
case "round not reached yet", a release in hand, now opens, and four cases
are added: the same with a network source, a release of another round from
a network source, and two release objects of another chain. SpecVersion
stays 0.14 until the author approves the draft.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What the update of the documentation found without a test: Header.UnusableNote
and the notice of decrypt, the helper that keeps a panic of a parser of
security in its own part, and that EncryptFiles writes nothing of the
capsule while it waits for a signature (spec 62.1 rules 19 and 25). The
usage text of decrypt -expect-author says that it writes no file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the details of the review: author keygen -plain no longer speaks of a
passphrase that the file does not have, and decrypt -expect-author leaves a
prelude that does not parse to Open, which reports it at step 1 or 2 with
its code, instead of calling it a capsule that is not of format 3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes of the review of the session of 1 and 2 October that the text of
spec v0.11 already asks for:
- authorkey: String and GoString hide the secret key, which only Secret
returns; ParsePublic refuses a key that is not a point of the curve
(ed25519strict.OnCurve, checked against the square root of testkit).
- capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never
a capsule without the signature or the seal that was asked for. A panic
while evaluating the signature or the seal fails only that part, F1 or
S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can
refuse to publish the files.
- extension.CheckWrite, the rule of encoders of spec 72: the writers of
capsules and .dkk files refuse datekeys.note and datekeys.capsule outside
the arrays where they are registered, or with invalid data.
- CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE
before it signs (rule 20); decrypt -expect-author compares the key of an
F4 and writes nothing unless it matches; decrypt notifies a public note
that it does not show; the lines of the verdicts break at the last space
that fits, each row after the first behind a mark, so that the terminal
never breaks them; L is the payload, not the content.
- locator: a reader rejects an address that breaks 44.1 and keeps the
others; addresses refuse the special-purpose blocks of IANA, IPv6 outside
2000::/3, localhost and local names, characters outside RFC 3986, dot
segments, and a CID that does not decode to version 1 and a multihash;
ParseInfo checks that the locator is an age file with one tlock stanza
for the round of its DateKey; Info.Extension reads what it writes; its
errors carry no normative code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CheckURI works on the raw authority, as an HTTP client reads it: no percent
signs, userinfo or backslashes, a host of letters, digits and hyphens or a
public IP literal, a port from 1 to 65535, and Host returns that host. The
integers of the locator stop at 2^53 - 1, and Info.Extension refuses what
ParseInfo would. extension.Standard validates datekeys.capsule through
locator.Standard, and Info.OpenLocator ties the locator to the round of its
own DateKey.
inspect shows the public note as text of the creator, with its prefix and
wrapping and the warning, and says when a note is unusable. encrypt -note
warns that it is public. decrypt -expect-author fails before the release
is requested when the capsule is not format 3. Author key files are read
with the work factor of the spec as their maximum, the passphrase is not
read from a terminal, and two copies of secrets are cleared.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
extension.CheckNote, NewNote and Note apply the rules of spec v0.11 24.1,
and extension.Standard registers the note for the noncritical array of
PUBLIC_HEADER only. Header.PublicNote reads it, and header_binding ties it to
the control: a note changed after writing fails step 15. The CLI writes it
with -note and shows it as text of the creator, with the warning that nobody
can check it before the date.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The passphrase of a key comes from a file, or from the standard input with
"-", never from the command line or the environment, so the CLI needs no
terminal library. decrypt -expect-author shows the signature as always and
then fails, with the files already written, unless it is F3 with that key.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
As the spec v0.11 draft decides after the review (38.1):
- the salt is "DateKeys llave de palabras v2|chain|round|capsule_id", so
the same words give another key in each capsule and a dictionary
cannot attack together the many capsules of a popular round;
- the words are lowered with the table of Unicode 18.0.0 of pathrule;
- wordkey.Check refuses controls, Default_Ignorable code points and
unassigned ones, and counts toward the six words only the different
ones of three letters or more.
EncryptOptions.Words takes the words: the writer derives their identity
once it has drawn capsule_id, and adds its recipient to the credentials.
The CLI passes them to the writer, and decrypt salts them with the
capsule_id of the capsule. New vector of 38.1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The author asked for keys that people can keep without files. Package
wordkey derives the X25519 identity of words a person chooses, at least
six: their NFD by the tables of pathrule without the marks U+0300 to
U+036F, each code point in lower case by its simple mapping, split at
white space, joined by one space, and stretched with PBKDF2-HMAC-SHA256
of the standard library, 600 000 rounds, salted with the chain hash and
the round of the capsule. It is wordkey.ts of datekeys-ts byte for byte:
both check the same vector.
encrypt takes -words or -words-file for a time_and_key capsule, and adds
the key as one more recipient, derived for the round of -at; decrypt
takes them and adds the identity, for the round the capsule shows. The
format does not change. Checked: the CLI opened a capsule that the page
wrote with words, with the release from the public relays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
decrypt creates the folder of a format 3 capsule at step 17, after the
release is requested. With a missing parent it failed only then, from
the Sink, reported with ERR_INTEGRITY as any failure of the output is.
It now checks the parent right after the prelude, with a message of
its own and no request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reference moves to the DateKeys Protocol Specification v0.9, approved
by its author on 29 September 2026. Encrypt writes capsule format 2 only;
Open and Inspect read formats 1 and 2, and a format 1 capsule keeps the
verdict v0.8.2 gave it.
Format 2 (spec §22, §29.1, §31, §39):
- VERSION in the PRELUDE is the capsule format, capsule.Format; any other
value is ERR_UNSUPPORTED_VERSION at step 2.
- CONTROL_CBOR has the schema version of its format. Version 2 adds key 6,
payload_length (8 bytes, big-endian, at most L_MAX = 2^53 - 2^46), and
key 7, padding (1 bloque256, 2 reforzado); it is 103 bytes without
extensions, whatever L.
- The payload is the content padded with zeros to P = rule(L). Step 17
checks the length and the zeros, and Open writes only the first L bytes.
- INNER_ACCESS_AGE holds exactly 16 X25519 stanzas: 1 to 16 credentials,
and a dummy in each slot left, in a uniformly random order.
Writer rules (spec §62.1): EncryptOptions.Length is required and the
source must deliver exactly that many bytes; recipients that are not
canonical or of low order are rejected (agewrap.CheckX25519Recipient);
self-checks of the header, the control, INNER_ACCESS_AGE and PAYLOAD_AGE.
The CLI measures its input, takes -padding and reports the format.
Test data: seven format 2 fixtures, padding vectors checked against
math/big, format 2 CBOR vectors, and the mutation corpus in both formats
with the 22 cases of the third list of spec §64, built without randomness
by sealing the fixtures again with their known keys and nonces. The
format 1 fixtures are kept byte for byte and never regenerated; the
differential corpus keeps its 1825 cases and adds a block per format 2
fixture. The spec copy loses its "to be implemented" markers, and the
READMEs, CHANGELOG, traceability and testdata/README.md follow v0.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- datekeys.SpecVersion ("0.8.2") names the specification the module
implements. A test ties it to the spec file, its title and
spec/README.md, and TestCatalogueMatchesSpec and the vector files use
it (testkit.SpecVersion now aliases it), so the vectors regenerate
unchanged.
- datekeys.Version() is the version of the module as the go command
recorded it. That is a tag, or for a binary built in a checkout the
pseudo-version of its commit (for example
v0.0.0-20260928105528-9ac9cd952f04), or (devel) when it is unknown, as
in tests or under a replace directive to a directory. It works as the
main module and as a dependency, whatever the module path, which it
reads from the root package.
- `datekeys version` (also -version and --version) prints both and the
Go toolchain.
- README.md and README.es.md explain the three versions (format,
specification, module) and what the code on main covers.
traceability §70 and CHANGELOG follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Approved refinements, each recorded with its reproducible case in the
§76 v0.8.2 subsection:
- §69.1: layered error model with normative precedence (frame, type tag
and version, CBOR profile and CDDL, then fields with their own code in
ascending key order; across steps the §63 order decides), with a scope
paragraph for the optional steps 5, 6 and 8.
- §55.1: normative trust table per section (who can write it, from which
step it is bound, what it never proves); §72: security-relevant claims
go in CONTROL_CBOR or under a signature, .dkk data is advisory.
- §31/§54: extension arrays in strictly ascending unsigned byte order of
extension_id (one rule for order and uniqueness).
- Gaps a second implementation needed: §28.1 malformed age headers,
§15/§19 latest unlock time and dk1_ reading rules, §22/§23/§57 length
lower bounds, §63 step 8 tlock argument comparison and step 9 order,
§12.1 profile validation with the drand chain-hash formula, §74 table
of implementation limits.
Reference alignment: .dkk errors only at step 9.a (new
OpenOptions.AccessKeyFile, used by the CLI), CR/LF in dk1_ is
ERR_DATEKEY_INVALID, BODY_LEN 0 is ERR_INTEGRITY, nil identities are not
credentials, and AccessIdentity tries every identity on every stanza so
its verdict does not depend on their order. dk1.json gains three
vectors; every other testdata file is byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 2b: package codec is rewritten without reflection or struct tags. A
strict Decoder accepts only the spec §58 profile, Unmarshal decodes,
re-encodes and compares, Peek reads the type tag and version, and Walk is a
bounded iterative helper for vectors and fuzzing. Every schema has its own
hand-written encoder and decoder that checks all CDDL rules before the
fields with their own error codes. github.com/fxamacker/cbor/v2 and
github.com/x448/float16 are gone; nothing replaces them. Valid objects
encode and decode exactly as before (1.34 million differential verdicts);
the invalid-input differences are documented in CHANGELOG and
traceability decision 12. A review found and fixed an access_policy check
that truncated to uint8.
Step 3: testdata gains vectors/cbor.json (generic and per-schema CBOR
vectors), vectors/mutations.json (the 55-case mutation corpus, replayable
offline), vectors/inspect_differential.json (1,825 fixed-seed mutations
with the Go verdict) and one inspect -json golden per fixture, all
regenerated byte-identically by genfixtures and documented in
testdata/README.md for second implementations.
Gate green with 90 s of fuzzing per target on all 15 targets; codec at
100 % coverage; pre-existing testdata byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The module is imported as g.activething.com/go/DateKeys, the path the
project's Gitea advertises. The .github directory is gone: workflows
now live in .gitea/workflows, use the gitea.com action mirrors and
install every tool from its Go module; releases go to this Gitea with
goreleaser and a key-based cosign signature; Dependabot is replaced by
a nightly report of available updates. scripts/check.sh runs the same
checks on any machine and is the gate while the server has no runner.
SECURITY.md, README and CONTRIBUTING no longer refer to GitHub.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>