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/capsule/framing_test.go

352 lines
16 KiB

package capsule_test
import (
"bytes"
"context"
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
"encoding/binary"
"errors"
"fmt"
"io"
"strings"
"testing"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/datekey"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/cbortest"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/profile"
)
func TestPolicyNames(t *testing.T) {
for _, p := range []capsule.Policy{capsule.TimeOnly, capsule.TimeAndKey} {
got, err := capsule.ParsePolicy(p.String())
if err != nil || got != p {
t.Fatalf("%s: %v", p, err)
}
}
if _, err := capsule.ParsePolicy("time_or_key"); err == nil {
t.Fatal("unknown policy parsed")
}
if capsule.Policy(9).String() != "policy(9)" {
t.Fatal("unknown policy name")
}
}
func TestControlIsNotPrinted(t *testing.T) {
c := capsule.Control{}
for i := range c.PayloadIdentity {
c.PayloadIdentity[i] = 0xab
}
for _, s := range []string{fmt.Sprint(c), fmt.Sprintf("%+v", &c), fmt.Sprintf("%#v", c)} {
if strings.Contains(s, "abab") || !strings.Contains(s, "REDACTED") {
t.Fatalf("I_PAYLOAD printed: %s", s)
}
}
}
func TestEncodeHeaderAndControlReject(t *testing.T) {
dk := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000}
dup := []extension.Extension{{ID: "a", Version: 1}, {ID: "a", Version: 2}}
for name, h := range map[string]capsule.Header{
"invalid DateKey": {},
"unknown policy": {DateKey: dk, Policy: 5},
"repeated critical": {DateKey: dk, Critical: dup},
"repeated noncritical": {DateKey: dk, Noncritical: dup},
"both arrays": {DateKey: dk, Critical: dup[:1], Noncritical: dup[1:]},
} {
if _, err := capsule.EncodeHeader(&h); err == nil {
t.Errorf("%s: accepted", name)
}
}
for name, c := range map[string]capsule.Control{
"repeated critical": {Critical: dup},
"repeated noncritical": {Noncritical: dup},
"both arrays": {Critical: dup[:1], Noncritical: dup[1:]},
} {
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 _, f := range []capsule.Format{capsule.Format1, capsule.Format2} {
c := c
if f == capsule.Format2 {
c.Padding = capsule.Reforzado
}
if _, err := capsule.EncodeControl(&c, f); err == nil {
t.Errorf("control %s, format %d: accepted", name, f)
}
}
}
// Keys 6 and 7 exist in format 2 only, with a defined code and an L of at
// most L_MAX (spec §29.1, §31).
for name, tc := range map[string]struct {
c capsule.Control
f capsule.Format
}{
"format 1 with a padding code": {capsule.Control{Padding: capsule.Reforzado}, capsule.Format1},
"format 1 with a payload_length": {capsule.Control{PayloadLength: 1}, capsule.Format1},
"format 2 without a code": {capsule.Control{}, capsule.Format2},
"format 2 with code 3": {capsule.Control{Padding: 3}, capsule.Format2},
"format 2 above L_MAX": {capsule.Control{Padding: capsule.Bloque256, PayloadLength: capsule.MaxPayloadLength + 1}, capsule.Format2},
"format 3 without a code": {capsule.Control{}, capsule.Format3},
"format 4": {capsule.Control{Padding: capsule.Reforzado}, 4},
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 := capsule.EncodeControl(&tc.c, tc.f); err == nil {
t.Errorf("%s: accepted", name)
}
}
}
func marshal(t *testing.T, m map[uint64]any) []byte {
t.Helper()
b, err := cbortest.Marshal(m)
if err != nil {
t.Fatal(err)
}
return b
}
func ext(id string, v uint64) map[uint64]any { return map[uint64]any{0: id, 1: v} }
func extData(id string, data any) map[uint64]any { return map[uint64]any{0: id, 1: uint64(1), 2: data} }
// manyExts returns n distinct extensions in canonical order.
func manyExts(n int) []any {
out := make([]any, n)
for i := range out {
out[i] = ext(fmt.Sprintf("org.example.%03d", i), 1)
}
return out
}
// Spec §54: extension rules shared by PUBLIC_HEADER and CONTROL_CBOR, as edits
// of the extension array at key.
func extensionRules(key uint64) map[string]func(m map[uint64]any) {
return map[string]func(m map[uint64]any){
"data of type text": func(m map[uint64]any) { m[key] = []any{extData("a", "text")} },
"data of type array": func(m map[uint64]any) { m[key] = []any{extData("a", []any{uint64(1)})} },
"data of type unsigned": func(m map[uint64]any) { m[key] = []any{extData("a", uint64(7))} },
"null data": func(m map[uint64]any) { m[key] = []any{extData("a", nil)} },
"empty data h''": func(m map[uint64]any) { m[key] = []any{extData("a", []byte{})} },
"65 extensions": func(m map[uint64]any) { m[key] = manyExts(65) },
"extension_version 2^32": func(m map[uint64]any) { m[key] = []any{ext("a", 1<<32)} },
"extension_version 2^53-1": func(m map[uint64]any) { m[key] = []any{ext("a", 1<<53-1)} },
"extension map with key 3": func(m map[uint64]any) { m[key] = []any{map[uint64]any{0: "a", 1: uint64(1), 3: []byte{1}}} },
"extension without version": func(m map[uint64]any) { m[key] = []any{map[uint64]any{0: "a"}} },
}
}
func TestDecodeHeaderRejects(t *testing.T) {
dk := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000}.Compact()
base := func() map[uint64]any {
return map[uint64]any{0: capsule.HeaderTypeTag, 1: uint64(1), 2: make([]byte, 16), 3: dk, 4: uint64(0)}
}
for _, tc := range []struct {
name string
edit func(m map[uint64]any)
want error
}{
{"short capsule_id", func(m map[uint64]any) { m[2] = make([]byte, 15) }, datekeys.ErrNonCanonicalCBOR},
{"invalid DateKey", func(m map[uint64]any) { m[3] = "dk1_x" }, datekeys.ErrDateKeyInvalid},
{"type tag", func(m map[uint64]any) { m[0] = capsule.ControlTypeTag }, datekeys.ErrNonCanonicalCBOR},
{"critical out of order", func(m map[uint64]any) { m[5] = []any{ext("b", 1), ext("a", 1)} }, datekeys.ErrNonCanonicalCBOR},
{"noncritical repeated", func(m map[uint64]any) { m[6] = []any{ext("a", 1), ext("a", 2)} }, datekeys.ErrNonCanonicalCBOR},
{"both arrays", func(m map[uint64]any) { m[5] = []any{ext("a", 1)}; m[6] = []any{ext("a", 1)} }, datekeys.ErrNonCanonicalCBOR},
} {
m := base()
tc.edit(m)
if _, err := capsule.DecodeHeader(marshal(t, m)); !errors.Is(err, tc.want) {
t.Errorf("%s: got %v, want %v", tc.name, err, tc.want)
}
}
for name, edit := range extensionRules(6) {
m := base()
edit(m)
if _, err := capsule.DecodeHeader(marshal(t, m)); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
t.Errorf("%s: got %v", name, err)
}
}
valid := base()
valid[6] = append(manyExts(63), extData("z", []byte{0}))
if h, err := capsule.DecodeHeader(marshal(t, valid)); err != nil || len(h.Noncritical) != 64 {
t.Fatalf("valid header with 64 extensions rejected: %v", 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
// payloadLength is key 6 of a version 2 control: L in 8 bytes, big-endian.
func payloadLength(l uint64) []byte { return binary.BigEndian.AppendUint64(nil, l) }
func TestDecodeControlRejects(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
// base is a version 1 control, of format 1, and base2 a version 2
// control, of format 2.
base := func() map[uint64]any {
return map[uint64]any{0: capsule.ControlTypeTag, 1: uint64(1), 2: make([]byte, 32), 3: make([]byte, 32)}
}
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
base2 := func() map[uint64]any {
m := base()
m[1], m[6], m[7] = uint64(2), payloadLength(78000), uint64(capsule.Reforzado)
return m
}
for _, tc := range []struct {
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
name string
format capsule.Format
base func() map[uint64]any
edit func(m map[uint64]any)
want error
}{
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
{"schema version", capsule.Format1, base, func(m map[uint64]any) { m[1] = uint64(2) }, datekeys.ErrUnsupportedVersion},
{"type tag", capsule.Format1, base, func(m map[uint64]any) { m[0] = capsule.HeaderTypeTag }, datekeys.ErrNonCanonicalCBOR},
{"short binding", capsule.Format1, base, func(m map[uint64]any) { m[2] = make([]byte, 31) }, datekeys.ErrNonCanonicalCBOR},
{"long identity", capsule.Format1, base, func(m map[uint64]any) { m[3] = make([]byte, 33) }, datekeys.ErrNonCanonicalCBOR},
{"unknown key", capsule.Format1, base, func(m map[uint64]any) { m[6] = "x" }, datekeys.ErrNonCanonicalCBOR},
{"critical repeated", capsule.Format1, base, func(m map[uint64]any) { m[4] = []any{ext("a", 1), ext("a", 2)} }, datekeys.ErrNonCanonicalCBOR},
{"noncritical out of order", capsule.Format1, base, func(m map[uint64]any) { m[5] = []any{ext("b", 1), ext("a", 1)} }, datekeys.ErrNonCanonicalCBOR},
{"both arrays", capsule.Format1, base, func(m map[uint64]any) { m[4] = []any{ext("a", 1)}; m[5] = []any{ext("a", 1)} }, datekeys.ErrNonCanonicalCBOR},
// Keys 6 and 7 (spec §31): defined in version 2 only, required there.
{"format 1: keys 6 and 7", capsule.Format1, base, func(m map[uint64]any) { m[6], m[7] = payloadLength(0), uint64(2) }, datekeys.ErrNonCanonicalCBOR},
{"format 1: a valid version 2 control", capsule.Format1, base2, func(map[uint64]any) {}, datekeys.ErrUnsupportedVersion},
{"format 2: a valid version 1 control", capsule.Format2, base, func(map[uint64]any) {}, datekeys.ErrUnsupportedVersion},
{"format 2: version 1 and an unknown key", capsule.Format2, base2, func(m map[uint64]any) { m[1], m[9] = uint64(1), uint64(0) }, datekeys.ErrUnsupportedVersion},
{"format 2: version 3", capsule.Format2, base2, func(m map[uint64]any) { m[1] = uint64(3) }, datekeys.ErrUnsupportedVersion},
{"format 2: without key 6", capsule.Format2, base2, func(m map[uint64]any) { delete(m, 6) }, datekeys.ErrNonCanonicalCBOR},
{"format 2: without key 7", capsule.Format2, base2, func(m map[uint64]any) { delete(m, 7) }, datekeys.ErrNonCanonicalCBOR},
{"format 2: payload_length of 7 bytes", capsule.Format2, base2, func(m map[uint64]any) { m[6] = make([]byte, 7) }, datekeys.ErrNonCanonicalCBOR},
{"format 2: payload_length of 9 bytes", capsule.Format2, base2, func(m map[uint64]any) { m[6] = make([]byte, 9) }, datekeys.ErrNonCanonicalCBOR},
{"format 2: payload_length as an unsigned integer", capsule.Format2, base2, func(m map[uint64]any) { m[6] = uint64(78000) }, datekeys.ErrNonCanonicalCBOR},
{"format 2: payload_length L_MAX + 1", capsule.Format2, base2, func(m map[uint64]any) { m[6] = payloadLength(capsule.MaxPayloadLength + 1) }, datekeys.ErrNonCanonicalCBOR},
{"format 2: payload_length 2^64 - 1", capsule.Format2, base2, func(m map[uint64]any) { m[6] = bytes.Repeat([]byte{0xff}, 8) }, datekeys.ErrNonCanonicalCBOR},
{"format 2: padding 0", capsule.Format2, base2, func(m map[uint64]any) { m[7] = uint64(0) }, datekeys.ErrNonCanonicalCBOR},
{"format 2: padding 3", capsule.Format2, base2, func(m map[uint64]any) { m[7] = uint64(3) }, datekeys.ErrNonCanonicalCBOR},
{"format 2: padding 257", capsule.Format2, base2, func(m map[uint64]any) { m[7] = uint64(257) }, datekeys.ErrNonCanonicalCBOR},
{"format 2: padding as a byte string", capsule.Format2, base2, func(m map[uint64]any) { m[7] = []byte{2} }, datekeys.ErrNonCanonicalCBOR},
{"format 2: unknown key 8", capsule.Format2, base2, func(m map[uint64]any) { m[8] = uint64(0) }, datekeys.ErrNonCanonicalCBOR},
} {
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
m := tc.base()
tc.edit(m)
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 := capsule.DecodeControl(marshal(t, m), tc.format); !errors.Is(err, tc.want) {
t.Errorf("%s: got %v, want %v", tc.name, err, tc.want)
}
}
for name, edit := range extensionRules(4) {
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 _, b := range []struct {
f capsule.Format
base func() map[uint64]any
}{{capsule.Format1, base}, {capsule.Format2, base2}} {
m := b.base()
edit(m)
if _, err := capsule.DecodeControl(marshal(t, m), b.f); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
t.Errorf("format %d, %s: got %v", b.f, name, 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 _, err := capsule.DecodeControl([]byte("age-encryption.org/v1\n"), capsule.Format1); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
t.Fatalf("garbage control: %v", err)
}
if _, err := capsule.DecodeControl(marshal(t, base()), 4); err == nil || datekeys.Code(err) != "" {
t.Fatalf("format 4 is a caller error, not a normative one: %v", err)
}
// Format 3 exists since v0.10: a control of another schema version is
// the normative ERR_UNSUPPORTED_VERSION (spec §31, §69.1 layer 2).
if _, err := capsule.DecodeControl(marshal(t, base()), capsule.Format3); !errors.Is(err, datekeys.ErrUnsupportedVersion) {
t.Fatalf("a version 1 control in format 3: %v", 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
}
// Valid version 2 controls, at the limits, round-trip.
for _, c := range []capsule.Control{
{Padding: capsule.Bloque256},
{Padding: capsule.Reforzado, PayloadLength: 1<<32 + 1},
{Padding: capsule.Reforzado, PayloadLength: capsule.MaxPayloadLength},
} {
b, err := capsule.EncodeControl(&c, capsule.Format2)
if err != nil {
t.Fatal(err)
}
back, err := capsule.DecodeControl(b, capsule.Format2)
if err != nil || back.PayloadLength != c.PayloadLength || back.Padding != c.Padding {
t.Fatalf("%+v: got %+v, %v", c, back, err)
}
}
}
// Spec §31, §55.2: a version 2 control without extensions is 103 bytes,
// whatever L and the code, so that SEALED_CONTROL_LEN does not reveal L.
func TestControlLengthIsConstant(t *testing.T) {
for _, l := range []uint64{0, 23, 24, 255, 256, 65535, 65536, 1<<32 - 1, 1 << 32, capsule.MaxPayloadLength} {
for _, p := range []capsule.Padding{capsule.Bloque256, capsule.Reforzado} {
c := capsule.Control{PayloadLength: l, Padding: p}
for i := range c.PayloadIdentity {
c.PayloadIdentity[i], c.HeaderBinding[i] = byte(l), byte(i)
}
b, err := capsule.EncodeControl(&c, capsule.Format2)
if err != nil || len(b) != 103 {
t.Fatalf("L = %d, %s: %d bytes, %v", l, p, len(b), err)
}
}
}
// The layout of spec §31: ... 06 48 <L, 8 bytes> 07 <code>.
b, _ := capsule.EncodeControl(&capsule.Control{PayloadLength: 1<<32 + 1, Padding: capsule.Reforzado}, capsule.Format2)
if !bytes.HasSuffix(b, []byte{0x06, 0x48, 0, 0, 0, 1, 0, 0, 0, 1, 0x07, 0x02}) {
t.Fatalf("layout %x", b)
}
}
func TestHeaderLimit(t *testing.T) {
big, err := extension.New("org.example.big", 1, bytes.Repeat([]byte{1}, capsule.MaxPublicHeaderLen))
if err != nil {
t.Fatal(err)
}
opts := past(t, 1000)
opts.Noncritical = []extension.Extension{big}
var dkc bytes.Buffer
if _, err := capsule.Encrypt(&dkc, strings.NewReader("x"), opts); !errors.Is(err, datekeys.ErrIntegrity) || dkc.Len() != 0 {
t.Fatalf("PUBLIC_HEADER above 1 MiB accepted: %v", err)
}
// Spec §57: the encoder refuses it with the code the decoder uses.
if _, err := capsule.EncodeHeader(&capsule.Header{DateKey: datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000}, Noncritical: opts.Noncritical}); !errors.Is(err, datekeys.ErrIntegrity) {
t.Fatalf("EncodeHeader above 1 MiB: %v", err)
}
// Spec §57: the decoder applies the limit too, whatever the framing says.
if _, err := capsule.DecodeHeader(make([]byte, capsule.MaxPublicHeaderLen+1)); !errors.Is(err, datekeys.ErrIntegrity) {
t.Fatalf("DecodeHeader above 1 MiB: %v", err)
}
}
// A .dkk whose critical extension the application does not know is refused
// before any request.
func TestAccessKeyCriticalExtension(t *testing.T) {
f := loadFixture(t, "time_and_key_portable")
k := *f.dkk
k.Critical = []extension.Extension{{ID: "org.example.must-understand", Version: 1}}
o := f.openOptions(t)
o.AccessKey = &k
src := testkit.NewSource(f.release)
o.Source = src
if _, err := open(t, f.dkc, o); !errors.Is(err, datekeys.ErrExtensionCriticalUnknown) || src.Calls != 0 {
t.Fatalf("got %v (requests %d)", err, src.Calls)
}
o.Extensions = extension.Set{"org.example.must-understand": {1}}
if got, err := open(t, f.dkc, o); err != nil || !bytes.Equal(got, f.plaintext) {
t.Fatalf("known .dkk extension rejected: %v", err)
}
}
type brokenSeeker struct {
*bytes.Reader
seeks int
}
func (b *brokenSeeker) Seek(off int64, whence int) (int64, error) {
b.seeks++
if b.seeks > 1 {
return 0, errors.New("seek failed")
}
return b.Reader.Seek(off, whence)
}
func TestCapsuleDigestSeekFailure(t *testing.T) {
f := loadFixture(t, "time_and_key_portable")
r := &brokenSeeker{Reader: bytes.NewReader(f.dkc)}
if _, err := capsule.Open(context.Background(), io.Discard, r, f.openOptions(t)); err == nil {
t.Fatal("seek failure ignored")
}
}

Powered by TurnKey Linux.