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.
1383 lines
56 KiB
1383 lines
56 KiB
package testkit
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"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
|
|
// Verdicts, when not nil, are the verdicts of a format 3 capsule that
|
|
// opens without a code: the mutations of security, which never decides
|
|
// the opening (spec §29.3, §64). Want is then nil and Step 0.
|
|
Verdicts *capsule.Verdicts
|
|
Make func(e *MutationEnv) (*MutationInput, error)
|
|
}
|
|
|
|
// Code is the result the mutation is written for: the code of Want, or
|
|
// ResultOK for a capsule that opens.
|
|
func (m Mutation) Code() string {
|
|
if m.Want == nil {
|
|
return ResultOK
|
|
}
|
|
return m.Want.Code()
|
|
}
|
|
|
|
// 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
|
|
// Verdicts are those of a format 3 capsule that opens.
|
|
Verdicts capsule.Verdicts
|
|
}
|
|
|
|
// 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 {
|
|
v.Verdicts = opened.Verdicts
|
|
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. TimeOnly3, TimeAndKey3 and Tree3 are
|
|
// format3_single, format3_time_and_key_portable and format3_tree; Signed3,
|
|
// Unsigned3 and Note3 are format3_signed, format3_unsigned and
|
|
// format3_note, of spec v0.11.
|
|
TimeOnly, TimeAndKey *LoadedFixture
|
|
TimeOnly2, TimeAndKey2, Extensions2 *LoadedFixture
|
|
TimeOnly3, TimeAndKey3, Tree3 *LoadedFixture
|
|
Signed3, Unsigned3, Note3 *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"},
|
|
{&e.TimeOnly3, "format3_single"}, {&e.TimeAndKey3, "format3_time_and_key_portable"}, {&e.Tree3, "format3_tree"},
|
|
{&e.Signed3, "format3_signed"}, {&e.Unsigned3, "format3_unsigned"}, {&e.Note3, "format3_note"},
|
|
} {
|
|
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 {
|
|
switch f {
|
|
case capsule.Format1:
|
|
return e.TimeOnly
|
|
case capsule.Format2:
|
|
return e.TimeOnly2
|
|
}
|
|
return e.TimeOnly3
|
|
}
|
|
|
|
func (e *MutationEnv) timeAndKey(f capsule.Format) *LoadedFixture {
|
|
switch f {
|
|
case capsule.Format1:
|
|
return e.TimeAndKey
|
|
case capsule.Format2:
|
|
return e.TimeAndKey2
|
|
}
|
|
return e.TimeAndKey3
|
|
}
|
|
|
|
// Sibling returns another time_only capsule of format f for round 1000,
|
|
// built once per environment and format with fresh randomness: by
|
|
// capsule.EncryptFiles in format 3, and in formats 2 and 1 by capsule.Encrypt
|
|
// and Build, as a generator of test vectors.
|
|
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 if f == capsule.Format3 {
|
|
var b bytes.Buffer
|
|
p := profile.Quicknet()
|
|
unlock, err := datekey.RoundTime(p, 1000)
|
|
if err != nil {
|
|
return Parts{}, err
|
|
}
|
|
src := capsule.Source{Path: "sibling.txt", Size: int64(len(content)), Open: func() (io.ReadCloser, error) {
|
|
return io.NopCloser(bytes.NewReader(content)), nil
|
|
}}
|
|
if _, err := capsule.EncryptFiles(&b, []capsule.Source{src}, capsule.EncryptOptions{Profile: p, UnlockAt: unlock, Now: Fixed(Genesis())}); err != nil {
|
|
return Parts{}, err
|
|
}
|
|
e.siblings[f] = b.Bytes()
|
|
} 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, the mutations of the list of format 2
|
|
// of spec §64 with further cases of their own, the thirty-three on format 3
|
|
// fixtures, and the mutations of the list of format 3 with further cases.
|
|
func Mutations() []Mutation {
|
|
out := specMutations(capsule.Format1)
|
|
out = append(out, furtherMutations()...)
|
|
out = append(out, specMutations(capsule.Format2)...)
|
|
out = append(out, format2Mutations()...)
|
|
out = append(out, specMutations(capsule.Format3)...)
|
|
return append(out, format3Mutations()...)
|
|
}
|
|
|
|
// 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; the others are prefixed "format 2: " or
|
|
// "format 3: ". A capsule built in format 3 holds a BODY with one file.
|
|
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 fmt.Sprintf("format %d: %s", f, s)
|
|
}
|
|
build := func(b Build) (*MutationInput, error) {
|
|
b.Format = f
|
|
if f == capsule.Format3 {
|
|
const content = "malicious creator"
|
|
hb, err := capsule.EncodeHead(Head3("", "", []string{"malicious.txt"}, [][]byte{[]byte(content)}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b.Plaintext = Body3(capsule.AreaUnit, capsule.EncodeSecurity(), hb, []byte(content))
|
|
}
|
|
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.Format1 {
|
|
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 {
|
|
if m.Verdicts != nil {
|
|
switch {
|
|
case v.Err != nil:
|
|
return fmt.Errorf("got %v, want the capsule to open with the verdicts %+v", v.Err, *m.Verdicts)
|
|
case v.Verdicts != *m.Verdicts:
|
|
return fmt.Errorf("verdicts %+v, want %+v", v.Verdicts, *m.Verdicts)
|
|
}
|
|
return nil
|
|
}
|
|
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"`
|
|
// Verdicts are those of a format 3 capsule that opens: Error is then
|
|
// "ok" and Step 0 (spec §29.7, §64).
|
|
Verdicts *FixtureVerdicts `json:"verdicts,omitempty"`
|
|
}
|
|
|
|
// 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.Verdicts != nil && (string(v.Verdicts.Signature) != c.Verdicts.Signature || string(v.Verdicts.Seal) != c.Verdicts.Seal) {
|
|
return fmt.Errorf("verdicts %+v, want %+v", v.Verdicts, *c.Verdicts)
|
|
}
|
|
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"
|
|
}
|
|
if m.Verdicts != nil {
|
|
c.Verdicts = &FixtureVerdicts{Signature: string(v.Verdicts.Signature), Seal: string(v.Verdicts.Seal), Lines: v.Verdicts.Lines()}
|
|
}
|
|
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
|
|
}
|