You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/internal/testkit/mutations.go

1309 lines
53 KiB

package testkit
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"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/profile"
"g.activething.com/go/DateKeys/provider"
)
// Numbers of mutations of spec §64: the first two lists apply to formats 1
// and 2, and the third one is of format 2. Its first entry, VERSION 4, is
// the format 2 case of "version changed", in the first list.
const (
SpecMutationsPerFormat = 33
Format2SpecMutations = 23
)
// Mutation is one entry of the mutation corpus (spec §64): a capsule, and
// the options to open it, that must fail with one exact normative error at
// one step of spec §63.
type Mutation struct {
Name string
// Spec is true for the mutations listed in spec §64: the thirty-three of
// its first two lists, once for each format, and those of its third list.
Spec bool
Want *datekeys.Error
Step int
// Network reports whether the failure may happen after a release was
// requested. Failures of steps 1 to 8 and of the access pre-checks must
// not cause any request (spec §27, §63).
Network bool
// Random reports that Make builds the capsule with fresh age
// randomness, so that its bytes differ on every call. The exported
// corpus freezes the bytes of the first build (MutationCorpus).
Random bool
Make func(e *MutationEnv) (*MutationInput, error)
}
// MutationInput is a mutated capsule and what the reader is given to open
// it.
type MutationInput struct {
// Base is the file name of the official fixture the capsule derives
// from, or "" for a capsule built from nothing.
Base string
DKC []byte
// DKK is the .dkk file offered, or nil.
DKK []byte
// Identities are the age X25519 identities offered, AGE-SECRET-KEY-1...
Identities []string
// Release is the only release the source knows: it answers every request
// with it. Nil means that no release is available.
Release *provider.Release
Now time.Time
// EmptyRegistry pins no profile; otherwise profile.Default is used.
EmptyRegistry bool
// Extensions are the extensions the application implements; nil knows
// none.
Extensions KnownExtensions
}
// KnownExtension is an extension an application implements, whose data is
// valid only when it equals ValidData.
type KnownExtension struct {
ID string
Version uint64
ValidData []byte
}
// KnownExtensions is an extension.Registry and extension.DataValidator.
type KnownExtensions []KnownExtension
func (k KnownExtensions) find(id string, version uint64) *KnownExtension {
for i := range k {
if k[i].ID == id && k[i].Version == version {
return &k[i]
}
}
return nil
}
// Known implements extension.Registry.
func (k KnownExtensions) Known(id string, version uint64) bool { return k.find(id, version) != nil }
// ValidateData implements extension.DataValidator.
func (k KnownExtensions) ValidateData(e extension.Extension) error {
x := k.find(e.ID, e.Version)
if x == nil {
return fmt.Errorf("extension %s version %d is not known", e.ID, e.Version)
}
if !bytes.Equal(x.ValidData, e.Data) {
return fmt.Errorf("data %x is not %x", e.Data, x.ValidData)
}
return nil
}
// singleSource answers every request with one release, or with
// ErrReleaseUnavailable, and counts the requests.
type singleSource struct {
release *provider.Release
calls int
}
func (s *singleSource) Fetch(context.Context, *profile.Profile, provider.Condition) (provider.Release, error) {
s.calls++
if s.release == nil {
return provider.Release{}, fmt.Errorf("testkit: no release: %w", datekeys.ErrReleaseUnavailable)
}
return *s.release, nil
}
// Verdict is how a reader rejected a capsule.
type Verdict struct {
Err error
Step int // the step of spec §63 that failed, 0 on success
// Calls is the number of release requests made.
Calls int
}
// Open opens the capsule with capsule.Open, as a reader given exactly the
// input would, from a seekable reader, and returns the verdict.
func (in *MutationInput) Open() (Verdict, error) {
reg := Registry()
if in.EmptyRegistry {
var err error
if reg, err = profile.NewRegistry(); err != nil {
return Verdict{}, err
}
}
src := &singleSource{release: in.Release}
o := capsule.OpenOptions{Registry: reg, Source: src, Now: Fixed(in.Now)}
if in.Extensions != nil {
o.Extensions = in.Extensions
}
if in.DKK != nil {
k, err := accesskey.Decode(bytes.NewReader(in.DKK))
if err != nil {
return Verdict{}, fmt.Errorf("testkit: the .dkk of a mutation must decode: %w", err)
}
defer k.Wipe()
o.AccessKey = k
}
for _, s := range in.Identities {
id, err := age.ParseX25519Identity(s)
if err != nil {
return Verdict{}, err
}
o.Identities = append(o.Identities, id)
}
o.Sink = DiscardSink{}
opened, err := capsule.Open(context.Background(), discard{}, bytes.NewReader(in.DKC), o)
v := Verdict{Err: err, Calls: src.calls}
if err == nil {
return v, nil
}
if opened == nil || len(opened.Inspection.Checks) == 0 {
return v, fmt.Errorf("testkit: Open failed without recording a step: %w", err)
}
checks := opened.Inspection.Checks
if last := checks[len(checks)-1]; !last.OK {
v.Step = last.Step
}
return v, nil
}
type discard struct{}
func (discard) Write(p []byte) (int, error) { return len(p), nil }
// LoadedFixture is an official .dkc fixture read from a fixture directory.
type LoadedFixture struct {
DKCFixture
DKC []byte
DKK []byte // the .dkk file, when the fixture has one
Parts Parts
Published provider.Release // the release the fixture opens with
Unlock time.Time
}
// LoadFixture reads the fixture name from dir.
func LoadFixture(dir, name string) (*LoadedFixture, error) {
f := &LoadedFixture{}
if err := ReadJSON(filepath.Join(dir, name+".json"), &f.DKCFixture); err != nil {
return nil, err
}
var err error
if f.DKC, err = os.ReadFile(filepath.Join(dir, f.File)); err != nil {
return nil, err
}
if f.Parts, err = Split(f.DKC); err != nil {
return nil, err
}
if f.AccessKeyFile != "" {
if f.DKK, err = os.ReadFile(filepath.Join(dir, f.AccessKeyFile)); err != nil {
return nil, err
}
}
sig, err := hex.DecodeString(f.Release.Signature)
if err != nil {
return nil, err
}
f.Published = provider.Release{Round: f.Release.Round, Signature: sig}
if f.Unlock, err = time.Parse(time.RFC3339, f.UnlockAt); err != nil {
return nil, err
}
return f, nil
}
// input returns dkc, derived from f, with the options that open f: its
// release, its unlock time and, for time_and_key, its .dkk.
func (f *LoadedFixture) input(dkc []byte) *MutationInput {
r := f.Published
in := &MutationInput{Base: f.File, DKC: dkc, Release: &r, Now: f.Unlock}
if f.AccessPolicy == capsule.TimeAndKey.String() {
in.DKK = f.DKK
}
return in
}
// MutationEnv holds what the mutations derive from.
type MutationEnv struct {
Dir string
// TimeOnly and TimeAndKey are the time_only and time_and_key_portable
// fixtures, of format 1. TimeOnly2, TimeAndKey2 and Extensions2 are
// format2_time_only, format2_time_and_key_portable and
// format2_time_only_extensions.
TimeOnly, TimeAndKey *LoadedFixture
TimeOnly2, TimeAndKey2, Extensions2 *LoadedFixture
siblings map[capsule.Format][]byte
}
// NewMutationEnv loads the fixtures the mutations derive from.
func NewMutationEnv(dir string) (*MutationEnv, error) {
e := &MutationEnv{Dir: dir, siblings: map[capsule.Format][]byte{}}
for _, l := range []struct {
to **LoadedFixture
name string
}{
{&e.TimeOnly, "time_only"}, {&e.TimeAndKey, "time_and_key_portable"},
{&e.TimeOnly2, "format2_time_only"}, {&e.TimeAndKey2, "format2_time_and_key_portable"},
{&e.Extensions2, "format2_time_only_extensions"},
} {
var err error
if *l.to, err = LoadFixture(dir, l.name); err != nil {
return nil, err
}
}
return e, nil
}
// timeOnly and timeAndKey return the time_only and time_and_key_portable
// fixtures of format f.
func (e *MutationEnv) timeOnly(f capsule.Format) *LoadedFixture {
if f == capsule.Format1 {
return e.TimeOnly
}
return e.TimeOnly2
}
func (e *MutationEnv) timeAndKey(f capsule.Format) *LoadedFixture {
if f == capsule.Format1 {
return e.TimeAndKey
}
return e.TimeAndKey2
}
// Sibling returns another time_only capsule of format f for round 1000,
// built once per environment and format with fresh randomness: by
// capsule.Encrypt in format 2, and by Build, a generator of test vectors, in
// format 1.
func (e *MutationEnv) Sibling(f capsule.Format) (Parts, error) {
if e.siblings[f] == nil {
content := []byte("sibling")
if f == capsule.Format1 {
b, err := Build{Format: capsule.Format1, Plaintext: content}.Make()
if err != nil {
return Parts{}, err
}
e.siblings[f] = b.DKC
} else {
var b bytes.Buffer
p := profile.Quicknet()
unlock, err := datekey.RoundTime(p, 1000)
if err != nil {
return Parts{}, err
}
opts := capsule.EncryptOptions{Profile: p, UnlockAt: unlock, Length: int64(len(content)), Now: Fixed(Genesis()), TestVectors: true}
if _, err := capsule.Encrypt(&b, bytes.NewReader(content), opts); err != nil {
return Parts{}, err
}
e.siblings[f] = b.Bytes()
}
}
return Split(e.siblings[f])
}
// Stranger returns a fixed X25519 identity that is not a recipient of any
// fixture: the key of the tests' third party. Its scalar is
// SHA-256("DateKeys test identity: stranger").
func Stranger() *age.X25519Identity {
raw := sha256.Sum256([]byte("DateKeys test identity: stranger"))
id, err := agewrap.X25519IdentityFromRaw(raw[:])
if err != nil {
panic(err)
}
return id
}
// MustUnderstand is the critical extension of the mutations that need one.
const MustUnderstand = "org.example.must-understand"
// mustUnderstandKnown is an application that knows MustUnderstand at version
// 1 and accepts only the data "ok".
var mustUnderstandKnown = KnownExtensions{{ID: MustUnderstand, Version: 1, ValidData: []byte("ok")}}
func set(b []byte, i int, v byte) []byte {
c := bytes.Clone(b)
c[i] = v
return c
}
func xorLast(b []byte) []byte {
c := bytes.Clone(b)
c[len(c)-1] ^= 0x01
return c
}
// built returns a capsule made by Build and the options that open it.
func built(b Build) (*MutationInput, error) {
if b.Plaintext == nil {
b.Plaintext = []byte("malicious creator")
}
out, err := b.Make()
if err != nil {
return nil, err
}
r := Release(1000)
return &MutationInput{DKC: out.DKC, Release: &r, Now: Genesis().AddDate(1, 0, 0)}, nil
}
func headerWithDateKey(to *LoadedFixture, dk string) (*MutationInput, error) {
h, err := capsule.DecodeHeader(to.Parts.Header)
if err != nil {
return nil, err
}
raw, err := RawHeader(h.CapsuleID, dk, 0)
if err != nil {
return nil, err
}
return to.input(Reframe(to.Parts.Prelude, raw, to.Parts.Sealed, to.Parts.Payload)), nil
}
// headerWithExtensions replaces the noncritical_extensions of the header of
// the time_only fixture to with exts, encoded as given.
func headerWithExtensions(to *LoadedFixture, exts []any) (*MutationInput, error) {
m, err := cbortest.UnmarshalMap(to.Parts.Header)
if err != nil {
return nil, err
}
m[6] = exts
h, err := cbortest.Marshal(m)
if err != nil {
return nil, err
}
return to.input(Reframe(to.Parts.Prelude, h, to.Parts.Sealed, to.Parts.Payload)), nil
}
// policyByte returns the offset of the access_policy value in a header
// without extensions, whose last entry is 0x04 <value>.
func policyByte(header []byte) (int, error) {
i := len(header) - 2
if header[i] != 0x04 {
return 0, fmt.Errorf("testkit: unexpected header layout %x", header[i:])
}
return i + 1, nil
}
func (f *LoadedFixture) withPolicy(policy byte) (*MutationInput, error) {
i, err := policyByte(f.Parts.Header)
if err != nil {
return nil, err
}
h := set(f.Parts.Header, i, policy)
return f.input(Join(f.Parts.Prelude, h, f.Parts.Sealed, f.Parts.Payload)), nil
}
// TimeFileKey returns FK_TIME, the file key of the OUTER_TIME_AGE of f,
// which the tlock stanza wraps and the release of f unwraps.
func (f *LoadedFixture) TimeFileKey() ([]byte, error) {
h, err := capsule.DecodeHeader(f.Parts.Header)
if err != nil {
return nil, err
}
stanzas, err := agewrap.Stanzas(bytes.NewReader(f.Parts.Sealed))
if err != nil {
return nil, err
}
id, err := agewrap.NewTimeIdentity(profile.Quicknet(), h.DateKey.Round, f.Published)
if err != nil {
return nil, err
}
return id.Unwrap(stanzas)
}
// WithTlockBody returns f with the body of its tlock stanza replaced by
// edit(body) and the header MAC of OUTER_TIME_AGE recomputed with FK_TIME.
// The age header stays authentic, as the creator, or anyone once the round is
// published, can make it: only the rules of the stanza body can reject the
// capsule (spec §63 step 11).
func (f *LoadedFixture) WithTlockBody(edit func(body []byte) ([]byte, error)) (*MutationInput, error) {
fk, err := f.TimeFileKey()
if err != nil {
return nil, err
}
var editErr error
sealed, err := RewriteAge(f.Parts.Sealed, fk, func(s []*age.Stanza) []*age.Stanza {
s[0].Body, editErr = edit(s[0].Body)
return s
})
if err := errors.Join(err, editErr); err != nil {
return nil, err
}
return f.input(Reframe(f.Parts.Prelude, f.Parts.Header, sealed, f.Parts.Payload)), nil
}
// EditU returns a tlock stanza body edit that replaces U, the G2 point that
// starts a Quicknet body U || V || W (spec §63 step 11), with edit(U).
func EditU(edit func(u []byte) ([]byte, error)) func(body []byte) ([]byte, error) {
const uLen = 2 * CoordinateLen
return func(body []byte) ([]byte, error) {
u, err := edit(bytes.Clone(body[:uLen]))
if err != nil {
return nil, err
}
return append(u, body[uLen:]...), nil
}
}
// withSignature sets the release of in to the release of f with its
// signature replaced by edit(signature).
func (f *LoadedFixture) withSignature(in *MutationInput, edit func(sig []byte) []byte) *MutationInput {
in.Release = &provider.Release{Round: f.Published.Round, Signature: edit(bytes.Clone(f.Published.Signature))}
return in
}
func mustUnderstand(data []byte) extension.Extension {
e, err := extension.New(MustUnderstand, 1, data)
if err != nil {
panic(err)
}
return e
}
func b64(s string) string { return base64.RawURLEncoding.EncodeToString([]byte(s)) }
// Mutations returns the mutation corpus: the thirty-three mutations of the
// first two lists of spec §64 on format 1 fixtures, further cases, the same
// thirty-three on format 2 fixtures, and the mutations of the third list of
// spec §64, of format 2, with further cases of their own.
func Mutations() []Mutation {
out := specMutations(capsule.Format1)
out = append(out, furtherMutations()...)
out = append(out, specMutations(capsule.Format2)...)
return append(out, format2Mutations()...)
}
// specMutations returns the thirty-three mutations of the first two lists of
// spec §64 on the fixtures of format f, or built in format f. Those of format
// 1 keep the names of spec v0.8.2; those of format 2 are prefixed "format 2: ".
func specMutations(f capsule.Format) []Mutation {
ok := func(in *MutationInput) (*MutationInput, error) { return in, nil }
name := func(s string) string {
if f == capsule.Format1 {
return s
}
return "format 2: " + s
}
build := func(b Build) (*MutationInput, error) {
b.Format = f
return built(b)
}
return []Mutation{
{Name: name("PUBLIC_HEADER_A + SEALED_CONTROL_B"), Spec: true, Want: datekeys.ErrHeaderBinding, Step: 15, Network: true, Random: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
b, err := e.Sibling(f)
if err != nil {
return nil, err
}
to := e.timeOnly(f)
return to.input(Reframe(to.Parts.Prelude, to.Parts.Header, b.Sealed, b.Payload)), nil
}},
{Name: name("SEALED_CONTROL_A + PAYLOAD_AGE_B"), Spec: true, Want: datekeys.ErrIntegrity, Step: 17, Network: true, Random: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
b, err := e.Sibling(f)
if err != nil {
return nil, err
}
to := e.timeOnly(f)
return to.input(Join(to.Parts.Prelude, to.Parts.Header, to.Parts.Sealed, b.Payload)), nil
}},
{Name: name("DateKey A + release of round B"), Spec: true, Want: datekeys.ErrRoundMismatch, Step: 10, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
to := e.timeOnly(f)
in := to.input(to.DKC)
r := Release(1001)
in.Release = &r
return in, nil
}},
{Name: name("chain hash changed"), Spec: true, Want: datekeys.ErrProfileMismatch, Step: 8,
Make: func(e *MutationEnv) (*MutationInput, error) {
to := e.timeOnly(f)
other := strings.Repeat("ab", 32)
return ok(to.input(bytes.Replace(to.DKC, []byte(profile.Quicknet().ChainHashHex()), []byte(other), 1)))
}},
// VERSION 4, which no format defines (spec §22, §23). In format 2 it is
// also the first entry of the third list of spec §64.
{Name: name("version changed"), Spec: true, Want: datekeys.ErrUnsupportedVersion, Step: 2,
Make: func(e *MutationEnv) (*MutationInput, error) {
return ok(e.timeOnly(f).input(set(e.timeOnly(f).DKC, 4, 4)))
}},
{Name: name("flags != 0"), Spec: true, Want: datekeys.ErrInvalidFlags, Step: 2,
Make: func(e *MutationEnv) (*MutationInput, error) {
return ok(e.timeOnly(f).input(set(e.timeOnly(f).DKC, 5, 0x80)))
}},
{Name: name("reserved != 0"), Spec: true, Want: datekeys.ErrInvalidFlags, Step: 2,
Make: func(e *MutationEnv) (*MutationInput, error) {
return ok(e.timeOnly(f).input(set(e.timeOnly(f).DKC, 7, 1)))
}},
{Name: name("payload truncated"), Spec: true, Want: datekeys.ErrIntegrity, Step: 17, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
to := e.timeOnly(f)
return ok(to.input(to.DKC[:len(to.DKC)-1]))
}},
{Name: name("payload age modified"), Spec: true, Want: datekeys.ErrIntegrity, Step: 17, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return ok(e.timeOnly(f).input(xorLast(e.timeOnly(f).DKC)))
}},
{Name: name("control modified"), Spec: true, Want: datekeys.ErrIntegrity, Step: 11, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
to := e.timeOnly(f)
p := to.Parts
return ok(to.input(Join(p.Prelude, p.Header, xorLast(p.Sealed), p.Payload)))
}},
{Name: name("non-canonical dk1_ JSON"), Spec: true, Want: datekeys.ErrDateKeyNonCanonical, Step: 4,
Make: func(e *MutationEnv) (*MutationInput, error) {
return headerWithDateKey(e.timeOnly(f), datekey.Prefix+b64(`{"version":1, "network":"datekeys:quicknet:v1", "round":1000}`))
}},
{Name: name("unknown profile"), Spec: true, Want: datekeys.ErrUnknownProfile, Step: 4,
Make: func(e *MutationEnv) (*MutationInput, error) {
return headerWithDateKey(e.timeOnly(f), datekey.DateKey{ProfileID: "datekeys:evmnet:v1", Round: 1000}.Compact())
}},
{Name: name("release of another round"), Spec: true, Want: datekeys.ErrReleaseInvalid, Step: 10, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
to := e.timeOnly(f)
in := to.input(to.DKC)
in.Release = &provider.Release{Round: 1000, Signature: Release(1001).Signature}
return in, nil
}},
{Name: name("access_policy=time_only with time_and_key structure"), Spec: true, Want: datekeys.ErrPolicyStructureMismatch, Step: 12, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) { return e.timeAndKey(f).withPolicy(0) }},
{Name: name("access_policy=time_and_key with time_only structure"), Spec: true, Want: datekeys.ErrPolicyStructureMismatch, Step: 12, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
in, err := e.timeOnly(f).withPolicy(1)
if err != nil {
return nil, err
}
in.Identities = []string{Stranger().String()}
return in, nil
}},
{Name: name("extra stanza in OUTER_TIME_AGE"), Spec: true, Want: datekeys.ErrPolicyStructureMismatch, Step: 5, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) {
return build(Build{EditOuter: func(fk []byte, s []*age.Stanza) []*age.Stanza {
extra, _, _ := X25519Stanza(fk)
return append(s, extra)
}})
}},
{Name: name("extra stanza in PAYLOAD_AGE"), Spec: true, Want: datekeys.ErrPolicyStructureMismatch, Step: 6, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) {
return build(Build{EditPayload: func(fk []byte, s []*age.Stanza) []*age.Stanza {
extra, _, _ := X25519Stanza(fk)
return append(s, extra)
}})
}},
// In format 2 the stanza of another type takes the place of a dummy,
// so that INNER_ACCESS_AGE keeps its 16 stanzas.
{Name: name("non-X25519 stanza in INNER_ACCESS_AGE"), Spec: true, Want: datekeys.ErrPolicyStructureMismatch, Step: 12, Network: true, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) {
in, err := build(Build{Declared: capsule.TimeAndKey, Structure: capsule.TimeAndKey,
AccessRecipients: []age.Recipient{Stranger().Recipient()},
EditInner: func(fk []byte, s []*age.Stanza) []*age.Stanza {
if f == capsule.Format2 {
s = s[:len(s)-1]
}
return append(s, &age.Stanza{Type: "scrypt", Args: []string{"c2FsdHNhbHRzYWx0c2FsdA", "10"}, Body: make([]byte, 32)})
}})
if err != nil {
return nil, err
}
in.Identities = []string{Stranger().String()}
return in, nil
}},
{Name: name("tlock stanza round differs from DateKey.round"), Spec: true, Want: datekeys.ErrRoundMismatch, Step: 8, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) {
return build(Build{EditOuter: func(_ []byte, s []*age.Stanza) []*age.Stanza { s[0].Args[0] = "1001"; return s }})
}},
{Name: name("tlock stanza chain hash differs from the pinned profile"), Spec: true, Want: datekeys.ErrProfileMismatch, Step: 8, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) {
return build(Build{EditOuter: func(_ []byte, s []*age.Stanza) []*age.Stanza {
s[0].Args[1] = "dbd506d6ef76e5f386f41c651dcb808c5bcbd75471cc4eafa3f4df7ad4e4c493" // drand default chain
return s
}})
}},
{Name: name("extension data of a type other than bstr"), Spec: true, Want: datekeys.ErrNonCanonicalCBOR, Step: 4,
Make: func(e *MutationEnv) (*MutationInput, error) {
// The v0.8.1 form of the time_only_extensions header: data as a text string.
return headerWithExtensions(e.timeOnly(f), []any{map[uint64]any{0: "org.example.label", 1: uint64(1), 2: "public label"}})
}},
{Name: name("empty extension data (h'')"), Spec: true, Want: datekeys.ErrNonCanonicalCBOR, Step: 4,
Make: func(e *MutationEnv) (*MutationInput, error) {
return headerWithExtensions(e.timeOnly(f), []any{map[uint64]any{0: "org.example.label", 1: uint64(1), 2: []byte{}}})
}},
{Name: name("65 extensions in one array"), Spec: true, Want: datekeys.ErrNonCanonicalCBOR, Step: 4,
Make: func(e *MutationEnv) (*MutationInput, error) {
exts := make([]any, 65)
for i := range exts {
exts[i] = map[uint64]any{0: fmt.Sprintf("org.example.%03d", i), 1: uint64(1)}
}
return headerWithExtensions(e.timeOnly(f), exts)
}},
// Canonical point encodings (spec §12.2). The U mutations keep the
// header MAC of OUTER_TIME_AGE valid, so that only the rules of the
// stanza body can reject them.
{Name: name("tlock stanza U re-encoded with c0 + p"), Spec: true, Want: datekeys.ErrIntegrity, Step: 11, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return e.timeOnly(f).WithTlockBody(EditU(func(u []byte) ([]byte, error) { return AddModulus(u, CoordinateLen) }))
}},
{Name: name("tlock stanza U is the point at infinity"), Spec: true, Want: datekeys.ErrIntegrity, Step: 11, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return e.timeOnly(f).WithTlockBody(EditU(func(u []byte) ([]byte, error) { return Infinity(len(u)), nil }))
}},
{Name: name("tlock stanza U with the infinity flag and a payload"), Spec: true, Want: datekeys.ErrIntegrity, Step: 11, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return e.timeOnly(f).WithTlockBody(EditU(func(u []byte) ([]byte, error) { return InfinityWithPayload(u), nil }))
}},
{Name: name("tlock stanza body of 127 bytes"), Spec: true, Want: datekeys.ErrIntegrity, Step: 11, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return e.timeOnly(f).WithTlockBody(func(b []byte) ([]byte, error) { return b[:len(b)-1], nil })
}},
{Name: name("tlock stanza body of 129 bytes"), Spec: true, Want: datekeys.ErrIntegrity, Step: 11, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return e.timeOnly(f).WithTlockBody(func(b []byte) ([]byte, error) { return append(bytes.Clone(b), 0), nil })
}},
// No fixture round has a signature whose x + p fits in 381 bits: the
// capsule is built for XPlusPRound, and opens with its canonical
// signature.
{Name: name("release signature re-encoded with x + p"), Spec: true, Want: datekeys.ErrReleaseInvalid, Step: 10, Network: true, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) {
in, err := build(Build{Round: XPlusPRound})
if err != nil {
return nil, err
}
r := Release(XPlusPRound)
if r.Signature, err = AddModulus(r.Signature, 0); err != nil {
return nil, err
}
in.Release = &r
return in, nil
}},
{Name: name("release signature is the point at infinity"), Spec: true, Want: datekeys.ErrReleaseInvalid, Step: 10, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
to := e.timeOnly(f)
return to.withSignature(to.input(to.DKC), func(sig []byte) []byte { return Infinity(len(sig)) }), nil
}},
{Name: name("release signature with the infinity flag and a payload"), Spec: true, Want: datekeys.ErrReleaseInvalid, Step: 10, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
to := e.timeOnly(f)
return to.withSignature(to.input(to.DKC), InfinityWithPayload), nil
}},
{Name: name("release signature negated"), Spec: true, Want: datekeys.ErrReleaseInvalid, Step: 10, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
to := e.timeOnly(f)
return to.withSignature(to.input(to.DKC), Negated), nil
}},
// Step 10 comes first: the negated signature is a canonical point
// that does not verify, so a reader must verify it before it reads U.
{Name: name("negated release signature and U re-encoded with c0 + p"), Spec: true, Want: datekeys.ErrReleaseInvalid, Step: 10, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
to := e.timeOnly(f)
in, err := to.WithTlockBody(EditU(func(u []byte) ([]byte, error) { return AddModulus(u, CoordinateLen) }))
if err != nil {
return nil, err
}
return to.withSignature(in, Negated), nil
}},
}
}
// furtherMutations returns further cases of capsule.TestMutationCorpus, on
// format 1 fixtures or built in format 1.
func furtherMutations() []Mutation {
ok := func(in *MutationInput) (*MutationInput, error) { return in, nil }
build := func(b Build) (*MutationInput, error) {
b.Format = capsule.Format1
return built(b)
}
return []Mutation{
{Name: "magic", Want: datekeys.ErrInvalidMagic, Step: 1,
Make: func(e *MutationEnv) (*MutationInput, error) { return ok(e.TimeOnly.input(set(e.TimeOnly.DKC, 0, 'X'))) }},
{Name: "a .dkk offered as a .dkc", Want: datekeys.ErrInvalidMagic, Step: 1,
Make: func(e *MutationEnv) (*MutationInput, error) {
return ok(e.TimeOnly.input(append([]byte("DKK1"), e.TimeOnly.DKC[4:]...)))
}},
{Name: "empty file", Want: datekeys.ErrInvalidMagic, Step: 1,
Make: func(e *MutationEnv) (*MutationInput, error) { return ok(e.TimeOnly.input([]byte{})) }},
{Name: "truncated prelude", Want: datekeys.ErrIntegrity, Step: 1,
Make: func(e *MutationEnv) (*MutationInput, error) { return ok(e.TimeOnly.input(e.TimeOnly.DKC[:10])) }},
{Name: "PUBLIC_HEADER_LEN above the limit", Want: datekeys.ErrIntegrity, Step: 2,
Make: func(e *MutationEnv) (*MutationInput, error) {
c := bytes.Clone(e.TimeOnly.DKC)
binary.BigEndian.PutUint32(c[8:12], capsule.MaxPublicHeaderLen+1)
return ok(e.TimeOnly.input(c))
}},
{Name: "SEALED_CONTROL_LEN above the limit", Want: datekeys.ErrIntegrity, Step: 2,
Make: func(e *MutationEnv) (*MutationInput, error) {
c := bytes.Clone(e.TimeOnly.DKC)
binary.BigEndian.PutUint32(c[12:16], capsule.MaxSealedControlLen+1)
return ok(e.TimeOnly.input(c))
}},
{Name: "truncated inside SEALED_CONTROL", Want: datekeys.ErrIntegrity, Step: 5,
Make: func(e *MutationEnv) (*MutationInput, error) {
p := e.TimeOnly.Parts
return ok(e.TimeOnly.input(e.TimeOnly.DKC[:len(p.Prelude)+len(p.Header)+10]))
}},
{Name: "header schema version changed", Want: datekeys.ErrUnsupportedVersion, Step: 4,
Make: func(e *MutationEnv) (*MutationInput, error) {
// a5 00 6a "datekeycap" 01 <version>
return ok(e.TimeOnly.input(set(e.TimeOnly.DKC, capsule.PreludeSize+14, 2)))
}},
{Name: "unknown key in PUBLIC_HEADER", Want: datekeys.ErrNonCanonicalCBOR, Step: 4,
Make: func(e *MutationEnv) (*MutationInput, error) {
p := e.TimeOnly.Parts
h := append(bytes.Clone(p.Header), 0x07, 0x00)
h[0]++ // one more map entry
return ok(e.TimeOnly.input(Reframe(p.Prelude, h, p.Sealed, p.Payload)))
}},
{Name: "undefined access_policy", Want: datekeys.ErrNonCanonicalCBOR, Step: 4,
Make: func(e *MutationEnv) (*MutationInput, error) { return e.TimeOnly.withPolicy(2) }},
// 256 and 257 end in the byte of a V1 policy: a check made after a
// narrowing to one byte would read them as time_only and time_and_key.
{Name: "access_policy 256 with a consistent header_binding", Want: datekeys.ErrNonCanonicalCBOR, Step: 4, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) { return build(Build{RawPolicy: 256}) }},
{Name: "access_policy 257 with a consistent header_binding", Want: datekeys.ErrNonCanonicalCBOR, Step: 4, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) { return build(Build{RawPolicy: 257}) }},
{Name: "unknown critical PUBLIC_HEADER extension", Want: datekeys.ErrExtensionCriticalUnknown, Step: 4, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) {
return build(Build{HeaderCritical: []extension.Extension{{ID: MustUnderstand, Version: 1}}})
}},
{Name: "unknown critical CONTROL_CBOR extension", Want: datekeys.ErrExtensionCriticalUnknown, Step: 14, Network: true, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) {
return build(Build{ControlCritical: []extension.Extension{{ID: MustUnderstand, Version: 1}}})
}},
{Name: "known critical PUBLIC_HEADER extension with invalid data", Want: datekeys.ErrExtensionDataInvalid, Step: 4, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) {
in, err := build(Build{HeaderCritical: []extension.Extension{mustUnderstand([]byte("ko"))}})
if err != nil {
return nil, err
}
in.Extensions = mustUnderstandKnown
return in, nil
}},
{Name: "known critical CONTROL_CBOR extension with invalid data", Want: datekeys.ErrExtensionDataInvalid, Step: 14, Network: true, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) {
in, err := build(Build{ControlCritical: []extension.Extension{mustUnderstand([]byte("ko"))}})
if err != nil {
return nil, err
}
in.Extensions = mustUnderstandKnown
return in, nil
}},
{Name: "known critical .dkk extension with invalid data", Want: datekeys.ErrExtensionDataInvalid, Step: 9,
Make: func(e *MutationEnv) (*MutationInput, error) {
tk := e.TimeAndKey
k, err := accesskey.Decode(bytes.NewReader(tk.DKK))
if err != nil {
return nil, err
}
defer k.Wipe()
k.Critical = []extension.Extension{mustUnderstand([]byte("ko"))}
var b bytes.Buffer
if err := accesskey.Encode(&b, k); err != nil {
return nil, err
}
in := tk.input(tk.DKC)
in.DKK, in.Extensions = b.Bytes(), mustUnderstandKnown
return in, nil
}},
{Name: "extension_version above 2^32-1", Want: datekeys.ErrNonCanonicalCBOR, Step: 4,
Make: func(e *MutationEnv) (*MutationInput, error) {
return headerWithExtensions(e.TimeOnly, []any{map[uint64]any{0: "org.example.label", 1: uint64(1) << 32}})
}},
{Name: "null extension data", Want: datekeys.ErrNonCanonicalCBOR, Step: 4,
Make: func(e *MutationEnv) (*MutationInput, error) {
return headerWithExtensions(e.TimeOnly, []any{map[uint64]any{0: "org.example.label", 1: uint64(1), 2: nil}})
}},
{Name: "time_and_key without credentials", Want: datekeys.ErrAccessRequired, Step: 9,
Make: func(e *MutationEnv) (*MutationInput, error) {
in := e.TimeAndKey.input(e.TimeAndKey.DKC)
in.DKK = nil
return in, nil
}},
{Name: ".dkk of another capsule", Want: datekeys.ErrAccessInvalid, Step: 9,
Make: func(e *MutationEnv) (*MutationInput, error) {
other, err := LoadFixture(e.Dir, "time_and_key_recipients")
if err != nil {
return nil, err
}
in := e.TimeAndKey.input(e.TimeAndKey.DKC)
in.DKK = other.DKK
return in, nil
}},
{Name: "capsule_digest of the .dkk does not match", Want: datekeys.ErrAccessInvalid, Step: 9,
Make: func(e *MutationEnv) (*MutationInput, error) { return ok(e.TimeAndKey.input(xorLast(e.TimeAndKey.DKC))) }},
{Name: "identity that is not a recipient", Want: datekeys.ErrAccessInvalid, Step: 13, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
in := e.TimeAndKey.input(e.TimeAndKey.DKC)
in.DKK, in.Identities = nil, []string{Stranger().String()}
return in, nil
}},
{Name: "round not reached yet", Want: datekeys.ErrReleaseUnavailable, Step: 9,
Make: func(e *MutationEnv) (*MutationInput, error) {
in := e.TimeOnly.input(e.TimeOnly.DKC)
in.Now = e.TimeOnly.Unlock.Add(-1)
return in, nil
}},
{Name: "release source unavailable", Want: datekeys.ErrReleaseUnavailable, Step: 9, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
in := e.TimeOnly.input(e.TimeOnly.DKC)
in.Release = nil
return in, nil
}},
{Name: "trailing data after PAYLOAD_AGE", Want: datekeys.ErrIntegrity, Step: 17, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return ok(e.TimeOnly.input(append(bytes.Clone(e.TimeOnly.DKC), 0)))
}},
{Name: "payload stanza body modified", Want: datekeys.ErrIntegrity, Step: 17, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
p := e.TimeOnly.Parts
n, err := HeaderLen(p.Payload)
if err != nil {
return nil, err
}
// Flip a byte of the wrapped file key: the last body line before "---".
i := bytes.LastIndex(p.Payload[:n], []byte("\n---")) - 10
c := byte('A')
if p.Payload[i] == 'A' {
c = 'B'
}
return ok(e.TimeOnly.input(Join(p.Prelude, p.Header, p.Sealed, set(p.Payload, i, c))))
}},
{Name: "tlock round edited by a third party", Want: datekeys.ErrRoundMismatch, Step: 8,
Make: func(e *MutationEnv) (*MutationInput, error) {
return ok(e.TimeOnly.input(bytes.Replace(e.TimeOnly.DKC, []byte("-> tlock 1000 "), []byte("-> tlock 1001 "), 1)))
}},
{Name: "empty registry", Want: datekeys.ErrUnknownProfile, Step: 4,
Make: func(e *MutationEnv) (*MutationInput, error) {
in := e.TimeOnly.input(e.TimeOnly.DKC)
in.EmptyRegistry = true
return in, nil
}},
{Name: "time_only declared, time_and_key built by the creator", Want: datekeys.ErrPolicyStructureMismatch, Step: 12, Network: true, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) {
return build(Build{Declared: capsule.TimeOnly, Structure: capsule.TimeAndKey, AccessRecipients: []age.Recipient{Stranger().Recipient()}})
}},
{Name: "time_and_key declared, time_only built by the creator", Want: datekeys.ErrPolicyStructureMismatch, Step: 12, Network: true, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) {
in, err := build(Build{Declared: capsule.TimeAndKey, Structure: capsule.TimeOnly})
if err != nil {
return nil, err
}
in.Identities = []string{Stranger().String()}
return in, nil
}},
{Name: "two INNER_ACCESS_AGE stanzas for one recipient", Want: datekeys.ErrPolicyStructureMismatch, Step: 13, Network: true, Random: true,
Make: func(*MutationEnv) (*MutationInput, error) {
stranger := Stranger()
in, err := build(Build{Declared: capsule.TimeAndKey, Structure: capsule.TimeAndKey,
AccessRecipients: []age.Recipient{stranger.Recipient()},
EditInner: func(fk []byte, s []*age.Stanza) []*age.Stanza {
again, _ := stranger.Recipient().Wrap(fk)
return append(s, again[0])
}})
if err != nil {
return nil, err
}
in.Identities = []string{stranger.String()}
return in, nil
}},
}
}
// relabeled returns the capsule of f with the VERSION of its PRELUDE, its
// format, set to v: anyone can edit that public byte (spec §22, §76).
func relabeled(f *LoadedFixture, v byte) *MutationInput { return f.input(set(f.DKC, 4, v)) }
// controlEdit returns the time_only fixture f with its control changed by
// edit and sealed again (LoadedFixture.WithControl).
func controlEdit(f *LoadedFixture, edit func(c map[uint64]any)) (*MutationInput, error) {
return f.WithControl(edit)
}
// paddedPlaintext returns the plaintext of the PAYLOAD_AGE of the format 2
// fixture f: its content followed by zeros up to its P.
func paddedPlaintext(f *LoadedFixture, dir string) ([]byte, error) {
content, err := os.ReadFile(filepath.Join(dir, f.PlaintextFile))
if err != nil {
return nil, err
}
return append(content, make([]byte, f.PaddedLength-uint64(len(content)))...), nil
}
// format2Mutations returns the mutations of the third list of spec §64,
// except VERSION 4, which is "format 2: version changed", and the companions
// of those that edit a time_and_key capsule: the same capsule with its .dkk,
// whose capsule_digest no longer matches, fails at step 9 (spec §43, §76).
// Every capsule derives from a fixture without randomness, sealed again with
// its known file keys and nonces (reseal.go).
func format2Mutations() []Mutation {
ok := func(in *MutationInput) (*MutationInput, error) { return in, nil }
stanzasOf := func(e *MutationEnv, edit func(e *MutationEnv, fk []byte, s []*age.Stanza) ([]*age.Stanza, error)) (*MutationInput, error) {
return e.TimeAndKey2.WithInnerStanzas(func(fk []byte, s []*age.Stanza) ([]*age.Stanza, error) { return edit(e, fk, s) })
}
// The stanza of the .dkk in INNER_ACCESS_AGE, which the edits keep.
dkkStanza := func(e *MutationEnv) int { return *e.TimeAndKey2.AccessKeyStanza }
dropDummy := func(e *MutationEnv, _ []byte, s []*age.Stanza) ([]*age.Stanza, error) {
i := (dkkStanza(e) + 1) % len(s)
return append(s[:i:i], s[i+1:]...), nil
}
addStanza := func(_ *MutationEnv, fk []byte, s []*age.Stanza) ([]*age.Stanza, error) {
extra, err := FixedX25519Stanza(fk, Stranger().Recipient(), "DateKeys mutation: 17 stanzas")
return append(s, extra), err
}
twice := func(e *MutationEnv, fk []byte, s []*age.Stanza) ([]*age.Stanza, error) {
id, err := e.TimeAndKey2.AccessIdentity()
if err != nil {
return nil, err
}
again, err := FixedX25519Stanza(fk, id.Recipient(), "DateKeys mutation: a second stanza for the .dkk")
if err != nil {
return nil, err
}
i := (dkkStanza(e) + 1) % len(s)
s[i] = again
return s, nil
}
withIdentity := func(f *LoadedFixture, in *MutationInput) (*MutationInput, error) { return f.withIdentity(in) }
companion := func(name string, mk func(e *MutationEnv) (*MutationInput, error), dkk func(e *MutationEnv) []byte) Mutation {
return Mutation{Name: name + ", with the .dkk", Want: datekeys.ErrAccessInvalid, Step: 9,
Make: func(e *MutationEnv) (*MutationInput, error) {
in, err := mk(e)
if err != nil {
return nil, err
}
in.DKK, in.Identities = dkk(e), nil
return in, nil
}}
}
dkk1 := func(e *MutationEnv) []byte { return e.TimeAndKey.DKK }
dkk2 := func(e *MutationEnv) []byte { return e.TimeAndKey2.DKK }
payload := func(e *MutationEnv, edit func(p []byte) []byte) (*MutationInput, error) {
p, err := paddedPlaintext(e.Extensions2, e.Dir)
if err != nil {
return nil, err
}
return e.Extensions2.WithPayloadPlaintext(edit(p))
}
relabel1 := func(e *MutationEnv) (*MutationInput, error) {
return withIdentity(e.TimeAndKey, relabeled(e.TimeAndKey, 2))
}
relabel2 := func(e *MutationEnv) (*MutationInput, error) {
return withIdentity(e.TimeAndKey2, relabeled(e.TimeAndKey2, 1))
}
fifteen := func(e *MutationEnv) (*MutationInput, error) { return stanzasOf(e, dropDummy) }
seventeen := func(e *MutationEnv) (*MutationInput, error) { return stanzasOf(e, addStanza) }
two := func(e *MutationEnv) (*MutationInput, error) { return stanzasOf(e, twice) }
return []Mutation{
// Format 3 exists since v0.10: VERSION 3 passes step 2, and the
// version 2 control fails at step 14.
{Name: "format 2 time_only relabeled format 3", Spec: true, Want: datekeys.ErrUnsupportedVersion, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) { return ok(relabeled(e.TimeOnly2, 3)) }},
{Name: "format 1 time_only relabeled format 2", Spec: true, Want: datekeys.ErrUnsupportedVersion, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) { return ok(relabeled(e.TimeOnly, 2)) }},
{Name: "format 1 time_and_key with one stanza relabeled format 2, with the identity", Spec: true, Want: datekeys.ErrPolicyStructureMismatch, Step: 12, Network: true, Make: relabel1},
{Name: "format 2 time_only relabeled format 1", Spec: true, Want: datekeys.ErrUnsupportedVersion, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) { return ok(relabeled(e.TimeOnly2, 1)) }},
{Name: "format 2 time_and_key relabeled format 1, with the identity", Spec: true, Want: datekeys.ErrUnsupportedVersion, Step: 14, Network: true, Make: relabel2},
{Name: "INNER_ACCESS_AGE with 15 stanzas", Spec: true, Want: datekeys.ErrPolicyStructureMismatch, Step: 12, Network: true, Make: fifteen},
{Name: "INNER_ACCESS_AGE with 17 stanzas", Spec: true, Want: datekeys.ErrPolicyStructureMismatch, Step: 12, Network: true, Make: seventeen},
{Name: "16 stanzas, two for one recipient, and the identity of that recipient", Spec: true, Want: datekeys.ErrPolicyStructureMismatch, Step: 13, Network: true, Make: two},
{Name: "identity that is not a recipient of any of the 16", Spec: true, Want: datekeys.ErrAccessInvalid, Step: 13, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
in := e.TimeAndKey2.input(e.TimeAndKey2.DKC)
in.DKK, in.Identities = nil, []string{Stranger().String()}
return in, nil
}},
{Name: "control of schema version 2 without key 6", Spec: true, Want: datekeys.ErrNonCanonicalCBOR, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return controlEdit(e.TimeOnly2, func(c map[uint64]any) { delete(c, 6) })
}},
{Name: "control of schema version 2 without key 7", Spec: true, Want: datekeys.ErrNonCanonicalCBOR, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return controlEdit(e.TimeOnly2, func(c map[uint64]any) { delete(c, 7) })
}},
{Name: "padding code 0", Spec: true, Want: datekeys.ErrNonCanonicalCBOR, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return controlEdit(e.TimeOnly2, func(c map[uint64]any) { c[7] = uint64(0) })
}},
{Name: "padding code 3", Spec: true, Want: datekeys.ErrNonCanonicalCBOR, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return controlEdit(e.TimeOnly2, func(c map[uint64]any) { c[7] = uint64(3) })
}},
{Name: "payload_length L_MAX + 1", Spec: true, Want: datekeys.ErrNonCanonicalCBOR, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return controlEdit(e.TimeOnly2, func(c map[uint64]any) { c[6] = payloadLength(capsule.MaxPayloadLength + 1) })
}},
{Name: "payload_length of 7 bytes", Spec: true, Want: datekeys.ErrNonCanonicalCBOR, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return controlEdit(e.TimeOnly2, func(c map[uint64]any) { c[6] = c[6].([]byte)[1:] })
}},
{Name: "payload_length of 9 bytes", Spec: true, Want: datekeys.ErrNonCanonicalCBOR, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return controlEdit(e.TimeOnly2, func(c map[uint64]any) { c[6] = append([]byte{0}, c[6].([]byte)...) })
}},
{Name: "payload_length as an unsigned integer", Spec: true, Want: datekeys.ErrNonCanonicalCBOR, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return controlEdit(e.TimeOnly2, func(c map[uint64]any) { c[6] = binary.BigEndian.Uint64(c[6].([]byte)) })
}},
{Name: "last padding byte not zero", Spec: true, Want: datekeys.ErrIntegrity, Step: 17, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return payload(e, func(p []byte) []byte { p[len(p)-1] = 0x01; return p })
}},
{Name: "payload plaintext of P - 1 bytes", Spec: true, Want: datekeys.ErrIntegrity, Step: 17, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return payload(e, func(p []byte) []byte { return p[:len(p)-1] })
}},
{Name: "payload plaintext of P + 256 bytes", Spec: true, Want: datekeys.ErrIntegrity, Step: 17, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return payload(e, func(p []byte) []byte { return append(p, make([]byte, 256)...) })
}},
{Name: "payload plaintext without padding, of L bytes", Spec: true, Want: datekeys.ErrIntegrity, Step: 17, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return payload(e, func(p []byte) []byte { return p[:e.Extensions2.PayloadLength] })
}},
{Name: "padding code 2 changed to 1, with L = 78000", Spec: true, Want: datekeys.ErrIntegrity, Step: 17, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return controlEdit(e.TimeOnly2, func(c map[uint64]any) { c[7] = uint64(capsule.Bloque256) })
}},
{Name: "payload_length L - 1, the last byte of the content not zero", Spec: true, Want: datekeys.ErrIntegrity, Step: 17, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) {
return controlEdit(e.TimeOnly2, func(c map[uint64]any) { c[6] = payloadLength(binary.BigEndian.Uint64(c[6].([]byte)) - 1) })
}},
companion("format 1 time_and_key with one stanza relabeled format 2", relabel1, dkk1),
companion("format 2 time_and_key relabeled format 1", relabel2, dkk2),
companion("INNER_ACCESS_AGE with 15 stanzas", fifteen, dkk2),
companion("INNER_ACCESS_AGE with 17 stanzas", seventeen, dkk2),
companion("16 stanzas, two for one recipient", two, dkk2),
}
}
// Check opens in and compares the verdict with the mutation's expectation.
func (m Mutation) Check(in *MutationInput) error {
v, err := in.Open()
if err != nil {
return err
}
return m.checkVerdict(v)
}
func (m Mutation) checkVerdict(v Verdict) error {
switch {
case v.Err == nil:
return errors.New("mutation accepted")
case !errors.Is(v.Err, m.Want):
return fmt.Errorf("got %v, want %v", v.Err, m.Want)
case v.Step != m.Step:
return fmt.Errorf("failed at step %d, want step %d: %v", v.Step, m.Step, v.Err)
case !m.Network && v.Calls != 0:
return fmt.Errorf("an invalid capsule caused %d release requests", v.Calls)
}
return nil
}
// ---------------------------------------------------------------------------
// Exported corpus: testdata/vectors/mutations.json
// MutationFile is testdata/vectors/mutations.json.
type MutationFile struct {
Spec string `json:"spec"`
Description string `json:"description"`
Cases []MutationCase `json:"cases"`
}
// MutationCase is one mutation as frozen data.
type MutationCase struct {
Name string `json:"name"`
// Spec is true for the mutations listed in spec §64.
Spec bool `json:"spec"`
DKC EditedFile `json:"dkc"`
// DKK is the hex of the .dkk offered, absent for none.
DKK string `json:"dkk,omitempty"`
Identities []string `json:"identities,omitempty"`
// Release is the only release the source serves, for any requested
// round; null when no release is available.
Release *FixtureRelease `json:"release"`
// Now is the reader's clock, RFC 3339.
Now string `json:"now"`
// Registry is "default" (the Quicknet profile pinned) or "empty".
Registry string `json:"registry"`
Extensions []KnownExtensionRecord `json:"extensions,omitempty"`
// Network reports whether a release may be requested before the
// failure; when false the reader must fail without any request.
Network bool `json:"network"`
// Frozen reports that the capsule was built once with age randomness;
// its bytes are kept and never regenerated.
Frozen bool `json:"frozen"`
Error string `json:"error"`
Step int `json:"step"`
}
// EditedFile is a file given as edits of a base fixture.
type EditedFile struct {
// Base is the file name of an official fixture in testdata/fixtures, or
// absent for the empty file.
Base string `json:"base,omitempty"`
Edits []Edit `json:"edits"`
}
// KnownExtensionRecord is a KnownExtension in JSON.
type KnownExtensionRecord struct {
ID string `json:"id"`
Version uint64 `json:"version"`
ValidData string `json:"valid_data"`
}
func (f EditedFile) bytes(dir string) ([]byte, error) {
var base []byte
if f.Base != "" {
var err error
if base, err = os.ReadFile(filepath.Join(dir, f.Base)); err != nil {
return nil, err
}
}
return ApplyEdits(base, f.Edits)
}
// Input rebuilds the input of the case with the fixtures of dir.
func (c *MutationCase) Input(dir string) (*MutationInput, error) {
dkc, err := c.DKC.bytes(dir)
if err != nil {
return nil, err
}
in := &MutationInput{Base: c.DKC.Base, DKC: dkc, Identities: c.Identities, EmptyRegistry: c.Registry == "empty"}
if c.Registry != "default" && c.Registry != "empty" {
return nil, fmt.Errorf("testkit: unknown registry %q", c.Registry)
}
if c.DKK != "" {
if in.DKK, err = hex.DecodeString(c.DKK); err != nil {
return nil, err
}
}
if c.Release != nil {
sig, err := hex.DecodeString(c.Release.Signature)
if err != nil {
return nil, err
}
in.Release = &provider.Release{Round: c.Release.Round, Signature: sig}
}
if in.Now, err = time.Parse(time.RFC3339Nano, c.Now); err != nil {
return nil, err
}
for _, x := range c.Extensions {
data, err := hex.DecodeString(x.ValidData)
if err != nil {
return nil, err
}
in.Extensions = append(in.Extensions, KnownExtension{ID: x.ID, Version: x.Version, ValidData: data})
}
return in, nil
}
// Check opens the input of the case and compares the verdict with the
// recorded one.
func (c *MutationCase) Check(dir string) error {
in, err := c.Input(dir)
if err != nil {
return err
}
v, err := in.Open()
if err != nil {
return err
}
if got := Result(v.Err); got != c.Error || v.Step != c.Step {
return fmt.Errorf("got %s at step %d, want %s at step %d (%v)", got, v.Step, c.Error, c.Step, v.Err)
}
if !c.Network && v.Calls != 0 {
return fmt.Errorf("an invalid capsule caused %d release requests", v.Calls)
}
return nil
}
func (m Mutation) record(in *MutationInput, dir string, v Verdict) (MutationCase, error) {
c := MutationCase{
Name: m.Name, Spec: m.Spec, Identities: in.Identities, Now: in.Now.UTC().Format(time.RFC3339Nano),
Registry: "default", Network: m.Network, Frozen: m.Random, Error: Result(v.Err), Step: v.Step,
}
if in.EmptyRegistry {
c.Registry = "empty"
}
c.DKC.Edits = Splice(nil, in.DKC)
if in.Base != "" && !m.Random {
base, err := os.ReadFile(filepath.Join(dir, in.Base))
if err != nil {
return c, err
}
c.DKC = EditedFile{Base: in.Base, Edits: Splice(base, in.DKC)}
}
if c.DKC.Edits == nil {
c.DKC.Edits = []Edit{}
}
if in.DKK != nil {
c.DKK = hex.EncodeToString(in.DKK)
}
if in.Release != nil {
c.Release = &FixtureRelease{Round: in.Release.Round, Signature: hex.EncodeToString(in.Release.Signature)}
}
for _, x := range in.Extensions {
c.Extensions = append(c.Extensions, KnownExtensionRecord{ID: x.ID, Version: x.Version, ValidData: hex.EncodeToString(x.ValidData)})
}
return c, nil
}
// MutationCorpus computes testdata/vectors/mutations.json from the fixtures
// of dir. The capsules of the Random mutations are taken from frozen, the
// file as committed, when it records them, and built afresh otherwise. Every
// case is opened, and the file records the verdict; a verdict other than the
// one the mutation is written for is an error.
func MutationCorpus(dir string, frozen *MutationFile) (MutationFile, error) {
f := MutationFile{
Spec: SpecVersion,
Description: "Mutation corpus of spec §64 and further cases of capsule.TestMutationCorpus, generated by the reference implementation: " +
"each case is a .dkc and what the reader is given, with the normative error and the step of spec §63 at which capsule.Open fails. See testdata/README.md.",
}
env, err := NewMutationEnv(dir)
if err != nil {
return f, err
}
old := map[string]*MutationCase{}
if frozen != nil {
for i := range frozen.Cases {
old[frozen.Cases[i].Name] = &frozen.Cases[i]
}
}
for _, m := range Mutations() {
var in *MutationInput
if c := old[m.Name]; m.Random && c != nil && c.Frozen {
in, err = c.Input(dir)
} else {
in, err = m.Make(env)
}
if err != nil {
return f, fmt.Errorf("mutation %q: %w", m.Name, err)
}
v, err := in.Open()
if err == nil {
err = m.checkVerdict(v)
}
if err != nil {
return f, fmt.Errorf("mutation %q: %w", m.Name, err)
}
c, err := m.record(in, dir, v)
if err != nil {
return f, err
}
f.Cases = append(f.Cases, c)
}
return f, nil
}

Powered by TurnKey Linux.