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/internal/testkit/testkit_test.go

321 lines
11 KiB

package testkit_test
import (
"bytes"
"encoding/hex"
"encoding/json"
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
"os"
"path/filepath"
"reflect"
"testing"
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
"filippo.io/age"
Format 3, step 6c: the vectors of paths, keys, heads and security The vector files of spec 67 for format 3, generated with the result each case is written for, so that the generator fails when the implementation or the tables change: - paths.json: 83 paths with the result of the rules of one entry, the violation worded as every implementation must word it, and 16 trees, the paths of a head and the result of decoding it: U+00A0, accepted, and U+3000, R6c, at both ends of a segment; best-fit, full-width forms, U+00B4 of cp1253; 8.3 aliases with ~1; Cn; U+206A to U+206F, tags and other ignorables; a dot and ZWJ, and ZWJ alone; 127 and 85 times U+0390; U+F03A; .datekeys-x at two levels; U+FF5E and U+1F600 in both orders; ab with and without ZWNJ; U+00BF, U+00A7 and U+2665, accepted; VS16 after U+2764 and after a; ZWJ at the start, at the end and twice; the rainbow flag and the flag of Scotland; b/.. and a. - path_fold.json: 22 segments with their NFD and their key of R7, among them the entries F of CaseFolding, the dotless i, the Kelvin and Angstrom signs, Cherokee, Hangul and the whitelist dropped before NFD. - head_schema.json: 63 heads through layers 2, 3 and 4, in key order, with the violation of each ERR_HEAD_INVALID; they add comments with tags and with loose variation selectors. - security.json: 21 areas with their verdicts, X, F0, F1, S0, S1 and S2, among them key 2 that is not a byte string, a key 4, a byte more, alg 0, an empty key with the seal intact, and a seal that breaks its schema with an unknown seal_type. - cbor.json gains the control of schema version 3. A test replays every committed vector through the implementation, and another checks that the files are current. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
"g.activething.com/go/DateKeys/internal/pathrule"
"g.activething.com/go/DateKeys/internal/testkit"
Spec v0.8.2 amendment: canonical point encoding; no library error text Amendment of the unreleased v0.8.2, recorded in §76 with its case: the second implementation's phase-2 research found that tlock-js over @noble/curves 1.9.7 accepts U re-encoded as c0 + p and a signature x + p and returns the same file key, while the reference rejects both (noble 1.9.7 differed from kilic on 5,615 of 41,686 encodings), and the spec did not say which encodings are valid. - §12.2 defines the canonical encoding of a BLS12-381 point (drand's compressed ZCash form) and requires decoders to reject every other byte string; §12.1 applies it to public_key. - §63 step 10 applies it to the release signature (ERR_RELEASE_INVALID) and step 11 defines the tlock stanza body U || V || W (96 + 16 + 16 bytes for Quicknet) with a canonical, non-infinity U (ERR_INTEGRITY). - §64 gains ten mutations, exported to mutations.json (65 cases). The signature x + p case uses published Quicknet round 1004, the first after 1000 whose x allows x + p < 2^381. The reference already gave every stated code and step. Errors no longer copy text from tlock, kyber, age, drand or kyber-bls12381. kyber's IBE error carried the candidate plaintext and r, and with one bit of W flipped the message disclosed the real tlock file key with that bit flipped. Every such place now uses a fixed reason with its normative sentinel; TestTlockFailureDiagnosticsCarryNoSecrets fails with the old wrapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
"g.activething.com/go/DateKeys/profile"
)
func TestEdits(t *testing.T) {
base := []byte("0123456789")
for _, tc := range []struct {
out string
want string // JSON of Splice(base, out)
}{
{"0123456789", `[]`},
{"01X3456789", `[[2,1,"58"]]`},
{"012", `[[3,7,""]]`},
{"", `[[0,10,""]]`},
{"01234567890", `[[10,0,"30"]]`},
{"0123XY456789", `[[4,0,"5859"]]`},
} {
edits := testkit.Splice(base, []byte(tc.out))
if edits == nil {
edits = []testkit.Edit{}
}
b, err := json.Marshal(edits)
if err != nil || string(b) != tc.want {
t.Errorf("Splice(%q) = %s, %v; want %s", tc.out, b, err, tc.want)
}
var back []testkit.Edit
if err := json.Unmarshal(b, &back); err != nil {
t.Fatal(err)
}
out, err := testkit.ApplyEdits(base, back)
if err != nil || string(out) != tc.out {
t.Errorf("ApplyEdits(%s) = %q, %v; want %q", b, out, err, tc.out)
}
}
// Several edits refer to offsets of the base.
out, err := testkit.ApplyEdits(base, []testkit.Edit{{At: 1, Delete: 1, Insert: []byte("ab")}, {At: 5, Delete: 2}, {At: 10, Insert: []byte("!")}})
if err != nil || string(out) != "0ab234789!" {
t.Fatalf("got %q, %v", out, err)
}
for _, bad := range [][]testkit.Edit{
{{At: 5, Delete: 1}, {At: 2, Delete: 1}}, // out of order
{{At: 2, Delete: 3}, {At: 4, Delete: 1}}, // overlapping
{{At: 9, Delete: 2}}, // beyond the base
{{At: -1}},
} {
if _, err := testkit.ApplyEdits(base, bad); err == nil {
t.Errorf("ApplyEdits accepted %v", bad)
}
}
for _, bad := range []string{`[1,2]`, `[1,2,"zz"]`, `["1",2,""]`, `{}`} {
var e testkit.Edit
if err := json.Unmarshal([]byte(bad), &e); err == nil {
t.Errorf("Edit accepted %s", bad)
}
}
}
Spec v0.8.2 amendment: canonical point encoding; no library error text Amendment of the unreleased v0.8.2, recorded in §76 with its case: the second implementation's phase-2 research found that tlock-js over @noble/curves 1.9.7 accepts U re-encoded as c0 + p and a signature x + p and returns the same file key, while the reference rejects both (noble 1.9.7 differed from kilic on 5,615 of 41,686 encodings), and the spec did not say which encodings are valid. - §12.2 defines the canonical encoding of a BLS12-381 point (drand's compressed ZCash form) and requires decoders to reject every other byte string; §12.1 applies it to public_key. - §63 step 10 applies it to the release signature (ERR_RELEASE_INVALID) and step 11 defines the tlock stanza body U || V || W (96 + 16 + 16 bytes for Quicknet) with a canonical, non-infinity U (ERR_INTEGRITY). - §64 gains ten mutations, exported to mutations.json (65 cases). The signature x + p case uses published Quicknet round 1004, the first after 1000 whose x allows x + p < 2^381. The reference already gave every stated code and step. Errors no longer copy text from tlock, kyber, age, drand or kyber-bls12381. kyber's IBE error carried the candidate plaintext and r, and with one bit of W flipped the message disclosed the real tlock file key with that bit flipped. Every such place now uses a fixed reason with its normative sentinel; TestTlockFailureDiagnosticsCarryNoSecrets fails with the old wrapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
// The re-encodings of the point mutations (spec §12.2) change only what their
// names say, and x + p fits for XPlusPRound only among the known rounds.
func TestPointReencodings(t *testing.T) {
sig := testkit.Release(testkit.XPlusPRound).Signature
xp, err := testkit.AddModulus(sig, 0)
if err != nil {
t.Fatal(err)
}
if xp[0]&0xe0 != sig[0]&0xe0 || bytes.Equal(xp, sig) || !bytes.Equal(testkit.ReduceCoordinate(xp, 0), sig) {
t.Fatalf("x + p of %x is %x", sig, xp)
}
for _, r := range testkit.Rounds {
if _, err := testkit.AddModulus(testkit.Release(r).Signature, 0); (err == nil) != (r == testkit.XPlusPRound) {
t.Errorf("round %d: x + p fits: %v", r, err == nil)
}
}
key := profile.Quicknet().PublicKey
for _, at := range []int{0, testkit.CoordinateLen} {
k, err := testkit.AddModulus(key, at)
if err != nil {
t.Fatalf("coordinate at %d of the Quicknet key: %v", at, err)
}
other := testkit.CoordinateLen - at
if !bytes.Equal(k[other:other+testkit.CoordinateLen], key[other:other+testkit.CoordinateLen]) || !bytes.Equal(testkit.ReduceCoordinate(k, at), key) {
t.Fatalf("coordinate at %d: %x", at, k)
}
}
for _, at := range []int{-48, 1, testkit.CoordinateLen} {
if _, err := testkit.AddModulus(sig, at); err == nil {
t.Errorf("offset %d of a G1 point accepted", at)
}
}
if n := testkit.Negated(sig); n[0]^sig[0] != testkit.FlagSort || !bytes.Equal(n[1:], sig[1:]) || !bytes.Equal(testkit.Negated(n), sig) {
t.Fatalf("negated %x", n)
}
if i := testkit.InfinityWithPayload(sig); i[0] != 0xc0|sig[0]&0x1f || !bytes.Equal(i[1:], sig[1:]) {
t.Fatalf("infinity with payload %x", i)
}
if i := testkit.Infinity(96); len(i) != 96 || i[0] != 0xc0 || !bytes.Equal(i[1:], make([]byte, 95)) {
t.Fatalf("infinity %x", i)
}
}
// The schema vectors of testdata/vectors/cbor.json replay: the decoder of each
// schema gives exactly the recorded result.
func TestSchemaVectors(t *testing.T) {
var f testkit.CBORVectorFile
if err := testkit.ReadJSON("../../testdata/vectors/cbor.json", &f); err != nil {
t.Fatal(err)
}
blocks := map[string]int{}
for _, v := range f.Schemas {
b, err := hex.DecodeString(v.Hex)
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
if got := testkit.Result(testkit.DecodeSchema(v.Schema, b, v.Format)); got != v.Result {
t.Errorf("%s %q: got %s, want %s", v.Block, v.Name, got, v.Result)
}
blocks[v.Block]++
}
for _, b := range []string{testkit.SchemaProfile, testkit.SchemaHeader, testkit.SchemaControl, testkit.SchemaDKKBody, "verification_metadata", "extension"} {
if blocks[b] < 5 {
t.Errorf("block %s has %d vectors", b, blocks[b])
}
}
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 := testkit.DecodeSchema("nope", nil, 0); err == nil {
t.Error("unknown schema accepted")
}
}
Format 3, step 6c: the vectors of paths, keys, heads and security The vector files of spec 67 for format 3, generated with the result each case is written for, so that the generator fails when the implementation or the tables change: - paths.json: 83 paths with the result of the rules of one entry, the violation worded as every implementation must word it, and 16 trees, the paths of a head and the result of decoding it: U+00A0, accepted, and U+3000, R6c, at both ends of a segment; best-fit, full-width forms, U+00B4 of cp1253; 8.3 aliases with ~1; Cn; U+206A to U+206F, tags and other ignorables; a dot and ZWJ, and ZWJ alone; 127 and 85 times U+0390; U+F03A; .datekeys-x at two levels; U+FF5E and U+1F600 in both orders; ab with and without ZWNJ; U+00BF, U+00A7 and U+2665, accepted; VS16 after U+2764 and after a; ZWJ at the start, at the end and twice; the rainbow flag and the flag of Scotland; b/.. and a. - path_fold.json: 22 segments with their NFD and their key of R7, among them the entries F of CaseFolding, the dotless i, the Kelvin and Angstrom signs, Cherokee, Hangul and the whitelist dropped before NFD. - head_schema.json: 63 heads through layers 2, 3 and 4, in key order, with the violation of each ERR_HEAD_INVALID; they add comments with tags and with loose variation selectors. - security.json: 21 areas with their verdicts, X, F0, F1, S0, S1 and S2, among them key 2 that is not a byte string, a key 4, a byte more, alg 0, an empty key with the seal intact, and a seal that breaks its schema with an unknown seal_type. - cbor.json gains the control of schema version 3. A test replays every committed vector through the implementation, and another checks that the files are current. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// The vectors of format 3 replay: each path, tree, key, head and security
// area of the committed files gets its recorded result from the
// implementation, with the tables the files name (spec §29.3 to §29.7).
func TestFormat3VectorFiles(t *testing.T) {
const dir = "../../testdata/vectors/"
var paths testkit.PathVectorFile
if err := testkit.ReadJSON(dir+"paths.json", &paths); err != nil {
t.Fatal(err)
}
var fold testkit.PathFoldFile
if err := testkit.ReadJSON(dir+"path_fold.json", &fold); err != nil {
t.Fatal(err)
}
for _, f := range []struct{ version, digest string }{{paths.UnicodeVersion, paths.TablesDigest}, {fold.UnicodeVersion, fold.TablesDigest}} {
if f.version != pathrule.UnicodeVersion || f.digest != pathrule.TablesDigest {
t.Fatalf("vectors of Unicode %s and tables %s", f.version, f.digest)
}
}
for _, v := range paths.Paths {
if got := testkit.PathResult(v.Path); got != v.Result {
t.Errorf("path %q: %q, want %q", v.Name, got, v.Result)
}
}
for _, v := range paths.Trees {
b, err := testkit.TreeHead(v.Paths)
if err != nil {
t.Fatal(err)
}
if got, detail := testkit.HeadResult(b); got != v.Result || detail != v.Detail {
t.Errorf("tree %q: %s %q, want %s %q", v.Name, got, detail, v.Result, v.Detail)
}
}
for _, v := range fold.Keys {
if pathrule.NFD(v.Segment) != v.NFD || pathrule.Key(v.Segment) != v.Key {
t.Errorf("key %q: NFD %+q, key %+q", v.Name, pathrule.NFD(v.Segment), pathrule.Key(v.Segment))
}
}
var heads testkit.HeadSchemaFile
if err := testkit.ReadJSON(dir+"head_schema.json", &heads); err != nil {
t.Fatal(err)
}
for _, v := range heads.Heads {
b, err := hex.DecodeString(v.Hex)
if err != nil {
t.Fatal(err)
}
if got, detail := testkit.HeadResult(b); got != v.Result || detail != v.Detail {
t.Errorf("head %q: %s %q, want %s %q", v.Name, got, detail, v.Result, v.Detail)
}
}
var security testkit.SecurityVectorFile
if err := testkit.ReadJSON(dir+"security.json", &security); err != nil {
t.Fatal(err)
}
verdicts := map[string]bool{}
for _, v := range security.Vectors {
b, err := hex.DecodeString(v.Hex)
if err != nil {
t.Fatal(err)
}
if sig, seal := testkit.SecurityResult(b); sig != v.Signature || seal != v.Seal {
t.Errorf("security %q: %s %s, want %s %s", v.Name, sig, seal, v.Signature, v.Seal)
}
verdicts[v.Signature], verdicts[v.Seal] = true, true
}
// Every verdict this version can reach has a vector.
for _, v := range []string{"X", "F0", "F1", "S0", "S1", "S2"} {
if !verdicts[v] {
t.Errorf("no vector gives %s", v)
}
}
}
// The committed vector files are what the generators compute now.
func TestVectorFilesAreCurrent(t *testing.T) {
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
cbor, err := testkit.CBORVectors()
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
padding, err := testkit.PaddingVectors()
if err != nil {
t.Fatal(err)
}
Format 3, step 6c: the vectors of paths, keys, heads and security The vector files of spec 67 for format 3, generated with the result each case is written for, so that the generator fails when the implementation or the tables change: - paths.json: 83 paths with the result of the rules of one entry, the violation worded as every implementation must word it, and 16 trees, the paths of a head and the result of decoding it: U+00A0, accepted, and U+3000, R6c, at both ends of a segment; best-fit, full-width forms, U+00B4 of cp1253; 8.3 aliases with ~1; Cn; U+206A to U+206F, tags and other ignorables; a dot and ZWJ, and ZWJ alone; 127 and 85 times U+0390; U+F03A; .datekeys-x at two levels; U+FF5E and U+1F600 in both orders; ab with and without ZWNJ; U+00BF, U+00A7 and U+2665, accepted; VS16 after U+2764 and after a; ZWJ at the start, at the end and twice; the rainbow flag and the flag of Scotland; b/.. and a. - path_fold.json: 22 segments with their NFD and their key of R7, among them the entries F of CaseFolding, the dotless i, the Kelvin and Angstrom signs, Cherokee, Hangul and the whitelist dropped before NFD. - head_schema.json: 63 heads through layers 2, 3 and 4, in key order, with the violation of each ERR_HEAD_INVALID; they add comments with tags and with loose variation selectors. - security.json: 21 areas with their verdicts, X, F0, F1, S0, S1 and S2, among them key 2 that is not a byte string, a key 4, a byte more, alg 0, an empty key with the seal intact, and a seal that breaks its schema with an unknown seal_type. - cbor.json gains the control of schema version 3. A test replays every committed vector through the implementation, and another checks that the files are current. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
paths, err := testkit.PathVectors()
if err != nil {
t.Fatal(err)
}
fold, err := testkit.PathFoldVectors()
if err != nil {
t.Fatal(err)
}
heads, err := testkit.HeadSchemaVectors()
if err != nil {
t.Fatal(err)
}
security, err := testkit.SecurityVectors()
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
for _, v := range []struct {
file string
want any
got any
}{
{"cbor.json", cbor, &testkit.CBORVectorFile{}},
{"padding.json", padding, &testkit.PaddingVectorFile{}},
Format 3, step 6c: the vectors of paths, keys, heads and security The vector files of spec 67 for format 3, generated with the result each case is written for, so that the generator fails when the implementation or the tables change: - paths.json: 83 paths with the result of the rules of one entry, the violation worded as every implementation must word it, and 16 trees, the paths of a head and the result of decoding it: U+00A0, accepted, and U+3000, R6c, at both ends of a segment; best-fit, full-width forms, U+00B4 of cp1253; 8.3 aliases with ~1; Cn; U+206A to U+206F, tags and other ignorables; a dot and ZWJ, and ZWJ alone; 127 and 85 times U+0390; U+F03A; .datekeys-x at two levels; U+FF5E and U+1F600 in both orders; ab with and without ZWNJ; U+00BF, U+00A7 and U+2665, accepted; VS16 after U+2764 and after a; ZWJ at the start, at the end and twice; the rainbow flag and the flag of Scotland; b/.. and a. - path_fold.json: 22 segments with their NFD and their key of R7, among them the entries F of CaseFolding, the dotless i, the Kelvin and Angstrom signs, Cherokee, Hangul and the whitelist dropped before NFD. - head_schema.json: 63 heads through layers 2, 3 and 4, in key order, with the violation of each ERR_HEAD_INVALID; they add comments with tags and with loose variation selectors. - security.json: 21 areas with their verdicts, X, F0, F1, S0, S1 and S2, among them key 2 that is not a byte string, a key 4, a byte more, alg 0, an empty key with the seal intact, and a seal that breaks its schema with an unknown seal_type. - cbor.json gains the control of schema version 3. A test replays every committed vector through the implementation, and another checks that the files are current. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
{"paths.json", paths, &testkit.PathVectorFile{}},
{"path_fold.json", fold, &testkit.PathFoldFile{}},
{"head_schema.json", heads, &testkit.HeadSchemaFile{}},
{"security.json", security, &testkit.SecurityVectorFile{}},
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 := testkit.ReadJSON("../../testdata/vectors/"+v.file, v.got); err != nil {
t.Fatal(err)
}
a, _ := json.Marshal(v.want)
b, _ := json.Marshal(v.got)
if !bytes.Equal(a, b) {
t.Errorf("testdata/vectors/%s is stale: run go run ./internal/testkit/genfixtures -out testdata", v.file)
}
}
}
// The tools that seal age files again with known keys reproduce the official
// fixtures byte for byte when nothing is edited: their STREAM, header MAC and
// PRELUDE are those of age and of the reference, so that the format 2
// mutations derived with them differ only where they are edited.
func TestResealReproducesFixtures(t *testing.T) {
const dir = "../../testdata/fixtures"
to, err := testkit.LoadFixture(dir, "format2_time_only")
if err != nil {
t.Fatal(err)
}
same, err := to.WithControl(func(map[uint64]any) {})
if err != nil || !bytes.Equal(same.DKC, to.DKC) {
t.Fatalf("WithControl without an edit changes the capsule: %v", err)
}
content, err := os.ReadFile(filepath.Join(dir, to.PlaintextFile))
if err != nil {
t.Fatal(err)
}
padded := append(content, make([]byte, to.PaddedLength-to.PayloadLength)...)
same, err = to.WithPayloadPlaintext(padded)
if err != nil || !bytes.Equal(same.DKC, to.DKC) {
t.Fatalf("WithPayloadPlaintext of the same plaintext changes the capsule: %v", err)
}
tk, err := testkit.LoadFixture(dir, "format2_time_and_key_portable")
if err != nil {
t.Fatal(err)
}
same, err = tk.WithInnerStanzas(func(_ []byte, s []*age.Stanza) ([]*age.Stanza, error) { return s, nil })
if err != nil || !bytes.Equal(same.DKC, tk.DKC) || same.DKK != nil || len(same.Identities) != 1 {
t.Fatalf("WithInnerStanzas without an edit changes the capsule: %v", err)
}
if testkit.StreamLen(0) != 16 || testkit.StreamLen(65536) != 65552 || testkit.StreamLen(65537) != 65569 {
t.Fatal("StreamLen")
}
}
// FixedX25519Stanza builds the stanza age builds, with a chosen ephemeral:
// the identity of the recipient unwraps the file key, and the same seed gives
// the same stanza.
func TestFixedX25519Stanza(t *testing.T) {
id, _ := age.GenerateX25519Identity()
fk := bytes.Repeat([]byte{7}, 16)
s, err := testkit.FixedX25519Stanza(fk, id.Recipient(), "seed")
if err != nil {
t.Fatal(err)
}
got, err := id.Unwrap([]*age.Stanza{s})
if err != nil || !bytes.Equal(got, fk) {
t.Fatalf("unwrap: %v", err)
}
again, _ := testkit.FixedX25519Stanza(fk, id.Recipient(), "seed")
other, _ := testkit.FixedX25519Stanza(fk, id.Recipient(), "other seed")
if !reflect.DeepEqual(s, again) || reflect.DeepEqual(s, other) {
t.Fatal("the stanza does not depend on the seed alone")
}
}

Powered by TurnKey Linux.