|
|
# datekeys-go
|
|
|
|
|
|
Reference implementation in Go of the **DateKeys Protocol Specification
|
|
|
v0.10** ([`spec/`](spec/DateKeys_Protocol_Specification_v0.10.md)).
|
|
|
[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 | §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) |
|
|
|
| DateKeys Access Key `.dkk` | §40–§44 | [`accesskey`](accesskey) |
|
|
|
| Extensions, the public note | §24.1, §54 | [`extension`](extension) |
|
|
|
| Author keys `dkauthor1…`, `alg` 1 | §29.9, §29.12 | [`authorkey`](authorkey), [`internal/ed25519strict`](internal/ed25519strict) |
|
|
|
| Signature with certificates (`alg` 2, CMS), time seal (RFC 3161) | §29.10, §29.11 | [`internal/cms`](internal/cms), [`internal/der`](internal/der), [`capsule`](capsule) |
|
|
|
| The extension `datekeys.capsule`: locator and envelope | §44.1 | [`locator`](locator) |
|
|
|
| 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. This module adds
|
|
|
framing, CBOR, 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, and the TypeScript client (plan §2). 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.10`, and the tag `spec-v0.10` | 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.10: it writes capsule format 3 and reads formats 1, 2 and 3;
|
|
|
- the pinned Quicknet profile, and any profile on the three drand schemes that tlock supports;
|
|
|
- encryption, inspection and opening, 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 (512) signature and seal: empty in v0.10
|
|
|
|| 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). 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 -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 profile hash
|
|
|
datekeys version
|
|
|
```
|
|
|
|
|
|
`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. 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). Formats 1 and 2 still give one file.
|
|
|
Outputs are never overwritten.
|
|
|
|
|
|
`author keygen` makes an Ed25519 author key (spec v0.11, §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. `encrypt -sign` signs the
|
|
|
capsule with it (`alg` 1) and checks the signature before writing anything;
|
|
|
`decrypt` shows the signature, and with `-expect-author` it fails, after
|
|
|
writing the files, unless the capsule is signed with that public key. The
|
|
|
passphrase comes from a file, or from the standard input with `-`, never from
|
|
|
the command line or the environment. `encrypt -note` puts a public note in
|
|
|
clear in the capsule (§24.1): `inspect` and `decrypt` show it as text of the
|
|
|
creator that nobody has checked.
|
|
|
|
|
|
## 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).
|
|
|
`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 18 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; the date, the access policy,
|
|
|
`capsule_id` and the header extensions 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).
|
|
|
- **No authorship**: the declared author and the comment are text of the
|
|
|
creator that proves nothing, and this version checks no signature and no
|
|
|
seal: its verdicts only say so, and never decide the opening (spec §29.3,
|
|
|
§29.7, §36.1). `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).
|
|
|
|
|
|
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
|
|
|
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. Nine are of format 3; 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 and the 47 of the
|
|
|
list of format 3, 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-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).
|