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

246 lines
13 KiB

# 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 | §54 | [`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. 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, concrete extensions, the author signature and the time seal, which
the specification reserves for later versions, and the TypeScript client
(plan §2).
## 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` once it is closed | 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)
Implement capsule format 2 of spec v0.9 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>
1 week ago
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.
Implement capsule format 2 of spec v0.9 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>
1 week ago
## 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 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.
## 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).
Spec v0.8.2: second-round corrections from the formal review The second round of the formal review confirmed the nine corrections of c57ed48 and asked for these, recorded in §76 as corrections 4 to 6 and an editorial note: - §72: an encoder MUST NOT write a registered extension in an object or array it is not registered for; §54: a reader MUST NOT interpret the data of a noncritical one it ignores for that reason. capsule.Encrypt and accesskey.Encode take no Registry, so the application applies the rule; their documentation and extension.Placement say so. - §17 and §51 give the step-10 codes only for a directly supplied release, as step 10 does; a network source discards a failing one at step 9. - Step 9 reports ERR_RELEASE_UNAVAILABLE and no other code, whatever the failure of the source. provider/drand.Client keeps each relay's failure as text only (errors.Join made a relay's ERR_ROUND_MISMATCH match with errors.Is), and capsule.Open keeps only the text of a source error that carries another code (a caller's source failing with ERR_RELEASE_INVALID gave that code at step 9). A context that ended stays detectable: Fetch now has a single failure path, so the canceled and deadline cases are deterministic. - TestExtensionPlacement covers the noncritical array of a .dkk: with the object-blind extension.CheckNoncritical at step 9.a it fails. - Editorial: §28.1 "analizan solo la cabecera age", one arrow at step 9, two §76 introductions; §73 lines for release sources and placement. - testdata/README.md says the corpus registers its extensions in both arrays of every object; traceability, CHANGELOG and both READMEs (integrity holds against whoever lacks the file keys, §27, §55.1) follow. Spec dated 28 September 2026; new SHA-256 in spec/README.md. No fixture or vector changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
- **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.
Implement capsule format 2 of spec v0.9 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>
1 week ago
- `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).

Powered by TurnKey Linux.