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/precedence_test.go

575 lines
24 KiB

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)
}
}

Powered by TurnKey Linux.