Initial implementation of the DateKeys Protocol v0.8.1
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>
2 weeks ago
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
Version constants: the specification and the module
- 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>
1 week ago
|
|
|
"runtime"
|
Initial implementation of the DateKeys Protocol v0.8.1
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>
2 weeks ago
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"filippo.io/age"
|
|
|
|
|
|
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
|
|
|
"g.activething.com/go/DateKeys/internal/inspectview"
|
|
|
|
|
"g.activething.com/go/DateKeys/internal/testkit"
|
|
|
|
|
"g.activething.com/go/DateKeys/profile"
|
Initial implementation of the DateKeys Protocol v0.8.1
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>
2 weeks ago
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const fixtures = "../../testdata/fixtures"
|
|
|
|
|
|
|
|
|
|
// relay serves the known Quicknet releases like a drand HTTP relay.
|
|
|
|
|
func relay(t *testing.T) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
p := profile.Quicknet()
|
|
|
|
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
for _, round := range testkit.Rounds {
|
|
|
|
|
if r.URL.Path == fmt.Sprintf("/v2/chains/%s/rounds/%d", p.ChainHashHex(), round) {
|
|
|
|
|
fmt.Fprintf(w, `{"round":%d,"signature":"%s"}`, round, hex.EncodeToString(testkit.Release(round).Signature))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
}))
|
|
|
|
|
t.Cleanup(s.Close)
|
|
|
|
|
return s.URL
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cli(t *testing.T, now time.Time, args ...string) (string, string, error) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
var out, errOut bytes.Buffer
|
|
|
|
|
err := run(args, &out, &errOut, func() time.Time { return now })
|
|
|
|
|
return out.String(), errOut.String(), err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var later = time.Date(2026, 9, 25, 12, 0, 0, 0, time.UTC)
|
|
|
|
|
|
|
|
|
|
func TestOutputNotPublishedOnFailureOrOverwrite(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
out := filepath.Join(dir, "output")
|
|
|
|
|
err := writeAtomic(out, func(w io.Writer) error {
|
|
|
|
|
_, _ = w.Write([]byte("partial plaintext"))
|
|
|
|
|
return errors.New("invalid authentication tag")
|
|
|
|
|
})
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected failure")
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Stat(out); !errors.Is(err, os.ErrNotExist) {
|
|
|
|
|
t.Fatal("published partial output")
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(out, []byte("keep me"), 0o600); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := writeAtomic(out, func(io.Writer) error { t.Fatal("should not run"); return nil }); err == nil {
|
|
|
|
|
t.Fatal("overwrote output")
|
|
|
|
|
}
|
|
|
|
|
if b, _ := os.ReadFile(out); string(b) != "keep me" {
|
|
|
|
|
t.Fatal("output changed")
|
|
|
|
|
}
|
|
|
|
|
if files, _ := filepath.Glob(filepath.Join(dir, ".datekeys-*")); len(files) != 0 {
|
|
|
|
|
t.Fatal("temporary file left behind")
|
|
|
|
|
}
|
|
|
|
|
if err := copyExclusive(out, out); err == nil {
|
|
|
|
|
t.Fatal("exclusive copy replaced a file")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDecryptFixtures(t *testing.T) {
|
|
|
|
|
url := relay(t)
|
|
|
|
|
for _, tc := range []struct{ name, dkk string }{
|
|
|
|
|
{"time_only", ""},
|
|
|
|
|
{"time_only_extensions", ""},
|
|
|
|
|
{"empty_payload", ""},
|
|
|
|
|
{"time_and_key_portable", "time_and_key_portable.dkk"},
|
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
|
|
|
{"format2_time_only", ""},
|
|
|
|
|
{"format2_time_only_bloque256", ""},
|
|
|
|
|
{"format2_empty_payload", ""},
|
|
|
|
|
{"format2_time_only_extensions", ""},
|
|
|
|
|
{"format2_time_and_key_portable", "format2_time_and_key_portable.dkk"},
|
|
|
|
|
{"format2_time_and_key_recipients", "format2_time_and_key_recipients.dkk"},
|
Initial implementation of the DateKeys Protocol v0.8.1
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>
2 weeks ago
|
|
|
} {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
out := filepath.Join(t.TempDir(), "plain")
|
|
|
|
|
args := []string{"decrypt", "-in", filepath.Join(fixtures, tc.name+".dkc"), "-out", out, "-relay", url}
|
|
|
|
|
if tc.dkk != "" {
|
|
|
|
|
args = append(args, "-dkk", filepath.Join(fixtures, tc.dkk))
|
|
|
|
|
}
|
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
|
|
|
_, stderr, err := cli(t, later, args...)
|
|
|
|
|
if err != nil {
|
Initial implementation of the DateKeys Protocol v0.8.1
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>
2 weeks ago
|
|
|
t.Fatalf("%v\n%s", err, stderr)
|
|
|
|
|
}
|
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
|
|
|
// Spec §70: the format is reported, with what format 1 reveals.
|
|
|
|
|
format1 := !strings.HasPrefix(tc.name, "format2_")
|
|
|
|
|
if strings.Contains(stderr, "format 1 does not hide") != format1 {
|
|
|
|
|
t.Fatalf("report:\n%s", stderr)
|
|
|
|
|
}
|
Initial implementation of the DateKeys Protocol v0.8.1
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>
2 weeks ago
|
|
|
got, _ := os.ReadFile(out)
|
|
|
|
|
want, _ := os.ReadFile(filepath.Join(fixtures, tc.name+".plaintext"))
|
|
|
|
|
if !bytes.Equal(got, want) {
|
|
|
|
|
t.Fatal("plaintext differs")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDecryptWithIdentityFile(t *testing.T) {
|
|
|
|
|
var f testkit.DKCFixture
|
|
|
|
|
if err := testkit.ReadJSON(filepath.Join(fixtures, "time_and_key_recipients.json"), &f); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
key := filepath.Join(dir, "key.txt")
|
|
|
|
|
os.WriteFile(key, []byte("# test identity\n"+f.Identities[1]+"\n"), 0o600)
|
|
|
|
|
out := filepath.Join(dir, "plain")
|
|
|
|
|
if _, stderr, err := cli(t, later, "decrypt", "-in", filepath.Join(fixtures, f.File), "-out", out, "-identity", key, "-relay", relay(t)); err != nil {
|
|
|
|
|
t.Fatalf("%v\n%s", err, stderr)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDecryptFailuresLeaveNothing(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
b, _ := os.ReadFile(filepath.Join(fixtures, "time_only.dkc"))
|
|
|
|
|
bad := filepath.Join(dir, "bad.dkc")
|
|
|
|
|
b[len(b)-1] ^= 1
|
|
|
|
|
os.WriteFile(bad, b, 0o600)
|
|
|
|
|
out := filepath.Join(dir, "plain")
|
|
|
|
|
_, _, err := cli(t, later, "decrypt", "-in", bad, "-out", out, "-relay", relay(t))
|
|
|
|
|
if !errors.Is(err, datekeys.ErrIntegrity) {
|
|
|
|
|
t.Fatalf("got %v", err)
|
|
|
|
|
}
|
|
|
|
|
if entries, _ := os.ReadDir(dir); len(entries) != 1 {
|
|
|
|
|
t.Fatalf("left files behind: %v", entries)
|
|
|
|
|
}
|
|
|
|
|
// time_and_key without credentials fails before contacting any relay.
|
|
|
|
|
_, _, err = cli(t, later, "decrypt", "-in", filepath.Join(fixtures, "time_and_key_portable.dkc"), "-out", out, "-relay", "http://127.0.0.1:1")
|
|
|
|
|
if !errors.Is(err, datekeys.ErrAccessRequired) {
|
|
|
|
|
t.Fatalf("got %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Spec v0.8.2 refinements: error precedence, trust model, strict order
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>
2 weeks ago
|
|
|
// Spec §63 step 9.a, §69.1: the CLI hands the .dkk to capsule.Open still
|
|
|
|
|
// encoded, so its 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.
|
|
|
|
|
func TestDecryptAccessKeyOrder(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
notDKK := filepath.Join(dir, "not.dkk")
|
|
|
|
|
os.WriteFile(notDKK, []byte("not a .dkk"), 0o600)
|
|
|
|
|
b, _ := os.ReadFile(filepath.Join(fixtures, "time_and_key_portable.dkc"))
|
|
|
|
|
b[5] = 1
|
|
|
|
|
flags := filepath.Join(dir, "flags.dkc")
|
|
|
|
|
os.WriteFile(flags, b, 0o600)
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
|
name, in string
|
|
|
|
|
want error
|
|
|
|
|
}{
|
|
|
|
|
{"time_and_key and bytes that are not a .dkk", filepath.Join(fixtures, "time_and_key_portable.dkc"), datekeys.ErrInvalidMagic},
|
|
|
|
|
{"FLAGS 1 and bytes that are not a .dkk", flags, datekeys.ErrInvalidFlags},
|
|
|
|
|
} {
|
|
|
|
|
_, _, err := cli(t, later, "decrypt", "-in", tc.in, "-out", filepath.Join(dir, "plain"), "-dkk", notDKK, "-relay", "http://127.0.0.1:1")
|
|
|
|
|
if !errors.Is(err, tc.want) {
|
|
|
|
|
t.Errorf("%s: got %v, want %s", tc.name, err, datekeys.Code(tc.want))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out := filepath.Join(dir, "plain")
|
|
|
|
|
if _, stderr, err := cli(t, later, "decrypt", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", out, "-dkk", notDKK, "-relay", relay(t)); err != nil {
|
|
|
|
|
t.Fatalf("time_only and bytes that are not a .dkk: %v\n%s", err, stderr)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Initial implementation of the DateKeys Protocol v0.8.1
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>
2 weeks ago
|
|
|
func TestEncryptDecryptRoundTrip(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
in := filepath.Join(dir, "secret.txt")
|
|
|
|
|
os.WriteFile(in, []byte("round trip through the CLI"), 0o600)
|
|
|
|
|
p := profile.Quicknet()
|
|
|
|
|
unlock := time.Unix(p.GenesisTime+999*3, 0).UTC() // round 1000
|
|
|
|
|
genesis := time.Unix(p.GenesisTime, 0)
|
|
|
|
|
x, _ := age.GenerateX25519Identity()
|
|
|
|
|
key := filepath.Join(dir, "x.txt")
|
|
|
|
|
os.WriteFile(key, []byte(x.String()+"\n"), 0o600)
|
|
|
|
|
|
|
|
|
|
dkc, dkk := filepath.Join(dir, "s.dkc"), filepath.Join(dir, "s.dkk")
|
|
|
|
|
_, stderr, err := cli(t, genesis, "encrypt", "-at", unlock.Format(time.RFC3339), "-in", in, "-out", dkc,
|
|
|
|
|
"-policy", "time_and_key", "-recipient", x.Recipient().String(), "-dkk", dkk)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("%v\n%s", err, stderr)
|
|
|
|
|
}
|
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
|
|
|
if !strings.Contains(stderr, "round 1000") || !strings.Contains(stderr, "format 2: 26 bytes of content, padded to 256 (reforzado)") ||
|
|
|
|
|
strings.Contains(stderr, "not post-quantum") {
|
Initial implementation of the DateKeys Protocol v0.8.1
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>
2 weeks ago
|
|
|
t.Fatalf("unexpected report:\n%s", stderr)
|
|
|
|
|
}
|
|
|
|
|
if _, _, err := cli(t, genesis, "encrypt", "-at", unlock.Format(time.RFC3339), "-in", in, "-out", dkc); err == nil {
|
|
|
|
|
t.Fatal("overwrote an existing capsule")
|
|
|
|
|
}
|
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
|
|
|
// Spec §29.1: the padding rule is one of the two, never none.
|
|
|
|
|
small := filepath.Join(dir, "small.dkc")
|
|
|
|
|
if _, stderr, err := cli(t, genesis, "encrypt", "-at", unlock.Format(time.RFC3339), "-in", in, "-out", small, "-padding", "bloque256"); err != nil ||
|
|
|
|
|
!strings.Contains(stderr, "(bloque256)") {
|
|
|
|
|
t.Fatalf("-padding bloque256: %v\n%s", err, stderr)
|
|
|
|
|
}
|
|
|
|
|
if _, _, err := cli(t, genesis, "encrypt", "-at", unlock.Format(time.RFC3339), "-in", in, "-out", filepath.Join(dir, "none.dkc"), "-padding", "none"); err == nil {
|
|
|
|
|
t.Fatal("encrypted without a padding rule")
|
|
|
|
|
}
|
Initial implementation of the DateKeys Protocol v0.8.1
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>
2 weeks ago
|
|
|
|
|
|
|
|
stdout, _, err := cli(t, later, "inspect", "-in", dkc, "-json")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
var v inspectview.View
|
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
|
|
|
if err := json.Unmarshal([]byte(stdout), &v); err != nil || !v.Valid || v.Round != 1000 || v.AccessPolicy != "time_and_key" || v.Format != 2 {
|
Initial implementation of the DateKeys Protocol v0.8.1
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>
2 weeks ago
|
|
|
t.Fatalf("inspect: %+v %v", v, err)
|
|
|
|
|
}
|
|
|
|
|
for i, extra := range [][]string{{"-dkk", dkk}, {"-identity", key}} {
|
|
|
|
|
out := filepath.Join(dir, fmt.Sprintf("out%d", i))
|
|
|
|
|
args := append([]string{"decrypt", "-in", dkc, "-out", out, "-relay", relay(t)}, extra...)
|
|
|
|
|
if _, stderr, err := cli(t, later, args...); err != nil {
|
|
|
|
|
t.Fatalf("%v\n%s", err, stderr)
|
|
|
|
|
}
|
|
|
|
|
if b, _ := os.ReadFile(out); string(b) != "round trip through the CLI" {
|
|
|
|
|
t.Fatal("plaintext differs")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestInspectReportsFailures(t *testing.T) {
|
|
|
|
|
b, _ := os.ReadFile(filepath.Join(fixtures, "time_only.dkc"))
|
|
|
|
|
b = bytes.Replace(b, []byte("-> tlock 1000 "), []byte("-> tlock 1001 "), 1)
|
|
|
|
|
path := filepath.Join(t.TempDir(), "bad.dkc")
|
|
|
|
|
os.WriteFile(path, b, 0o600)
|
|
|
|
|
stdout, _, err := cli(t, later, "inspect", "-in", path)
|
|
|
|
|
if !errors.Is(err, datekeys.ErrRoundMismatch) || !strings.Contains(stdout, "[FAIL] step 8") {
|
|
|
|
|
t.Fatalf("%v\n%s", err, stdout)
|
|
|
|
|
}
|
|
|
|
|
stdout, _, err = cli(t, later, "inspect", "-in", filepath.Join(fixtures, "time_only.dkc"))
|
|
|
|
|
if err != nil || !strings.Contains(stdout, "valid before unlock") {
|
|
|
|
|
t.Fatalf("%v\n%s", err, stdout)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The frozen inspect outputs (testdata/fixtures/<name>.inspect.json) are
|
|
|
|
|
// exactly what "datekeys inspect -json -in <name>.dkc" prints in the fixture
|
|
|
|
|
// directory.
|
|
|
|
|
func TestInspectJSONGoldens(t *testing.T) {
|
|
|
|
|
names, err := filepath.Glob(filepath.Join(fixtures, "*.inspect.json"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
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
|
|
|
dkcs, err := filepath.Glob(filepath.Join(fixtures, "*.dkc"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(names) != len(dkcs) || len(dkcs) != 12 {
|
|
|
|
|
t.Fatalf("%d frozen inspect outputs, want one per official .dkc (%d, 12)", len(names), len(dkcs))
|
|
|
|
|
}
|
|
|
|
|
t.Chdir(fixtures)
|
|
|
|
|
for _, path := range names {
|
|
|
|
|
name := strings.TrimSuffix(filepath.Base(path), ".inspect.json")
|
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
|
want, err := os.ReadFile(name + ".inspect.json")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
stdout, _, err := cli(t, later, "inspect", "-json", "-in", name+".dkc")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if stdout != string(want) {
|
|
|
|
|
t.Fatalf("output differs from %s.inspect.json:\n%s", name, stdout)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Initial implementation of the DateKeys Protocol v0.8.1
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>
2 weeks ago
|
|
|
func TestResolveAndProfile(t *testing.T) {
|
|
|
|
|
stdout, _, err := cli(t, later, "datekey", "resolve", "-at", "2030-01-01T00:00:00Z")
|
|
|
|
|
if err != nil || !strings.Contains(stdout, `"round":66884212`) || !strings.Contains(stdout, `"unlock_at":"2030-01-01T00:00:00Z"`) {
|
|
|
|
|
t.Fatalf("%v %s", err, stdout)
|
|
|
|
|
}
|
|
|
|
|
if _, _, err := cli(t, later, "datekey", "resolve", "-at", "2030-01-01 00:00"); err == nil {
|
|
|
|
|
t.Fatal("accepted a time without zone")
|
|
|
|
|
}
|
|
|
|
|
stdout, _, err = cli(t, later, "profile", "hash")
|
|
|
|
|
if err != nil || !strings.Contains(stdout, profile.QuicknetProfileHash) || !strings.Contains(stdout, `"pinned":true`) {
|
|
|
|
|
t.Fatalf("%v %s", err, stdout)
|
|
|
|
|
}
|
|
|
|
|
b, _ := profile.Quicknet().CanonicalCBOR()
|
|
|
|
|
path := filepath.Join(t.TempDir(), "q.cbor")
|
|
|
|
|
os.WriteFile(path, b, 0o600)
|
|
|
|
|
if stdout, _, err = cli(t, later, "profile", "hash", "-in", path); err != nil || !strings.Contains(stdout, profile.QuicknetProfileHash) {
|
|
|
|
|
t.Fatalf("%v %s", err, stdout)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUsage(t *testing.T) {
|
Version constants: the specification and the module
- 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>
1 week ago
|
|
|
for _, args := range [][]string{nil, {"nope"}, {"datekey"}, {"profile", "x"}, {"encrypt", "-bogus"}, {"inspect", "extra"}, {"version", "extra"}} {
|
Initial implementation of the DateKeys Protocol v0.8.1
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>
2 weeks ago
|
|
|
if _, _, err := cli(t, later, args...); err == nil {
|
|
|
|
|
t.Errorf("%v accepted", args)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Version constants: the specification and the module
- 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>
1 week ago
|
|
|
// The version command names the module version, the specification and the
|
|
|
|
|
// toolchain; in a test the module version is unknown.
|
|
|
|
|
func TestVersion(t *testing.T) {
|
|
|
|
|
for _, arg := range []string{"version", "-version", "--version"} {
|
|
|
|
|
out, _, err := cli(t, later, arg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
want := "datekeys " + datekeys.Version() + "\nspecification " + datekeys.SpecVersion + "\n" + runtime.Version() + " " + runtime.GOOS + "/" + runtime.GOARCH + "\n"
|
|
|
|
|
if out != want {
|
|
|
|
|
t.Errorf("%s: %q, want %q", arg, out, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(usage, "datekeys version") {
|
|
|
|
|
t.Error("the usage does not list the version command")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Initial implementation of the DateKeys Protocol v0.8.1
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>
2 weeks ago
|
|
|
// Spec §53: long horizons get the harvest-now, decrypt-later warning.
|
|
|
|
|
func TestLongHorizonWarning(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
in := filepath.Join(dir, "in")
|
|
|
|
|
os.WriteFile(in, []byte("x"), 0o600)
|
|
|
|
|
for i, tc := range []struct {
|
|
|
|
|
after time.Duration
|
|
|
|
|
warn bool
|
|
|
|
|
}{{time.Hour, false}, {2 * 365 * 24 * time.Hour, true}} {
|
|
|
|
|
out := filepath.Join(dir, fmt.Sprintf("%d.dkc", i))
|
|
|
|
|
_, stderr, err := cli(t, later, "encrypt", "-at", later.Add(tc.after).Format(time.RFC3339), "-in", in, "-out", out)
|
|
|
|
|
if err != nil || strings.Contains(stderr, "not post-quantum") != tc.warn {
|
|
|
|
|
t.Fatalf("%s: %v: %s", tc.after, err, stderr)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|