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 capsule_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
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
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
|
|
|
"g.activething.com/go/DateKeys/accesskey"
|
|
|
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
|
|
|
"g.activething.com/go/DateKeys/datekey"
|
|
|
|
|
"g.activething.com/go/DateKeys/extension"
|
|
|
|
|
"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
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func seedFixtures(f *testing.F, part func(testkit.Parts) []byte) {
|
|
|
|
|
for _, name := range fixtureNames {
|
|
|
|
|
b, err := os.ReadFile(filepath.Join(fixtureDir, name+".dkc"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
f.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
p, err := testkit.Split(b)
|
|
|
|
|
if err != nil {
|
|
|
|
|
f.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
f.Add(part(p))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// whole keeps the first 512 bytes of the payload: the pre-unlock checks read
|
|
|
|
|
// only its age header, and small inputs keep the fuzzer fast.
|
|
|
|
|
func whole(p testkit.Parts) []byte {
|
|
|
|
|
payload := p.Payload
|
|
|
|
|
if len(payload) > 512 {
|
|
|
|
|
payload = payload[:512]
|
|
|
|
|
}
|
|
|
|
|
return testkit.Join(p.Prelude, p.Header, p.Sealed, payload)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func FuzzParsePrelude(f *testing.F) {
|
|
|
|
|
seedFixtures(f, func(p testkit.Parts) []byte { return p.Prelude })
|
|
|
|
|
f.Fuzz(func(t *testing.T, b []byte) {
|
|
|
|
|
p, err := capsule.ParsePrelude(b)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if datekeys.Code(err) == "" {
|
|
|
|
|
t.Fatalf("error without a normative code: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
got := p.Bytes()
|
|
|
|
|
if !bytes.Equal(got[:], b[:capsule.PreludeSize]) {
|
|
|
|
|
t.Fatal("accepted a prelude that does not re-encode to its input")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func FuzzDecodeHeader(f *testing.F) {
|
|
|
|
|
seedFixtures(f, func(p testkit.Parts) []byte { return p.Header })
|
|
|
|
|
// access_policy in a multi-byte head whose low byte is a V1 policy: a
|
|
|
|
|
// narrowing before the check accepted them (spec §25).
|
|
|
|
|
dk := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000}.Compact()
|
|
|
|
|
for _, p := range []uint64{256, 257, 1 << 32} {
|
|
|
|
|
h, err := testkit.RawHeader([capsule.CapsuleIDSize]byte{1}, dk, p)
|
|
|
|
|
if err != nil {
|
|
|
|
|
f.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
f.Add(h)
|
|
|
|
|
}
|
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
|
|
|
f.Fuzz(func(t *testing.T, b []byte) {
|
|
|
|
|
h, err := capsule.DecodeHeader(b)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if datekeys.Code(err) == "" {
|
|
|
|
|
t.Fatalf("error without a normative code: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
re, err := capsule.EncodeHeader(h)
|
|
|
|
|
if err != nil || !bytes.Equal(re, b) {
|
|
|
|
|
t.Fatal("accepted a PUBLIC_HEADER that does not re-encode to its input")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func FuzzDecodeControl(f *testing.F) {
|
|
|
|
|
for _, name := range fixtureNames {
|
|
|
|
|
fx := loadFixture(f, name)
|
|
|
|
|
b, _ := hexDecode(fx.ControlCBOR)
|
|
|
|
|
f.Add(b)
|
|
|
|
|
}
|
|
|
|
|
f.Fuzz(func(t *testing.T, b []byte) {
|
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
|
|
|
// The same bytes as the control of a capsule of each format: at most
|
Format 3, step 6b: the nine fixtures of format 3
The fixtures of spec 67 for format 3, each with its record, its BODY,
its inspect output and, for time_and_key, its .dkk:
- format3_single, format3_tree (five files in three folders, one over
two STREAM chunks, one without mtime, a comment and a declared
author), format3_comment_only (no files; a TAB in the comment),
format3_bloque256 and format3_time_and_key_portable, written with
EncryptFiles;
- format3_area_1024, format3_security_v2 (verdict X),
format3_signature_unsupported (an author-signature of alg 1 with a
random key of 32 bytes and a random signature of 64: F1) and
format3_seal_unsupported (that and a seal of seal_type 1: F1 and S1),
which only a generator of test vectors writes (62.1 rule 13), built
with testkit.Build.
The record of a format 3 fixture adds the area, SECURITY_CBOR,
HEAD_CBOR, the salt, the comment, the declared author, the head
extensions, the offset of CONTENT in BODY, each file with its layout,
SHA-256 and mtime, and the verdicts with their lines; its plaintext
file is BODY. The generator writes, then recovers every value by
opening layer by layer for the three formats alike, and refreshes the
records of format 3 through a Sink.
Tests: the conformance test checks BODY, the head, security and every
file, and opens through a MemorySink; the .dkk tests take the .dkk of
formats 2 and 3 too (spec 68); the CLI decrypts five of the fixtures
into folders; the control fuzz target decodes with the three schema
versions. The differential corpus gains two bases, format3_single and
format3_time_and_key_portable, one per policy: 5110 cases, the earlier
ones unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// one of the three schema versions accepts them.
|
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
|
|
|
accepted := 0
|
Format 3, step 6b: the nine fixtures of format 3
The fixtures of spec 67 for format 3, each with its record, its BODY,
its inspect output and, for time_and_key, its .dkk:
- format3_single, format3_tree (five files in three folders, one over
two STREAM chunks, one without mtime, a comment and a declared
author), format3_comment_only (no files; a TAB in the comment),
format3_bloque256 and format3_time_and_key_portable, written with
EncryptFiles;
- format3_area_1024, format3_security_v2 (verdict X),
format3_signature_unsupported (an author-signature of alg 1 with a
random key of 32 bytes and a random signature of 64: F1) and
format3_seal_unsupported (that and a seal of seal_type 1: F1 and S1),
which only a generator of test vectors writes (62.1 rule 13), built
with testkit.Build.
The record of a format 3 fixture adds the area, SECURITY_CBOR,
HEAD_CBOR, the salt, the comment, the declared author, the head
extensions, the offset of CONTENT in BODY, each file with its layout,
SHA-256 and mtime, and the verdicts with their lines; its plaintext
file is BODY. The generator writes, then recovers every value by
opening layer by layer for the three formats alike, and refreshes the
records of format 3 through a Sink.
Tests: the conformance test checks BODY, the head, security and every
file, and opens through a MemorySink; the .dkk tests take the .dkk of
formats 2 and 3 too (spec 68); the CLI decrypts five of the fixtures
into folders; the control fuzz target decodes with the three schema
versions. The differential corpus gains two bases, format3_single and
format3_time_and_key_portable, one per policy: 5110 cases, the earlier
ones unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
for _, format := range []capsule.Format{capsule.Format1, capsule.Format2, capsule.Format3} {
|
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
|
|
|
c, err := capsule.DecodeControl(b, format)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if datekeys.Code(err) == "" {
|
|
|
|
|
t.Fatalf("format %d: error without a normative code: %v", format, err)
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
accepted++
|
|
|
|
|
re, err := capsule.EncodeControl(c, format)
|
|
|
|
|
if err != nil || !bytes.Equal(re, b) {
|
|
|
|
|
t.Fatalf("format %d: accepted a CONTROL_CBOR that does not re-encode to its input", format)
|
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
|
|
|
}
|
|
|
|
|
}
|
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 accepted > 1 {
|
Format 3, step 6b: the nine fixtures of format 3
The fixtures of spec 67 for format 3, each with its record, its BODY,
its inspect output and, for time_and_key, its .dkk:
- format3_single, format3_tree (five files in three folders, one over
two STREAM chunks, one without mtime, a comment and a declared
author), format3_comment_only (no files; a TAB in the comment),
format3_bloque256 and format3_time_and_key_portable, written with
EncryptFiles;
- format3_area_1024, format3_security_v2 (verdict X),
format3_signature_unsupported (an author-signature of alg 1 with a
random key of 32 bytes and a random signature of 64: F1) and
format3_seal_unsupported (that and a seal of seal_type 1: F1 and S1),
which only a generator of test vectors writes (62.1 rule 13), built
with testkit.Build.
The record of a format 3 fixture adds the area, SECURITY_CBOR,
HEAD_CBOR, the salt, the comment, the declared author, the head
extensions, the offset of CONTENT in BODY, each file with its layout,
SHA-256 and mtime, and the verdicts with their lines; its plaintext
file is BODY. The generator writes, then recovers every value by
opening layer by layer for the three formats alike, and refreshes the
records of format 3 through a Sink.
Tests: the conformance test checks BODY, the head, security and every
file, and opens through a MemorySink; the .dkk tests take the .dkk of
formats 2 and 3 too (spec 68); the CLI decrypts five of the fixtures
into folders; the control fuzz target decodes with the three schema
versions. The differential corpus gains two bases, format3_single and
format3_time_and_key_portable, one per policy: 5110 cases, the earlier
ones unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
t.Fatal("two formats accept the same CONTROL_CBOR")
|
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
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
Format 3, step 8: fuzzing of format 3 and the SHA-256 of spec v0.10
- Three fuzz targets, in scripts/fuzz.sh too: FuzzDecodeHead (a head
that is accepted re-encodes to its input, and a rejection carries one
normative code), FuzzEvaluateSecurity (verdicts of this version, X for
both or for neither) and FuzzCheckPath (the rules of one entry and the
decoder of the head agree on every path). About a million runs each,
clean; scripts/check.sh 60s is clean.
- Spec v0.10, section 67: the fixtures of format 3 exist, so "Serán ...
(por implementar)" reads "Son ...", as for those of format 2. No rule
changes.
- spec/README.md: v0.10 approved by its author on 30 September 2026 and
implemented on this branch, with the SHA-256 of its text; the tag
spec-v0.10 waits for the author.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// FuzzDecodeHead: a head that DecodeHead accepts re-encodes to its input,
|
|
|
|
|
// and each rejection carries exactly one normative code (spec §29.4, §69.1).
|
|
|
|
|
func FuzzDecodeHead(f *testing.F) {
|
|
|
|
|
for _, name := range fixtureNames {
|
|
|
|
|
if fx := loadFixture(f, name); fx.Head != "" {
|
|
|
|
|
b, _ := hexDecode(fx.Head)
|
|
|
|
|
f.Add(b)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
var heads testkit.HeadSchemaFile
|
|
|
|
|
if err := testkit.ReadJSON("../testdata/vectors/head_schema.json", &heads); err != nil {
|
|
|
|
|
f.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for _, v := range heads.Heads {
|
|
|
|
|
b, _ := hexDecode(v.Hex)
|
|
|
|
|
f.Add(b)
|
|
|
|
|
}
|
|
|
|
|
f.Fuzz(func(t *testing.T, b []byte) {
|
|
|
|
|
h, err := capsule.DecodeHead(b, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if n := codes(err); n != 1 {
|
|
|
|
|
t.Fatalf("%d normative codes: %v", n, err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if re, err := capsule.EncodeHead(h); err != nil || !bytes.Equal(re, b) {
|
|
|
|
|
t.Fatalf("accepted a head that does not re-encode to its input: %v", err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FuzzEvaluateSecurity: security never fails, and gives verdicts of this
|
|
|
|
|
// version, X for both the signature and the seal or for neither (spec §29.3,
|
|
|
|
|
// §29.7).
|
|
|
|
|
func FuzzEvaluateSecurity(f *testing.F) {
|
|
|
|
|
for _, name := range fixtureNames {
|
|
|
|
|
if fx := loadFixture(f, name); fx.Security != "" {
|
|
|
|
|
b, _ := hexDecode(fx.Security)
|
|
|
|
|
f.Add(b)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
var security testkit.SecurityVectorFile
|
|
|
|
|
if err := testkit.ReadJSON("../testdata/vectors/security.json", &security); err != nil {
|
|
|
|
|
f.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for _, v := range security.Vectors {
|
|
|
|
|
b, _ := hexDecode(v.Hex)
|
|
|
|
|
f.Add(b)
|
|
|
|
|
}
|
|
|
|
|
f.Fuzz(func(t *testing.T, b []byte) {
|
|
|
|
|
v := capsule.EvaluateSecurity(b)
|
|
|
|
|
switch {
|
|
|
|
|
case v.Signature != capsule.VerdictUnreadable && v.Signature != capsule.VerdictNoSignature && v.Signature != capsule.VerdictSignatureUnchecked,
|
|
|
|
|
v.Seal != capsule.VerdictUnreadable && v.Seal != capsule.VerdictNoSeal && v.Seal != capsule.VerdictSealUnsupported && v.Seal != capsule.VerdictSealUnreadable,
|
|
|
|
|
(v.Signature == capsule.VerdictUnreadable) != (v.Seal == capsule.VerdictUnreadable),
|
|
|
|
|
len(v.Lines()) == 0:
|
|
|
|
|
t.Fatalf("verdicts %+v", v)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// FuzzInspect feeds whole capsules to the pre-unlock validation, and to Open
|
|
|
|
|
// with a source that never has the release: a capsule that Inspect rejects
|
|
|
|
|
// must not cause a request, and nothing may pass the release step. The
|
|
|
|
|
// cryptographic steps after it are exercised by the mutation corpus; keeping
|
|
|
|
|
// them out of this target keeps it fast.
|
|
|
|
|
func FuzzInspect(f *testing.F) {
|
|
|
|
|
seedFixtures(f, whole)
|
|
|
|
|
reg := testkit.Registry()
|
|
|
|
|
far := testkit.Fixed(testkit.Genesis().AddDate(5, 0, 0))
|
|
|
|
|
f.Fuzz(func(t *testing.T, b []byte) {
|
|
|
|
|
_, err := capsule.Inspect(bytes.NewReader(b), capsule.InspectOptions{Registry: reg})
|
|
|
|
|
if err != nil && datekeys.Code(err) == "" {
|
|
|
|
|
t.Fatalf("error without a normative code: %v", err)
|
|
|
|
|
}
|
|
|
|
|
src := testkit.NewSource()
|
Format 3, step 6b: the nine fixtures of format 3
The fixtures of spec 67 for format 3, each with its record, its BODY,
its inspect output and, for time_and_key, its .dkk:
- format3_single, format3_tree (five files in three folders, one over
two STREAM chunks, one without mtime, a comment and a declared
author), format3_comment_only (no files; a TAB in the comment),
format3_bloque256 and format3_time_and_key_portable, written with
EncryptFiles;
- format3_area_1024, format3_security_v2 (verdict X),
format3_signature_unsupported (an author-signature of alg 1 with a
random key of 32 bytes and a random signature of 64: F1) and
format3_seal_unsupported (that and a seal of seal_type 1: F1 and S1),
which only a generator of test vectors writes (62.1 rule 13), built
with testkit.Build.
The record of a format 3 fixture adds the area, SECURITY_CBOR,
HEAD_CBOR, the salt, the comment, the declared author, the head
extensions, the offset of CONTENT in BODY, each file with its layout,
SHA-256 and mtime, and the verdicts with their lines; its plaintext
file is BODY. The generator writes, then recovers every value by
opening layer by layer for the three formats alike, and refreshes the
records of format 3 through a Sink.
Tests: the conformance test checks BODY, the head, security and every
file, and opens through a MemorySink; the .dkk tests take the .dkk of
formats 2 and 3 too (spec 68); the CLI decrypts five of the fixtures
into folders; the control fuzz target decodes with the three schema
versions. The differential corpus gains two bases, format3_single and
format3_time_and_key_portable, one per policy: 5110 cases, the earlier
ones unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
o := capsule.OpenOptions{Registry: reg, Source: src, Now: far, Sink: testkit.DiscardSink{}}
|
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
|
|
|
_, openErr := capsule.Open(context.Background(), io.Discard, bytes.NewReader(b), o)
|
|
|
|
|
switch {
|
|
|
|
|
case openErr == nil:
|
|
|
|
|
t.Fatal("opened without a release")
|
|
|
|
|
case datekeys.Code(openErr) == "":
|
|
|
|
|
t.Fatalf("error without a normative code: %v", openErr)
|
|
|
|
|
case err != nil && src.Calls != 0:
|
|
|
|
|
t.Fatal("a capsule rejected by Inspect caused a release request")
|
|
|
|
|
case err == nil && !errors.Is(openErr, datekeys.ErrReleaseUnavailable) && !errors.Is(openErr, datekeys.ErrAccessRequired):
|
|
|
|
|
t.Fatalf("a capsule accepted by Inspect failed before the release step: %v", openErr)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FuzzEncodeImpliesDecode: whatever EncodeHeader, EncodeControl and
|
|
|
|
|
// accesskey.Encode accept, the matching decoder accepts and re-encodes to the
|
|
|
|
|
// same bytes. An encoder that writes what its reader rejects makes capsules
|
|
|
|
|
// that cannot be opened (spec §72, §76 case 5).
|
|
|
|
|
func FuzzEncodeImpliesDecode(f *testing.F) {
|
|
|
|
|
f.Add("org.example.label", uint64(1), []byte("public label"), "org.example.note", uint64(2), []byte{0xa2, 0x00, 0x07}, uint8(0), uint8(0))
|
|
|
|
|
f.Add("a", uint64(1)<<32, []byte{}, "a", uint64(0), []byte{0x81, 0x81, 0x00}, uint8(0b1011), uint8(63))
|
|
|
|
|
f.Add("org.\xff", uint64(0), []byte{0xf6}, "z", uint64(1)<<53, []byte(nil), uint8(0b0100), uint8(64))
|
|
|
|
|
dk := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000}
|
|
|
|
|
f.Fuzz(func(t *testing.T, id1 string, v1 uint64, d1 []byte, id2 string, v2 uint64, d2 []byte, mode, filler uint8) {
|
|
|
|
|
// mode bit 0: first extension critical; bit 1: second critical;
|
|
|
|
|
// bit 2: first without data; bit 3: second without data. filler
|
|
|
|
|
// adds extensions to the noncritical array, up to past the limit.
|
|
|
|
|
e1 := extension.Extension{ID: id1, Version: v1, Data: d1}
|
|
|
|
|
e2 := extension.Extension{ID: id2, Version: v2, Data: d2}
|
|
|
|
|
if mode&4 != 0 {
|
|
|
|
|
e1.Data = nil
|
|
|
|
|
}
|
|
|
|
|
if mode&8 != 0 {
|
|
|
|
|
e2.Data = nil
|
|
|
|
|
}
|
|
|
|
|
var crit, non []extension.Extension
|
|
|
|
|
for i, e := range []extension.Extension{e1, e2} {
|
|
|
|
|
if mode&(1<<i) != 0 {
|
|
|
|
|
crit = append(crit, e)
|
|
|
|
|
} else {
|
|
|
|
|
non = append(non, e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for i := range int(filler % 72) {
|
|
|
|
|
non = append(non, extension.Extension{ID: fmt.Sprintf("x.%02d", i), Version: 1})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h := &capsule.Header{DateKey: dk, Policy: capsule.Policy(mode >> 4 & 1), Critical: crit, Noncritical: non}
|
|
|
|
|
if b, err := capsule.EncodeHeader(h); err == nil {
|
|
|
|
|
back, err := capsule.DecodeHeader(b)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("EncodeHeader wrote a PUBLIC_HEADER that DecodeHeader rejects: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if re, err := capsule.EncodeHeader(back); err != nil || !bytes.Equal(re, b) {
|
|
|
|
|
t.Fatal("PUBLIC_HEADER does not re-encode to itself")
|
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
// mode bits 5 and 6: the padding code of the format 2 control, from 0
|
|
|
|
|
// to 3; v2 is its payload_length, possibly above L_MAX.
|
|
|
|
|
for _, format := range []capsule.Format{capsule.Format1, capsule.Format2} {
|
|
|
|
|
c := &capsule.Control{Critical: crit, Noncritical: non}
|
|
|
|
|
if format == capsule.Format2 {
|
|
|
|
|
c.PayloadLength, c.Padding = v2, capsule.Padding(mode>>5&3)
|
|
|
|
|
}
|
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 b, err := capsule.EncodeControl(c, format); err == nil {
|
|
|
|
|
back, err := capsule.DecodeControl(b, format)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("EncodeControl wrote a format %d CONTROL_CBOR that DecodeControl rejects: %v", format, err)
|
|
|
|
|
}
|
|
|
|
|
if re, err := capsule.EncodeControl(back, format); err != nil || !bytes.Equal(re, b) {
|
|
|
|
|
t.Fatalf("format %d CONTROL_CBOR does not re-encode to itself", format)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
k := &accesskey.AccessKey{Type: accesskey.TypeX25519, Material: make([]byte, 32), Critical: crit, Noncritical: non}
|
|
|
|
|
var dkk bytes.Buffer
|
|
|
|
|
if err := accesskey.Encode(&dkk, k); err == nil {
|
|
|
|
|
back, err := accesskey.Decode(bytes.NewReader(dkk.Bytes()))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("accesskey.Encode wrote a .dkk that Decode rejects: %v", err)
|
|
|
|
|
}
|
|
|
|
|
var re bytes.Buffer
|
|
|
|
|
if err := accesskey.Encode(&re, back); err != nil || !bytes.Equal(re.Bytes(), dkk.Bytes()) {
|
|
|
|
|
t.Fatal(".dkk does not re-encode to itself")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
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 hexDecode(s string) ([]byte, error) { return hex.DecodeString(s) }
|