package capsule_test import ( "bytes" "context" "encoding/binary" "errors" "os" "path/filepath" "strings" "testing" "time" "filippo.io/age" datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/accesskey" "g.activething.com/go/DateKeys/agewrap" "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" ) // The tests of this file check the precedence of errors of spec §69.1 and // the refinements of v0.8.2 recorded in spec §76: within one object the code // of the first failing layer, across objects and steps the order of §63. // failedStep returns the step of the last check, which failed, or 0 when // every check passed. func failedStep(t *testing.T, checks []capsule.CheckResult, err error) int { t.Helper() if err == nil { return 0 } if len(checks) == 0 || checks[len(checks)-1].OK { t.Fatalf("failure without a failed step: %v", err) } return checks[len(checks)-1].Step } // inspectStep runs steps 1 to 8 with the default registry. func inspectStep(t *testing.T, dkc []byte, exts extension.Registry) (int, error) { t.Helper() in, err := capsule.Inspect(bytes.NewReader(dkc), capsule.InspectOptions{Registry: testkit.Registry(), Extensions: exts}) return failedStep(t, in.Checks, err), err } // openStep runs the whole flow, with every release testkit knows, and also // returns the number of release requests. func openStep(t *testing.T, dkc []byte, o capsule.OpenOptions) (int, int, error) { t.Helper() src := testkit.NewSource() for _, r := range testkit.Rounds { src.Releases[r] = testkit.Release(r) } o.Source = src if o.Registry == nil { o.Registry = testkit.Registry() } if o.Now == nil { o.Now = testkit.Fixed(testkit.Genesis().AddDate(1, 0, 0)) } out, err := capsule.Open(context.Background(), discardWriter{}, bytes.NewReader(dkc), o) if out == nil { t.Fatalf("Open returned no result: %v", err) } return failedStep(t, out.Inspection.Checks, err), src.Calls, err } type discardWriter struct{} func (discardWriter) Write(p []byte) (int, error) { return len(p), nil } func expectStep(t *testing.T, name string, step int, err error, code error, wantStep int) { t.Helper() if !errors.Is(err, code) || step != wantStep { t.Errorf("%s: got %v at step %d, want %s at step %d", name, err, step, datekeys.Code(code), wantStep) } } // parts splits an official fixture and decodes its PUBLIC_HEADER map. func parts(t *testing.T, name string) (testkit.Parts, map[uint64]any) { t.Helper() p, err := testkit.Split(loadFixture(t, name).dkc) if err != nil { t.Fatal(err) } h, err := cbortest.UnmarshalMap(p.Header) if err != nil { t.Fatal(err) } return p, h } // rewriteHeader replaces the age header at the start of file with one built // from edit(stanzas). The MAC is computed with an unrelated key: steps 5, 6 // and 8 never verify it (spec §27). func rewriteHeader(t *testing.T, file []byte, edit func([]*age.Stanza) []*age.Stanza) []byte { t.Helper() stanzas, err := agewrap.Stanzas(bytes.NewReader(file)) if err != nil { t.Fatal(err) } n, err := testkit.HeaderLen(file) if err != nil { t.Fatal(err) } hdr, err := testkit.MarshalHeader(edit(stanzas), make([]byte, 16)) if err != nil { t.Fatal(err) } return append(hdr, file[n:]...) } // noStanzas replaces the age header at the start of file with the intro line // and the MAC line only: a header without recipient stanzas, which the age // grammar does not allow (header = v1-line 1*stanza end). func noStanzas(t *testing.T, file []byte) []byte { t.Helper() n, err := testkit.HeaderLen(file) if err != nil { t.Fatal(err) } hdr := "age-encryption.org/v1\n--- " + strings.Repeat("A", 43) + "\n" return append([]byte(hdr), file[n:]...) } // Spec §69.1, layers 2 to 4 of PUBLIC_HEADER at step 4: the type tag before // the version, the version before the CDDL, the CDDL before the fields with // codes of their own, and those in ascending key order: the DateKey (key 3) // and its pinned profile, then the critical extensions (key 5), where an // unknown one comes before invalid data. func TestPrecedenceWithinPublicHeader(t *testing.T) { p, h := parts(t, "time_only") unknown := []any{ext("com.example.unknown", 1)} unpinned := datekey.DateKey{ProfileID: "datekeys:other:v1", Round: 1000}.Compact() for _, tc := range []struct { name string edit func(m map[uint64]any) want error }{ {"type tag of another schema and version 2", func(m map[uint64]any) { m[0], m[1] = capsule.ControlTypeTag, uint64(2) }, datekeys.ErrNonCanonicalCBOR}, {"version 2, unknown key and invalid DateKey", func(m map[uint64]any) { m[1], m[3], m[9] = uint64(2), "dk1_x", uint64(0) }, datekeys.ErrUnsupportedVersion}, {"undefined access_policy and unknown critical extension", func(m map[uint64]any) { m[4], m[5] = uint64(2), unknown }, datekeys.ErrNonCanonicalCBOR}, {"DateKey as a byte string", func(m map[uint64]any) { m[3] = []byte(h[3].(string)) }, datekeys.ErrNonCanonicalCBOR}, {"invalid DateKey and unknown critical extension", func(m map[uint64]any) { m[3], m[5] = "dk1_x", unknown }, datekeys.ErrDateKeyInvalid}, {"unpinned profile and unknown critical extension", func(m map[uint64]any) { m[3], m[5] = unpinned, unknown }, datekeys.ErrUnknownProfile}, {"invalid data before an unknown extension", func(m map[uint64]any) { m[5] = []any{extData("org.example.a", []byte("ko")), ext("zz.unknown", 1)} }, datekeys.ErrExtensionCriticalUnknown}, {"invalid data alone", func(m map[uint64]any) { m[5] = []any{extData("org.example.a", []byte("ko"))} }, datekeys.ErrExtensionDataInvalid}, } { dkc := testkit.Reframe(p.Prelude, marshal(t, with(h, tc.edit)), p.Sealed, p.Payload) step, err := inspectStep(t, dkc, strictRegistry{}) expectStep(t, tc.name, step, err, tc.want, 4) } // The frame comes first, whatever the object holds (spec §57). big := append(marshal(t, with(h, func(m map[uint64]any) { m[1] = uint64(2) })), make([]byte, capsule.MaxPublicHeaderLen)...) if _, err := capsule.DecodeHeader(big); !errors.Is(err, datekeys.ErrIntegrity) { t.Errorf("PUBLIC_HEADER above 1 MiB with version 2: %v", err) } } // Spec §63, §69.1: across objects and steps the first step that fails // decides, and a check that relates an object to another belongs to its // step, not to the object's layer 4. func TestPrecedenceAcrossSteps(t *testing.T) { p, h := parts(t, "time_only") q := profile.Quicknet() beyond := datekey.DateKey{ProfileID: profile.QuicknetID, Round: q.MaxRound() + 1}.Compact() largest := datekey.DateKey{ProfileID: profile.QuicknetID, Round: datekey.MaxRound}.Compact() withDateKey := func(dk string) []byte { return marshal(t, with(h, func(m map[uint64]any) { m[3] = dk })) } badPolicy := marshal(t, with(h, func(m map[uint64]any) { m[4] = uint64(2) })) twoStanzas := rewriteHeader(t, p.Payload, func(s []*age.Stanza) []*age.Stanza { return append(s, s[0]) }) otherRound := rewriteHeader(t, p.Sealed, func(s []*age.Stanza) []*age.Stanza { s[0].Args[0] = "1001"; return s }) for _, tc := range []struct { name string dkc []byte want error step int }{ {"header fault and truncated SEALED_CONTROL", testkit.Reframe(p.Prelude, badPolicy, p.Sealed, nil)[:capsule.PreludeSize+len(badPolicy)+len(p.Sealed)/2], datekeys.ErrNonCanonicalCBOR, 4}, // Spec §15: the round time of a DateKey is at most // 9999-12-31T23:59:59Z, checked against the profile at step 7. {"round after 9999-12-31T23:59:59Z", testkit.Reframe(p.Prelude, withDateKey(beyond), p.Sealed, p.Payload), datekeys.ErrDateKeyInvalid, 7}, {"round 2^53-1 passes step 4 and fails step 7", testkit.Reframe(p.Prelude, withDateKey(largest), p.Sealed, p.Payload), datekeys.ErrDateKeyInvalid, 7}, {"round beyond the profile and malformed PAYLOAD_AGE", testkit.Reframe(p.Prelude, withDateKey(beyond), p.Sealed, []byte("not age")), datekeys.ErrIntegrity, 6}, {"tlock round mismatch and two PAYLOAD_AGE stanzas", testkit.Reframe(p.Prelude, p.Header, otherRound, twoStanzas), datekeys.ErrPolicyStructureMismatch, 6}, {"tlock round mismatch alone", testkit.Reframe(p.Prelude, p.Header, otherRound, p.Payload), datekeys.ErrRoundMismatch, 8}, } { step, err := inspectStep(t, tc.dkc, nil) expectStep(t, tc.name, step, err, tc.want, tc.step) } // The largest round of the profile is still valid (spec §15). last := datekey.DateKey{ProfileID: profile.QuicknetID, Round: q.MaxRound()} if u, err := datekey.RoundTime(q, last.Round); err != nil || u.Unix() > profile.MaxUnixTime || u.Unix()+3 <= profile.MaxUnixTime { t.Fatalf("last round %d at %v: %v", last.Round, u, err) } // The examples of spec §69.1 for format 2. precedenceFormat2(t) } // Spec §22, §57: PUBLIC_HEADER_LEN and SEALED_CONTROL_LEN are at least 1, and // PAYLOAD_AGE, which has no length field, is at least a well-formed age // header. func TestFrameLengthLowerBounds(t *testing.T) { p, _ := parts(t, "time_only") lengths := func(headerLen, sealedLen uint32, flags byte) []byte { pre := bytes.Clone(p.Prelude) pre[5] = flags binary.BigEndian.PutUint32(pre[8:12], headerLen) binary.BigEndian.PutUint32(pre[12:16], sealedLen) return testkit.Join(pre, p.Header, p.Sealed, p.Payload) } for _, tc := range []struct { name string dkc []byte want error step int }{ {"PUBLIC_HEADER_LEN 0", lengths(0, uint32(len(p.Sealed)), 0), datekeys.ErrIntegrity, 2}, {"SEALED_CONTROL_LEN 0", lengths(uint32(len(p.Header)), 0, 0), datekeys.ErrIntegrity, 2}, {"both 0 and FLAGS 1", lengths(0, 0, 1), datekeys.ErrInvalidFlags, 2}, {"empty PAYLOAD_AGE", testkit.Join(p.Prelude, p.Header, p.Sealed), datekeys.ErrIntegrity, 6}, } { step, err := inspectStep(t, tc.dkc, nil) expectStep(t, tc.name, step, err, tc.want, tc.step) } } // Spec §28.1: an age file whose header does not follow the age grammar, // including a header without stanzas, is malformed: ERR_INTEGRITY at step 5 // or 6. A well-formed header with the wrong stanzas is // ERR_POLICY_STRUCTURE_MISMATCH. The plaintext of OUTER_TIME_AGE is judged // against access_policy at step 12 (spec §36). func TestMalformedAgeHeaders(t *testing.T) { p, _ := parts(t, "time_only") for _, tc := range []struct { name string sealed, payload []byte want error step int }{ {"OUTER_TIME_AGE without stanzas", noStanzas(t, p.Sealed), p.Payload, datekeys.ErrIntegrity, 5}, {"PAYLOAD_AGE without stanzas", p.Sealed, noStanzas(t, p.Payload), datekeys.ErrIntegrity, 6}, {"two spaces between tlock arguments", bytes.Replace(p.Sealed, []byte("-> tlock 1000 "), []byte("-> tlock 1000 "), 1), p.Payload, datekeys.ErrIntegrity, 5}, {"CR at the end of the intro line", bytes.Replace(p.Sealed, []byte("v1\n"), []byte("v1\r\n"), 1), p.Payload, datekeys.ErrIntegrity, 5}, {"padding in the MAC line", bytes.Replace(p.Sealed, []byte("\n--- "), []byte("\n--- ="), 1), p.Payload, datekeys.ErrIntegrity, 5}, {"extra well-formed stanza in OUTER_TIME_AGE", rewriteHeader(t, p.Sealed, func(s []*age.Stanza) []*age.Stanza { return append(s, s[0]) }), p.Payload, datekeys.ErrPolicyStructureMismatch, 5}, } { step, err := inspectStep(t, testkit.Reframe(p.Prelude, p.Header, tc.sealed, tc.payload), nil) expectStep(t, tc.name, step, err, tc.want, tc.step) } // INNER_ACCESS_AGE is the authenticated plaintext of OUTER_TIME_AGE: a // header without stanzas there does not match time_and_key (step 12), // and an age file of any form does not match time_only. stranger := testkit.Stranger() empty := func(fk []byte, s []*age.Stanza) []*age.Stanza { return nil } for _, tc := range []struct { name string declared capsule.Policy }{ {"time_and_key sealing an age header without stanzas", capsule.TimeAndKey}, {"time_only sealing an age header without stanzas", capsule.TimeOnly}, } { b, err := testkit.Build{Declared: tc.declared, Structure: capsule.TimeAndKey, AccessRecipients: []age.Recipient{stranger.Recipient()}, EditInner: empty}.Make() if err != nil { t.Fatal(err) } step, calls, err := openStep(t, b.DKC, capsule.OpenOptions{Identities: []age.Identity{stranger}}) expectStep(t, tc.name, step, err, datekeys.ErrPolicyStructureMismatch, 12) if calls != 1 { t.Errorf("%s: %d release requests", tc.name, calls) } } // time_only: a plaintext that is not an age file is read as CONTROL_CBOR // at step 14. rec, err := agewrap.NewTimeRecipient(profile.Quicknet(), 1000) if err != nil { t.Fatal(err) } sealed, _, err := testkit.Encrypt([]byte{0xff}, rec) if err != nil { t.Fatal(err) } step, _, err := openStep(t, testkit.Reframe(p.Prelude, p.Header, sealed, p.Payload), capsule.OpenOptions{}) expectStep(t, "time_only sealing bytes that are not CBOR", step, err, datekeys.ErrNonCanonicalCBOR, 14) } // Spec §63 step 8: the tlock stanza has exactly two arguments; the first is // the canonical decimal of DateKey.round, the second the pinned chain_hash in // lowercase hexadecimal, both compared as strings and never parsed. func TestTlockStanzaArgumentComparison(t *testing.T) { p, _ := parts(t, "time_only") chain := profile.Quicknet().ChainHashHex() for _, tc := range []struct { name string args []string want error }{ {"canonical", []string{"1000", chain}, nil}, {"plus sign", []string{"+1000", chain}, datekeys.ErrRoundMismatch}, {"leading zero", []string{"01000", chain}, datekeys.ErrRoundMismatch}, {"exponent", []string{"1e3", chain}, datekeys.ErrRoundMismatch}, {"uppercase chain hash", []string{"1000", strings.ToUpper(chain)}, datekeys.ErrProfileMismatch}, {"prefixed chain hash", []string{"1000", "0x" + chain}, datekeys.ErrProfileMismatch}, {"round and chain hash both wrong", []string{"1001", strings.Repeat("0", 64)}, datekeys.ErrRoundMismatch}, {"one argument", []string{"1000"}, datekeys.ErrPolicyStructureMismatch}, {"three arguments, wrong round", []string{"1001", chain, "x"}, datekeys.ErrPolicyStructureMismatch}, } { sealed := rewriteHeader(t, p.Sealed, func(s []*age.Stanza) []*age.Stanza { s[0].Args = tc.args; return s }) step, err := inspectStep(t, testkit.Reframe(p.Prelude, p.Header, sealed, p.Payload), nil) if tc.want == nil { if err != nil { t.Errorf("%s: %v", tc.name, err) } continue } expectStep(t, tc.name, step, err, tc.want, 8) } } // Spec §63 step 9, §69.1: a .dkk offered for a time_and_key capsule is // checked as an object first, access_type and access_material (keys 4 and // 5) and then its critical extensions (key 7), and only then bound to the // capsule: capsule_id (key 3), then capsule_digest (key 6). No release is // requested. func TestAccessKeyCheckOrder(t *testing.T) { f := loadFixture(t, "time_and_key_portable") other := loadFixture(t, "time_and_key_recipients") unknown := []extension.Extension{{ID: "com.example.unknown", Version: 1}} wrongDigest := &accesskey.Verification{CapsuleDigest: make([]byte, 32)} for _, tc := range []struct { name string edit func(k *accesskey.AccessKey) want error }{ {"another capsule and an unknown critical extension", func(k *accesskey.AccessKey) { *k = *other.dkk; k.Critical = unknown }, datekeys.ErrExtensionCriticalUnknown}, {"unknown critical extension and capsule_digest mismatch", func(k *accesskey.AccessKey) { k.Critical, k.Verification = unknown, wrongDigest }, datekeys.ErrExtensionCriticalUnknown}, {"unsupported access_type and an unknown critical extension", func(k *accesskey.AccessKey) { k.Type, k.Critical = "mlkem768", unknown }, datekeys.ErrAccessInvalid}, {"another capsule", func(k *accesskey.AccessKey) { *k = *other.dkk }, datekeys.ErrAccessInvalid}, {"capsule_digest mismatch", func(k *accesskey.AccessKey) { k.Verification = wrongDigest }, datekeys.ErrAccessInvalid}, } { k := *f.dkk tc.edit(&k) step, calls, err := openStep(t, f.dkc, capsule.OpenOptions{AccessKey: &k, Now: testkit.Fixed(f.unlock(t))}) expectStep(t, tc.name, step, err, tc.want, 9) if calls != 0 { t.Errorf("%s: %d release requests", tc.name, calls) } } // Step 9.b before 9.c: without a credential the clock is not even // consulted, and a nil identity is not a credential. early := testkit.Fixed(f.unlock(t).Add(-time.Second)) for _, tc := range []struct { name string ids []age.Identity }{ {"no credential and the round time not reached", nil}, {"a nil identity and the round time not reached", []age.Identity{nil}}, } { step, calls, err := openStep(t, f.dkc, capsule.OpenOptions{Identities: tc.ids, Now: early}) expectStep(t, tc.name, step, err, datekeys.ErrAccessRequired, 9) if calls != 0 { t.Errorf("%s: %d release requests", tc.name, calls) } } // time_only: the credentials play no part (step 9). g := loadFixture(t, "time_only") k := *f.dkk k.Type, k.Critical = "mlkem768", unknown o := g.openOptions(t) o.AccessKey = &k if got, err := open(t, g.dkc, o); err != nil || !bytes.Equal(got, g.plaintext) { t.Fatalf("time_only with an invalid .dkk: %v", err) } } // Spec §63 step 9.a, §69.1: a .dkk handed over still encoded is decoded at // step 9.a, so that its errors, framing included, come after those of steps // 1 to 8 and never for a time_only capsule, whatever the reader does first. func TestAccessKeyFileAtStep9(t *testing.T) { f := loadFixture(t, "time_and_key_portable") raw, err := os.ReadFile(filepath.Join(fixtureDir, f.AccessKeyFile)) if err != nil { t.Fatal(err) } notDKK := []byte("not a .dkk") badVersion := bytes.Clone(raw) badVersion[4] = 2 now := testkit.Fixed(f.unlock(t)) for _, tc := range []struct { name string dkk []byte want error }{ {"not a .dkk", notDKK, datekeys.ErrInvalidMagic}, {"framing version 2", badVersion, datekeys.ErrUnsupportedVersion}, {"truncated body", raw[:len(raw)-1], datekeys.ErrIntegrity}, } { step, calls, err := openStep(t, f.dkc, capsule.OpenOptions{AccessKeyFile: bytes.NewReader(tc.dkk), Now: now}) expectStep(t, tc.name, step, err, tc.want, 9) if calls != 0 { t.Errorf("%s: %d release requests", tc.name, calls) } } // A failure of steps 1 to 8 comes first. broken := bytes.Clone(f.dkc) broken[5] = 1 step, _, err := openStep(t, broken, capsule.OpenOptions{AccessKeyFile: bytes.NewReader(notDKK), Now: now}) expectStep(t, "FLAGS 1 and not a .dkk", step, err, datekeys.ErrInvalidFlags, 2) // time_only never reads it. g := loadFixture(t, "time_only") o := g.openOptions(t) o.AccessKeyFile = bytes.NewReader(notDKK) if got, err := open(t, g.dkc, o); err != nil || !bytes.Equal(got, g.plaintext) { t.Fatalf("time_only with bytes that are not a .dkk: %v", err) } // The fixture key, still encoded, opens its capsule. o = f.openOptions(t) o.AccessKey, o.Identities = nil, nil o.AccessKeyFile = bytes.NewReader(raw) if got, err := open(t, f.dkc, o); err != nil || !bytes.Equal(got, f.plaintext) { t.Fatalf("encoded fixture .dkk: %v", err) } o.AccessKey = f.dkk if _, err := open(t, f.dkc, o); err == nil || datekeys.Code(err) != "" { t.Fatalf("both AccessKey and AccessKeyFile: %v", err) } } // Spec §63 steps 14 and 15: the critical extensions of CONTROL_CBOR are part // of the object (step 14); header_binding binds it to PUBLIC_HEADER at step // 15. func TestControlCriticalBeforeHeaderBinding(t *testing.T) { for _, c := range []struct { fixture string format capsule.Format }{{"time_only", capsule.Format1}, {"format2_time_only", capsule.Format2}} { p, _ := parts(t, c.fixture) b, err := testkit.Build{Format: c.format, ControlCritical: []extension.Extension{{ID: "com.example.unknown", Version: 1}}}.Make() if err != nil { t.Fatal(err) } // The control of b is bound to b's header, not to the fixture's. dkc := testkit.Reframe(p.Prelude, p.Header, b.Sealed, b.Payload) step, _, err := openStep(t, dkc, capsule.OpenOptions{}) expectStep(t, c.fixture+": unknown critical extension and header_binding mismatch", step, err, datekeys.ErrExtensionCriticalUnknown, 14) step, _, err = openStep(t, dkc, capsule.OpenOptions{Extensions: extension.Set{"com.example.unknown": {1}}}) expectStep(t, c.fixture+": header_binding mismatch alone", step, err, datekeys.ErrHeaderBinding, 15) } // Within CONTROL_CBOR, the CDDL comes before the critical extensions. c := map[uint64]any{0: capsule.ControlTypeTag, 1: uint64(1), 2: make([]byte, 32), 3: make([]byte, 32), 4: []any{ext("com.example.unknown", 1)}, 6: uint64(0)} if _, err := capsule.DecodeControl(marshal(t, c), capsule.Format1); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) { t.Errorf("unknown key 6 and an unknown critical extension: %v", err) } } // precedenceFormat2 checks the examples of spec §69.1 for format 2, as part // of TestPrecedenceAcrossSteps. Each capsule derives from a format 2 fixture, // sealed again with its known file keys, so that only the faults the example // names are present. func precedenceFormat2(t *testing.T) { to, err := testkit.LoadFixture(fixtureDir, "format2_time_only_extensions") if err != nil { t.Fatal(err) } tk, err := testkit.LoadFixture(fixtureDir, "format2_time_and_key_portable") if err != nil { t.Fatal(err) } unknown := []any{ext("com.example.unknown", 1)} control := func(edit func(m map[uint64]any)) testkit.Parts { t.Helper() in, err := to.WithControl(edit) if err != nil { t.Fatal(err) } p, err := testkit.Split(in.DKC) if err != nil { t.Fatal(err) } return p } // badPadding is the PAYLOAD_AGE of to with its last padding byte 0x01. content, err := os.ReadFile(filepath.Join(fixtureDir, to.PlaintextFile)) if err != nil { t.Fatal(err) } padded := append(bytes.Clone(content), make([]byte, to.PaddedLength-to.PayloadLength)...) padded[len(padded)-1] = 0x01 in, err := to.WithPayloadPlaintext(padded) if err != nil { t.Fatal(err) } withBadPadding, err := testkit.Split(in.DKC) if err != nil { t.Fatal(err) } badPadding := withBadPadding.Payload // otherHeader is the PUBLIC_HEADER of to with another capsule_id: as // valid, and bound to no control. h, err := cbortest.UnmarshalMap(to.Parts.Header) if err != nil { t.Fatal(err) } h[2] = bytes.Repeat([]byte{0x5a}, capsule.CapsuleIDSize) otherHeader := marshal(t, h) code3 := control(func(m map[uint64]any) { m[7] = uint64(3) }) for _, tc := range []struct { name string p testkit.Parts want error step int }{ {"CONTROL_CBOR of version 1 in a format 2 capsule, with an unknown key", control(func(m map[uint64]any) { m[1], m[9] = uint64(1), uint64(0) }), datekeys.ErrUnsupportedVersion, 14}, {"padding code 3 and an unknown critical extension in CONTROL_CBOR", control(func(m map[uint64]any) { m[7], m[4] = uint64(3), unknown }), datekeys.ErrNonCanonicalCBOR, 14}, {"padding code 3 and the header_binding of another header", testkit.Parts{Prelude: code3.Prelude, Header: otherHeader, Sealed: code3.Sealed, Payload: code3.Payload}, datekeys.ErrNonCanonicalCBOR, 14}, {"unknown critical CONTROL_CBOR extension and a padding byte other than 0x00", func() testkit.Parts { p := control(func(m map[uint64]any) { m[4] = unknown }) p.Payload = badPadding return p }(), datekeys.ErrExtensionCriticalUnknown, 14}, {"a padding byte other than 0x00 alone", withBadPadding, datekeys.ErrIntegrity, 17}, {"a padding byte other than 0x00 and an extra PAYLOAD_AGE stanza", testkit.Parts{Prelude: to.Parts.Prelude, Header: to.Parts.Header, Sealed: to.Parts.Sealed, Payload: rewriteHeader(t, badPadding, func(s []*age.Stanza) []*age.Stanza { return append(s, s[0]) })}, datekeys.ErrPolicyStructureMismatch, 6}, } { dkc := testkit.Reframe(tc.p.Prelude, tc.p.Header, tc.p.Sealed, tc.p.Payload) step, _, err := openStep(t, dkc, capsule.OpenOptions{Now: testkit.Fixed(to.Unlock)}) expectStep(t, tc.name, step, err, tc.want, tc.step) } // time_and_key: 15 stanzas and an identity that opens one fail at step // 12; a format 1 capsule relabeled 2 with its .dkk fails at step 9.a, // where its capsule_digest no longer matches. fifteen, err := tk.WithInnerStanzas(func(_ []byte, s []*age.Stanza) ([]*age.Stanza, error) { i := (*tk.AccessKeyStanza + 1) % len(s) return append(s[:i:i], s[i+1:]...), nil }) if err != nil { t.Fatal(err) } id, err := age.ParseX25519Identity(fifteen.Identities[0]) if err != nil { t.Fatal(err) } step, _, err := openStep(t, fifteen.DKC, capsule.OpenOptions{Identities: []age.Identity{id}, Now: testkit.Fixed(tk.Unlock)}) expectStep(t, "a format 2 capsule with 15 INNER_ACCESS_AGE stanzas and an identity that opens one", step, err, datekeys.ErrPolicyStructureMismatch, 12) f1 := loadFixture(t, "time_and_key_portable") relabeled := bytes.Clone(f1.dkc) relabeled[4] = 2 step, calls, err := openStep(t, relabeled, capsule.OpenOptions{AccessKey: f1.dkk, Now: testkit.Fixed(f1.unlock(t))}) expectStep(t, "a format 1 time_and_key capsule relabeled 2 and its .dkk, with capsule_digest", step, err, datekeys.ErrAccessInvalid, 9) if calls != 0 { t.Errorf("%d release requests", calls) } }