|
|
|
|
package capsule_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"maps"
|
|
|
|
|
"slices"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
|
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
|
|
|
"g.activething.com/go/DateKeys/datekey"
|
|
|
|
|
"g.activething.com/go/DateKeys/internal/cbortest"
|
|
|
|
|
"g.activething.com/go/DateKeys/profile"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// swapped encodes m with the entries of keys a and b in each other's place.
|
|
|
|
|
func swapped(t *testing.T, m map[uint64]any, a, b uint64) []byte {
|
|
|
|
|
t.Helper()
|
|
|
|
|
var p cbortest.Pairs
|
|
|
|
|
for _, k := range slices.Sorted(maps.Keys(m)) {
|
|
|
|
|
switch k {
|
|
|
|
|
case a:
|
|
|
|
|
k = b
|
|
|
|
|
case b:
|
|
|
|
|
k = a
|
|
|
|
|
}
|
|
|
|
|
p = append(p, k, m[k])
|
|
|
|
|
}
|
|
|
|
|
out, err := cbortest.Marshal(p)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func with(m map[uint64]any, edit func(m map[uint64]any)) map[uint64]any {
|
|
|
|
|
c := maps.Clone(m)
|
|
|
|
|
edit(c)
|
|
|
|
|
return c
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spec §58: the maps of PUBLIC_HEADER and CONTROL_CBOR are closed, their keys
|
|
|
|
|
// strictly ascending unsigned integers, and every required key is present.
|
|
|
|
|
func TestDecodeMapStructure(t *testing.T) {
|
|
|
|
|
dk := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000}.Compact()
|
|
|
|
|
h := map[uint64]any{0: capsule.HeaderTypeTag, 1: uint64(1), 2: make([]byte, 16), 3: dk, 4: uint64(0)}
|
|
|
|
|
c := map[uint64]any{0: capsule.ControlTypeTag, 1: uint64(1), 2: make([]byte, 32), 3: make([]byte, 32)}
|
|
|
|
|
exts := []any{ext("a", 1)}
|
|
|
|
|
for name, in := range map[string][]byte{
|
|
|
|
|
"missing capsule_id": marshal(t, with(h, func(m map[uint64]any) { delete(m, 2) })),
|
|
|
|
|
"missing access_policy": marshal(t, with(h, func(m map[uint64]any) { delete(m, 4) })),
|
|
|
|
|
"capsule_id as text": marshal(t, with(h, func(m map[uint64]any) { m[2] = "x" })),
|
|
|
|
|
"access_policy 2^53": marshal(t, with(h, func(m map[uint64]any) { m[4] = uint64(1) << 53 })),
|
|
|
|
|
"unknown key 7": marshal(t, with(h, func(m map[uint64]any) { m[7] = uint64(0) })),
|
|
|
|
|
"eight entries": marshal(t, with(h, func(m map[uint64]any) { m[5], m[6], m[7] = exts, []any{ext("b", 1)}, uint64(0) })),
|
|
|
|
|
"keys 3 and 4 swapped": swapped(t, h, 3, 4),
|
|
|
|
|
"text key": mustMarshal(t, cbortest.Pairs{uint64(0), capsule.HeaderTypeTag, uint64(1), uint64(1), "2", make([]byte, 16)}),
|
|
|
|
|
} {
|
|
|
|
|
if _, err := capsule.DecodeHeader(in); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
|
|
|
t.Errorf("PUBLIC_HEADER %s: %v", name, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// access_policy is 0 or 1 (spec §25): a value whose low byte is 0 or 1
|
|
|
|
|
// is not a V1 policy.
|
|
|
|
|
for _, p := range []uint64{2, 255, 256, 257, 512, 65536, 65537, 1 << 32, 1<<32 + 1, 1<<53 - 256, 1<<53 - 255, 1<<53 - 1} {
|
|
|
|
|
if _, err := capsule.DecodeHeader(marshal(t, with(h, func(m map[uint64]any) { m[4] = p }))); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
|
|
|
t.Errorf("PUBLIC_HEADER access_policy %d: %v", p, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for name, in := range map[string][]byte{
|
|
|
|
|
"missing payload_identity": marshal(t, with(c, func(m map[uint64]any) { delete(m, 3) })),
|
|
|
|
|
"seven entries": marshal(t, with(c, func(m map[uint64]any) { m[4], m[5], m[6] = exts, []any{ext("b", 1)}, uint64(0) })),
|
|
|
|
|
"keys 2 and 3 swapped": swapped(t, c, 2, 3),
|
|
|
|
|
"text key": mustMarshal(t, cbortest.Pairs{uint64(0), capsule.ControlTypeTag, uint64(1), uint64(1), "2", 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
|
|
|
if _, err := capsule.DecodeControl(in, capsule.Format1); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
|
|
|
t.Errorf("CONTROL_CBOR %s: %v", name, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func mustMarshal(t *testing.T, v any) []byte {
|
|
|
|
|
t.Helper()
|
|
|
|
|
b, err := cbortest.Marshal(v)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spec §70: the schema version is reported as such whatever follows it, and
|
|
|
|
|
// a version that is missing or outside the profile is not a version.
|
|
|
|
|
func TestDecodeSchemaVersion(t *testing.T) {
|
|
|
|
|
for name, tc := range map[string]struct {
|
|
|
|
|
in cbortest.Pairs
|
|
|
|
|
want error
|
|
|
|
|
}{
|
|
|
|
|
"version 2, rest unreadable": {cbortest.Pairs{uint64(0), capsule.HeaderTypeTag, uint64(1), uint64(2), uint64(2), cbortest.Raw{0xff}}, datekeys.ErrUnsupportedVersion},
|
|
|
|
|
"version missing": {cbortest.Pairs{uint64(0), capsule.HeaderTypeTag, uint64(2), make([]byte, 16)}, datekeys.ErrNonCanonicalCBOR},
|
|
|
|
|
"version null": {cbortest.Pairs{uint64(0), capsule.HeaderTypeTag, uint64(1), nil}, datekeys.ErrNonCanonicalCBOR},
|
|
|
|
|
"version before type": {cbortest.Pairs{uint64(1), uint64(1), uint64(0), capsule.HeaderTypeTag}, datekeys.ErrNonCanonicalCBOR},
|
|
|
|
|
} {
|
|
|
|
|
if _, err := capsule.DecodeHeader(mustMarshal(t, tc.in)); !errors.Is(err, tc.want) {
|
|
|
|
|
t.Errorf("%s: got %v, want %v", name, err, tc.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A PUBLIC_HEADER that breaks the CDDL and holds an invalid DateKey reports
|
|
|
|
|
// the CDDL violation: the DateKey is parsed after the map (spec §57, §63
|
|
|
|
|
// step 4).
|
|
|
|
|
func TestDecodeHeaderReportsTheCDDLFirst(t *testing.T) {
|
|
|
|
|
h := map[uint64]any{0: capsule.HeaderTypeTag, 1: uint64(1), 2: make([]byte, 16), 3: "dk1_x", 4: uint64(0)}
|
|
|
|
|
if _, err := capsule.DecodeHeader(marshal(t, h)); !errors.Is(err, datekeys.ErrDateKeyInvalid) {
|
|
|
|
|
t.Fatalf("invalid DateKey alone: %v", err)
|
|
|
|
|
}
|
|
|
|
|
for name, edit := range map[string]func(m map[uint64]any){
|
|
|
|
|
"undefined access_policy": func(m map[uint64]any) { m[4] = uint64(2) },
|
|
|
|
|
"access_policy 256": func(m map[uint64]any) { m[4] = uint64(256) },
|
|
|
|
|
"extensions out of order": func(m map[uint64]any) { m[6] = []any{ext("b", 1), ext("a", 1)} },
|
|
|
|
|
"id in both arrays": func(m map[uint64]any) { m[5], m[6] = []any{ext("a", 1)}, []any{ext("a", 1)} },
|
|
|
|
|
} {
|
|
|
|
|
if _, err := capsule.DecodeHeader(marshal(t, with(h, edit))); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
|
|
|
t.Errorf("%s and an invalid DateKey: %v", name, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|