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