|
|
# datekeys-go
|
|
|
|
|
|
Reference implementation in Go of the **DateKeys Protocol Specification
|
|
|
v0.15** ([`spec/`](spec/DateKeys_Protocol_Specification_v0.15.md)), tagged
|
|
|
`spec-v0.15`,
|
|
|
on the long-term recovery of capsules: the release object, a release in
|
|
|
hand that the clock does not stop, archives and cache services that keep the
|
|
|
releases of all rounds, and an annex to open a capsule without DateKeys
|
|
|
software.
|
|
|
[Versión en español](README.es.md).
|
|
|
|
|
|
DateKeys encrypts data so that it can only be opened after a chosen instant.
|
|
|
The time condition comes from the drand **Quicknet** randomness beacon: data is
|
|
|
sealed with timelock encryption to a future round, and the round's BLS
|
|
|
signature, published by drand when the round arrives, is the key. Everything
|
|
|
that can be verified locally is verified locally; relays, caches and APIs are
|
|
|
untrusted transports.
|
|
|
|
|
|
> **Status: v0.x, pre-standard.** The specification is a draft and the API may
|
|
|
> change before v1.0.0. The code has not had an external cryptographic review
|
|
|
> yet (spec §75). Do not rely on it for high-value secrets.
|
|
|
|
|
|
## What it implements
|
|
|
|
|
|
| Object | Spec | Package |
|
|
|
|---|---|---|
|
|
|
| DateKey: date → round, canonical `dk1_…` string | §14–§19 | [`datekey`](datekey) |
|
|
|
| Provider Profile, pinned Quicknet profile, `profile_hash` | §10–§13 | [`profile`](profile) |
|
|
|
| Release sources, local BLS verification, drand relays | §45–§52 | [`provider`](provider), [`provider/drand`](provider/drand) |
|
|
|
| DateKeyCap `.dkc`: `time_only` and `time_and_key`, the files of format 3, the security area and its verdicts | §20–§39, §61–§63 | [`capsule`](capsule) |
|
|
|
| Paths and texts of a head, with the Unicode 18.0.0 and best-fit tables | §29.5, §29.6 | [`internal/pathrule`](internal/pathrule) |
|
|
|
| Author signature: what is signed, `alg` 1 and 2, the seal of `seal_type` 2 | §29.8–§29.11 | [`capsule`](capsule) |
|
|
|
| Author keys `dkauthor1…` and the strict Ed25519 of `alg` 1 | §29.9, §29.12 | [`authorkey`](authorkey), [`internal/ed25519strict`](internal/ed25519strict) |
|
|
|
| Signature with certificates (`alg` 2, CMS) and time seal (RFC 3161): DER, the profile of the certificate, the closed table of algorithms | §29.10, §29.11 | [`internal/cms`](internal/cms), [`internal/der`](internal/der) |
|
|
|
| The public note `datekeys.note` | §24.1 | [`extension`](extension), [`capsule`](capsule) |
|
|
|
| Key of words | §38.1 | [`wordkey`](wordkey) |
|
|
|
| DateKeys Access Key `.dkk` | §40–§44 | [`accesskey`](accesskey) |
|
|
|
| The extension `datekeys.capsule`: locator, envelope and addresses | §44.1 | [`locator`](locator) |
|
|
|
| Extensions and their registry | §54, §72 | [`extension`](extension) |
|
|
|
| Deterministic CBOR | §58 | [`codec`](codec) |
|
|
|
| Normative errors | §69 | [`errors.go`](errors.go) |
|
|
|
| CLI | — | [`cmd/datekeys`](cmd/datekeys) |
|
|
|
|
|
|
No cryptography is implemented here. Encryption is [age](https://age-encryption.org)
|
|
|
(`filippo.io/age`); the timelock is [tlock](https://github.com/drand/tlock)
|
|
|
(its exported core only); BLS verification is drand's; author signatures and
|
|
|
seals are verified with the standard library of Go, with the strict profile of
|
|
|
Ed25519 checked around `crypto/ed25519`. This module adds framing, CBOR, DER,
|
|
|
bindings, verification rules and the flow, and it enforces the stanza rules of
|
|
|
the protocol inside the age identities, so that a file is never accepted just
|
|
|
because age could unwrap a key.
|
|
|
|
|
|
Not implemented on purpose: the Release API server and queue, storage and
|
|
|
delivery, the download of the rest of an envelope, and the TypeScript client
|
|
|
(plan §2). Package `locator` checks the addresses of a locator and what a
|
|
|
reader brings from them, and downloads nothing (spec §44.1). The signature with
|
|
|
certificates and the seal check the cryptography, never who issued a
|
|
|
certificate or a seal, or whether it was revoked: that is for an official
|
|
|
validator (spec §29.10). Brainpool curves and national algorithms such as GOST
|
|
|
or SM2 are outside the table.
|
|
|
|
|
|
## Versions
|
|
|
|
|
|
Three version numbers, each with its own meaning:
|
|
|
|
|
|
| Version | Where | Changes when |
|
|
|
|---|---|---|
|
|
|
| Format | Inside the objects: the capsule format, the `VERSION` of the DKC1 prelude, 3 when written and 1 to 3 when read, which is also the schema version of CONTROL_CBOR; and 1 for the framing of DKK1 and the schema of the other objects, the head and security of format 3 included | The format changes. A reader rejects a version it does not know (spec §22, §70) |
|
|
|
| Specification | `datekeys.SpecVersion`, today `0.15`, and the tag `spec-v0.15`. A draft, such as v0.15 was, has no tag and does not change it | The normative text changes. Spec §76 records each change with its case |
|
|
|
| Module | The tags of this Go module, `vX.Y.Z`, and `datekeys.Version()` | The API or the behaviour changes. Semantic versioning, with no stability promise before v1.0.0 |
|
|
|
|
|
|
`datekeys version` prints the module version, the specification and the Go toolchain. A binary built in a checkout shows the pseudo-version of its commit, for example `v0.0.0-20260928105528-9ac9cd952f04`.
|
|
|
|
|
|
Each release states what it covers, here and in the [CHANGELOG](CHANGELOG.md). The code of this branch, not yet released, covers:
|
|
|
- specification 0.15: it writes capsule format 3 and reads formats 1, 2 and 3;
|
|
|
- the pinned Quicknet profile, and any profile of the scheme `bls-unchained-g1-rfc9380`, the only one spec v0.14 admits;
|
|
|
- encryption, inspection and opening, author signatures and seals, the public note, the key of words and the locator, and the CLI;
|
|
|
- every shared vector and fixture of [`testdata/`](testdata).
|
|
|
|
|
|
The first tag, v0.1.0, comes once `go get` works from a clean machine.
|
|
|
|
|
|
## A capsule, in one picture
|
|
|
|
|
|
```text
|
|
|
.dkc = PRELUDE (16 B) || PUBLIC_HEADER (CBOR) || SEALED_CONTROL (age) || PAYLOAD_AGE (age, to EOF)
|
|
|
|
|
|
time_only: SEALED_CONTROL = age(tlock round R → CONTROL_CBOR)
|
|
|
time_and_key: SEALED_CONTROL = age(tlock round R → age(16 X25519 stanzas → CONTROL_CBOR))
|
|
|
CONTROL_CBOR = { header_binding = SHA-256(PRELUDE || PUBLIC_HEADER), I_PAYLOAD, extensions, L, padding rule }
|
|
|
PAYLOAD_AGE = age(X25519 R_PAYLOAD → BODY || zeros up to P = rule(L)), streamed
|
|
|
|
|
|
BODY = AREA_LEN || SECURITY_LEN || HEAD_LEN (3 × uint32)
|
|
|
|| SECURITY_CBOR, zeros up to AREA_LEN (32 KiB) author signature and seal, or empty
|
|
|
|| HEAD_CBOR salt, comment, declared author, the files
|
|
|
|| the files, one after another
|
|
|
```
|
|
|
|
|
|
This is format 3, the one `encrypt` writes (spec §22, §29.2). Its 16 stanzas
|
|
|
hold from 1 to 16 credentials and a dummy in each slot left, in a random
|
|
|
order, and its payload is padded to P, so that until the unlock date nobody
|
|
|
learns the number of credentials, the exact length L, the names of the files
|
|
|
or how many there are, beyond what P bounds (spec §29.1, §39, §55.2). The
|
|
|
security area measures 32 KiB, signed or not, so that P does not tell whether
|
|
|
a capsule is signed, and 64 KiB only when the creator asks for it because a
|
|
|
signature does not fit (spec §29.2). The writers of v0.10 wrote 512 bytes,
|
|
|
which a reader still accepts. Once open, it gives files with their paths,
|
|
|
sizes, SHA-256 and modification times, a comment and a declared author, and
|
|
|
the verdicts of its security area.
|
|
|
Format 2, that of v0.9, holds one content; format 1, that of v0.8.2, has one
|
|
|
stanza per credential and no padding. Readers still open both.
|
|
|
|
|
|
## CLI
|
|
|
|
|
|
```bash
|
|
|
go install g.activething.com/go/DateKeys/cmd/datekeys@latest
|
|
|
```
|
|
|
|
|
|
The module is served by the project's own Gitea, whose certificate Go does not
|
|
|
trust by default. Set `GOPRIVATE=g.activething.com` so that the Go proxy and
|
|
|
checksum database are bypassed for it, and either install the server's
|
|
|
certificate or, on a trusted network, set `GOINSECURE=g.activething.com`.
|
|
|
|
|
|
```bash
|
|
|
datekeys datekey resolve -at 2030-01-01T00:00:00Z
|
|
|
datekeys encrypt -at 2030-01-01T00:00:00Z -in letter.txt -out letter.dkc
|
|
|
datekeys encrypt -at 2030-01-01T00:00:00Z -in photos -in letter.txt -comment "For Ana" -author "Juan" -out gift.dkc
|
|
|
datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -dkk gift.dkk -in photos -out gift.dkc
|
|
|
datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -words-file words.txt -in letter.txt -out letter.dkc
|
|
|
datekeys wordlist -dic en > words-for-dice.txt
|
|
|
datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -dice-file dice.txt -in letter.txt -out letter.dkc
|
|
|
datekeys encrypt -at 2030-01-01T00:00:00Z -padding bloque256 -no-mtime -in letter.txt -out letter.dkc
|
|
|
datekeys author keygen -out author.key -pass-file pass.txt
|
|
|
datekeys encrypt -at 2030-01-01T00:00:00Z -in letter.txt -note "Letters from Lisbon" -sign author.key -sign-pass-file pass.txt -out letter.dkc
|
|
|
datekeys decrypt -in letter.dkc -out letter -expect-author dkauthor1...
|
|
|
datekeys inspect -in gift.dkc
|
|
|
datekeys decrypt -in gift.dkc -out gift -dkk gift.dkk
|
|
|
datekeys decrypt -in gift.dkc -out gift -dkk gift.dkk -release round.cbor
|
|
|
datekeys profile hash
|
|
|
datekeys version
|
|
|
```
|
|
|
|
|
|
`encrypt` writes next to the capsule `FILE.dkc.recuperacion.txt`, the annex of the specification, in Spanish, on how to open a capsule without DateKeys software (spec §79), unless `-no-recovery`; and says what opening the capsule years later will take: 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 (spec §50). Beyond one year, it recommends `time_and_key` to a `time_only` capsule (spec §7.6). A profile that is not active in the registry of §71 writes no capsule, and `decrypt` and `inspect` warn when the profile of a capsule is compromised.
|
|
|
|
|
|
`encrypt` never touches the network. Each `-in` is a file or a folder; a
|
|
|
folder gives its name as the first segment of its paths, as a browser does,
|
|
|
and is walked without following links, taking regular files only and leaving
|
|
|
out `.DS_Store`, `Thumbs.db`, `desktop.ini`, `._*` and `__MACOSX`. A path or
|
|
|
a text that breaks a rule of spec §29.5 or §29.6 is refused, naming the rule
|
|
|
and the character. The files keep their modification times unless
|
|
|
`-no-mtime`. The content is padded with the rule reforzado, or bloque256 if
|
|
|
asked (spec §29.1).
|
|
|
`inspect` runs only the pre-unlock checks (spec §63 steps 1–8): it never
|
|
|
requests a release and never uses a secret.
|
|
|
`decrypt` fetches the release from public drand relays and verifies it
|
|
|
locally, never before the round time; with `-release` it takes the release
|
|
|
in hand instead, a release object, drand's JSON or a local release
|
|
|
archive, from any source, without any request and whatever the clock says
|
|
|
(spec v0.15, §47.1, §63 step 9.c). The files of a format 3 capsule go to the new folder `-out`, staged
|
|
|
inside it and moved into place only after every check passed (spec §56); a
|
|
|
capsule with only a comment creates no folder. It shows the verdicts first
|
|
|
and last, and the declared author, the comment and the paths as text of the
|
|
|
creator that nobody has checked, each line behind a `│ ` prefix and never
|
|
|
wider than the terminal (spec §29.7). A line of the verdicts breaks at the
|
|
|
last space that fits, and each row after its first starts with ` ↳ ` (two
|
|
|
spaces, U+21B3 and a space): the terminal never breaks one, and no name of a
|
|
|
certificate can start a row and pass for a verdict. Formats 1 and 2 still give
|
|
|
one file. Outputs are never overwritten.
|
|
|
|
|
|
`author keygen` makes an Ed25519 author key (spec §29.12) in a file encrypted
|
|
|
with a passphrase, or as text with `-plain`, and prints its public key,
|
|
|
`dkauthor1…`; `author public` prints it again. The passphrase comes from a
|
|
|
file, or from the standard input with `-`, never from the command line or the
|
|
|
environment. `encrypt -sign` signs the capsule with it (`alg` 1): before it
|
|
|
signs it shows the key and the code of `AUTHOR_MESSAGE` (spec §62.1 rule 20),
|
|
|
and it checks the signature before writing anything. `decrypt` shows the
|
|
|
signature; with `-expect-author` it fails, and writes nothing, unless the
|
|
|
capsule has a valid signature of that public key, the verdict F4: it compares
|
|
|
the key before step 18, when no file has been published.
|
|
|
|
|
|
`encrypt -note` puts a public note in clear in the capsule (spec §24.1), and
|
|
|
warns that it is public. `inspect` and `decrypt` show it as text of the creator
|
|
|
that nobody has checked; a note that breaks the rules of text is not shown,
|
|
|
and both say so (`public_note_unusable` in `inspect -json`).
|
|
|
`-words` and `-words-file` give a `time_and_key` capsule a key of words: at
|
|
|
least six different words of three or more letters, which open it with
|
|
|
`decrypt -words-file` instead of a `.dkk` (spec §38.1). `-new-words FILE`
|
|
|
draws them at random instead, 7 by default, from a built-in list (`-dic
|
|
|
en`, the list of the EFF, by default, or `-dic es`; `-word-count N`), writes
|
|
|
them to a new file and says their strength in bits: words a person chooses
|
|
|
are weaker. `-dice` and `-dice-file` take them from dice instead, for
|
|
|
whoever does not trust the random numbers of a computer: five dice for each
|
|
|
word give a number from 11111 to 66666, its position in the list, and
|
|
|
`datekeys wordlist` writes the list numbered for dice, to print it, with its
|
|
|
SHA-256; for `en` it is the file of the EFF, byte for byte. `encrypt` shows
|
|
|
the words of the dice: they open the capsule, not the numbers. A
|
|
|
list is used only if each word is of the alphabet of its language. The lists
|
|
|
and their license are in [`wordkey/lists`](wordkey/lists/README.md).
|
|
|
|
|
|
## Library
|
|
|
|
|
|
```go
|
|
|
reg, err := profile.Default() // pinned Quicknet profile, checked against its profile_hash
|
|
|
|
|
|
// EncryptFiles: no network, the round is resolved locally. It reads each
|
|
|
// file twice, and fails if a file changes between the two readings.
|
|
|
res, err := capsule.EncryptFiles(dst, []capsule.Source{{
|
|
|
Path: "photos/beach.jpg",
|
|
|
Size: info.Size(),
|
|
|
ModTime: info.ModTime(),
|
|
|
Open: func() (io.ReadCloser, error) { return os.Open(name) },
|
|
|
}}, capsule.EncryptOptions{
|
|
|
Profile: profile.Quicknet(),
|
|
|
UnlockAt: time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
|
Policy: capsule.TimeAndKey,
|
|
|
NewPortableKey: true, // res.PortableKey is the .dkk; encode it with accesskey.Encode
|
|
|
Comment: "For Ana",
|
|
|
Now: time.Now,
|
|
|
})
|
|
|
|
|
|
// Inspect: steps 1–8, no network, no secrets.
|
|
|
in, err := capsule.Inspect(f, capsule.InspectOptions{Registry: reg})
|
|
|
|
|
|
// Open: steps 1–18; the release is verified locally. A format 3 capsule
|
|
|
// gives its files to a Sink, which publishes them in Commit, at step 18.
|
|
|
opened, err := capsule.Open(ctx, nil, f, capsule.OpenOptions{
|
|
|
Registry: reg,
|
|
|
Source: drand.New(),
|
|
|
AccessKey: key, // or Identities: []age.Identity{...}
|
|
|
Sink: sink,
|
|
|
Now: time.Now,
|
|
|
})
|
|
|
if errors.Is(err, datekeys.ErrReleaseUnavailable) { /* not yet */ }
|
|
|
```
|
|
|
|
|
|
A `Sink` gets `Begin` with the head, `Create` for each file and `Commit` only
|
|
|
when step 17 has passed; after any failure it gets `Abort`, and nothing it
|
|
|
received may be presented as valid (spec §56). `opened.Head` holds the paths,
|
|
|
sizes, SHA-256 and modification times, the comment and the declared author,
|
|
|
and `opened.Verdicts.Lines()` the verdicts to show before them (spec §29.7).
|
|
|
`EncryptOptions.AuthorKey` or `CMSSigner` sign the capsule, and `Sealer` seals
|
|
|
it (spec §29.9 to §29.11); `OpenOptions.AuthorKeys` are the keys that the
|
|
|
person saved, which give F3, and `OpenOptions.Accept` sees the verdicts before
|
|
|
step 18 and can refuse to publish the files.
|
|
|
`Open` without a `Sink` stops right after step 2 with `capsule.ErrSinkRequired`,
|
|
|
a caller error with no code. Capsules of formats 1 and 2 still stream their
|
|
|
content to `dst`, never the padding. `opened.Format` is the format of the
|
|
|
capsule: format 1 hides neither the number of credentials nor the exact
|
|
|
length of the content, so show it (spec §70). Every protocol failure wraps
|
|
|
one of the 19 normative errors of spec §69, so `errors.Is` and
|
|
|
`datekeys.Code(err)` identify it.
|
|
|
|
|
|
## Security properties and limits
|
|
|
|
|
|
- **Time confidentiality** holds under drand's threshold assumption. The
|
|
|
Quicknet timelock is **not post-quantum**: ciphertexts kept for years are
|
|
|
exposed to harvest-now, decrypt-later (spec §7.7, §53).
|
|
|
- **No trust in servers**: the profile is pinned in the binary, the round is
|
|
|
computed locally, releases are BLS-verified locally, and a valid signature of
|
|
|
another round is rejected (spec §13, §17, §51).
|
|
|
- **Integrity**: framing, header, control and payload are authenticated
|
|
|
against whoever does not know the file keys: a third party's change makes
|
|
|
opening fail (fixtures and the mutation corpus). Once the round is
|
|
|
published anyone can compute the time file key, and anyone can seal a new
|
|
|
control for a public header; who can write each part, and from which step
|
|
|
it is bound, is the trust model of spec §27 and §55.1.
|
|
|
- **Metadata privacy**: until the date, the exact length of the content, the
|
|
|
number of credentials, and the names, sizes and number of the files stay
|
|
|
hidden, beyond what the padded size P bounds, and so does whether the
|
|
|
capsule is signed, unless its area was widened; the date, the access policy,
|
|
|
`capsule_id` and the header extensions, the public note among them, do not
|
|
|
(spec §55.2).
|
|
|
- **Safe extraction**: the paths of a head cannot leave the folder, collide
|
|
|
on Windows, macOS or Linux, hide characters or change the direction of the
|
|
|
text; the rules use fixed Unicode 18.0.0 tables, never those of the
|
|
|
platform (spec §29.5, §29.6).
|
|
|
- **Authorship only by a signature**: the declared author, the comment, the
|
|
|
public note and the modification times are text of the creator that proves
|
|
|
nothing (spec §24.1, §29.7, §36.1). A signature of `alg` 1 proves that
|
|
|
whoever had the secret key signed, not who has it; for a signature with
|
|
|
certificates and a seal, DateKeys checks the cryptography and the dates,
|
|
|
never who issued them or whether they were revoked (spec §29.9 to §29.11).
|
|
|
The verdicts never decide the opening (spec §29.3). `time_and_key` adds an
|
|
|
access barrier, not a signature.
|
|
|
- **Recovery years later** needs the historical release: from a drand relay
|
|
|
that still serves it or from any cache, re-verified locally (spec §50).
|
|
|
Spec v0.15 rests it on archives and cache services, of DateKeys or
|
|
|
others, that keep the releases of all rounds, with no promise of hosting
|
|
|
one, and its §79 says how to open a capsule without DateKeys software,
|
|
|
which `scripts/recovery_check.sh` checks.
|
|
|
|
|
|
See [SECURITY.md](SECURITY.md).
|
|
|
|
|
|
## Conformance and tests
|
|
|
|
|
|
```bash
|
|
|
go test ./... # unit, golden vectors, fixtures, mutation corpus
|
|
|
go test -race -cover ./...
|
|
|
go test -fuzz=FuzzInspect ./capsule # one fuzz target at a time
|
|
|
go test -tags interop ./capsule # official age and tle CLIs open our files
|
|
|
go test -tags integration ./capsule ./provider/drand # live Quicknet
|
|
|
```
|
|
|
|
|
|
- `testdata/vectors`: profile hash, date→round and `dk1_` vectors (spec §65, §66),
|
|
|
CBOR profile and schema vectors, padding vectors (spec §29.1), the paths, the
|
|
|
keys of R7, the heads and the security areas of format 3 (spec §67), the
|
|
|
strict Ed25519 of `alg` 1, the signatures with certificates and the seals,
|
|
|
the public note and the extension `datekeys.capsule`, the exported mutation
|
|
|
corpus and a differential corpus of the pre-unlock checks; formats in
|
|
|
[`testdata/README.md`](testdata/README.md).
|
|
|
- `testdata/fixtures`: official `.dkc`/`.dkk` fixtures over published rounds,
|
|
|
with the BLS signature embedded and every intermediate value (spec §67, §68);
|
|
|
they decrypt offline. Fourteen are of format 3, five of them with the area
|
|
|
of 32 KiB of v0.11: unsigned, signed with `alg` 1, sealed, signed with
|
|
|
certificates, and with a public note. The seven of format 2, from v0.9, and
|
|
|
the five of format 1, from v0.8.2, are kept for compatibility. Each `.dkc`
|
|
|
has its frozen `datekeys inspect -json` output.
|
|
|
- `internal/testkit.Mutations`: the mutations of spec §64, the 33 of its first
|
|
|
two lists in each format, the 23 of the list of format 2, the 48 of the list
|
|
|
of format 3 and 8 of the list of v0.11, and 40 more, each with its exact
|
|
|
error and step, or its verdicts when it opens, and a check that pre-unlock
|
|
|
failures never cause a release request; exported to
|
|
|
`testdata/vectors/mutations.json`.
|
|
|
- [`docs/traceability.md`](docs/traceability.md): spec section → code → test.
|
|
|
- [`spec/datekeys.cddl`](spec/datekeys.cddl): CBOR schemas.
|
|
|
|
|
|
## License
|
|
|
|
|
|
Code: Apache-2.0 ([LICENSE](LICENSE)). Specification: CC-BY-ND-4.0
|
|
|
([spec/README.md](spec/README.md)). `codec/bech32` is copied from age under its
|
|
|
own license. "DateKeys" is a reserved name: see [TRADEMARKS.md](TRADEMARKS.md).
|