Format 3, step 6d: the mutations of format 3

The mutation corpus of spec 64 gains format 3: 209 cases, 169 of the
spec.

- The 33 mutations of the first two lists on format3_single and
  format3_time_and_key_portable, named "format 3: ...", with a sibling
  written by EncryptFiles and built capsules that hold a BODY.
- The list of format 3, 47 cases: one for each value of a line with
  several (AREA_LEN 0, 511, 513 and 66048; SECURITY_LEN 0 and 513;
  HEAD_LEN 0 and 2^24 + 1), and the three that open without a code,
  with the verdicts X, F1 and S1. VERSION 4 is "format 3: version
  changed", as in format 2. Each seals BODY again with FK_PAYLOAD and
  the nonce of its fixture, and the control with the new L when L
  changes; the two that need a head followed by another STREAM chunk
  derive from format3_tree, whose comment takes the bytes the path
  loses so that only the head and its chunk change.
- Further cases: format 3 relabeled 1, and time_and_key relabeled 2
  with the identity and with the .dkk.
- A mutation may expect the capsule to open with its verdicts; the
  exported case records them, with the result ok at step 0.
- Splice gives an edit for each run of changed bytes, runs closer than
  16 bytes merged, and one more for what one side has beyond the
  other: a head sealed again changes its bytes and the tag of its
  chunk, 64 KiB apart. Earlier cases are written with more edits and
  give the same capsules. The corpus is 706 KB, 476 KB of them the
  capsule of 65536 implicit folders, whose head is 235 KB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.10
dev 1 week ago
parent db862d5524
commit 18eb3c3f48

@ -26,10 +26,10 @@ func build(t *testing.T, b testkit.Build) ([]byte, capsule.OpenOptions) {
}
// Spec §64: every mutation of the corpus (testkit.Mutations), built afresh,
// fails with its exact normative error at its exact step, and a failure
// before the release request causes no request. The thirty-three mutations
// of the first two lists of spec §64 are there once for each format, and
// those of the third list once.
// fails with its exact normative error at its exact step, or opens with its
// verdicts, and a failure before the release request causes no request. The
// thirty-three mutations of the first two lists of spec §64 are there once
// for each format, and those of the lists of formats 2 and 3 once.
func TestMutationCorpus(t *testing.T) {
e, err := testkit.NewMutationEnv(fixtureDir)
if err != nil {
@ -50,7 +50,7 @@ func TestMutationCorpus(t *testing.T) {
}
})
}
if want := 2*testkit.SpecMutationsPerFormat + testkit.Format2SpecMutations; n != want {
if want := 3*testkit.SpecMutationsPerFormat + testkit.Format2SpecMutations + testkit.Format3SpecMutations; n != want {
t.Fatalf("spec §64 gives %d mutations, the corpus has %d", want, n)
}
}
@ -75,7 +75,8 @@ func TestExportedMutationCorpus(t *testing.T) {
for i, c := range f.Cases {
m := muts[i]
t.Run(c.Name, func(t *testing.T) {
if c.Name != m.Name || c.Spec != m.Spec || c.Error != m.Want.Code() || c.Step != m.Step || c.Network != m.Network || c.Frozen != m.Random {
if c.Name != m.Name || c.Spec != m.Spec || c.Error != m.Code() || c.Step != m.Step || c.Network != m.Network || c.Frozen != m.Random ||
(c.Verdicts != nil) != (m.Verdicts != nil) {
t.Fatalf("exported case %+v does not match mutation %q", c, m.Name)
}
if err := c.Check(fixtureDir); err != nil {

@ -58,11 +58,57 @@ func ApplyEdits(base []byte, edits []Edit) ([]byte, error) {
return append(out, base[next:]...), nil
}
// Splice returns the edits that turn base into out: one edit covering the
// bytes between their common prefix and their common suffix, or none when
// they are equal.
// spliceGap is the least run of equal bytes that separates two edits of
// Splice: shorter runs cost less inside an edit than a new edit costs.
const spliceGap = 16
// Splice returns the edits that turn base into out, none when they are
// equal. Between their common prefix and their common suffix, the bytes
// that both have at the same offset are compared one by one, and each run
// of changed bytes is an edit, runs closer than spliceGap bytes merged; what
// one has beyond the other is one more edit, a deletion or an insertion at
// its end. A change sealed again with the same key and nonce, such as a
// head, changes its bytes and the tag of their chunk, 64 KiB apart: two
// small edits instead of one of a whole chunk.
func Splice(base, out []byte) []Edit {
return trimEdits(base, []Edit{{At: 0, Delete: len(base), Insert: out}})
edits := trimEdits(base, []Edit{{At: 0, Delete: len(base), Insert: out}})
if len(edits) == 0 {
return edits
}
e := edits[0]
old, ins := base[e.At:e.At+e.Delete], e.Insert
m := min(len(old), len(ins))
var res []Edit
for i := 0; i < m; {
if old[i] == ins[i] {
i++
continue
}
j := i + 1
for j < m {
if old[j] != ins[j] {
j++
continue
}
k := j
for k < m && old[k] == ins[k] && k-j < spliceGap {
k++
}
if k-j >= spliceGap || k == m {
break
}
j = k
}
res = append(res, Edit{At: e.At + i, Delete: j - i, Insert: bytes.Clone(ins[i:j])})
i = j
}
switch {
case len(old) > m:
res = append(res, Edit{At: e.At + m, Delete: len(old) - m})
case len(ins) > m:
res = append(res, Edit{At: e.At + m, Insert: bytes.Clone(ins[m:])})
}
return res
}
// trimEdits drops from each edit the leading and trailing bytes it leaves

@ -9,6 +9,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
@ -53,7 +54,20 @@ type Mutation struct {
// randomness, so that its bytes differ on every call. The exported
// corpus freezes the bytes of the first build (MutationCorpus).
Random bool
Make func(e *MutationEnv) (*MutationInput, error)
// 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
@ -134,6 +148,8 @@ type Verdict struct {
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
@ -170,6 +186,7 @@ func (in *MutationInput) Open() (Verdict, error) {
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 {
@ -242,9 +259,11 @@ type MutationEnv struct {
// 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.
// format2_time_only_extensions. TimeOnly3, TimeAndKey3 and Tree3 are
// format3_single, format3_time_and_key_portable and format3_tree.
TimeOnly, TimeAndKey *LoadedFixture
TimeOnly2, TimeAndKey2, Extensions2 *LoadedFixture
TimeOnly3, TimeAndKey3, Tree3 *LoadedFixture
siblings map[capsule.Format][]byte
}
@ -258,6 +277,7 @@ func NewMutationEnv(dir string) (*MutationEnv, error) {
{&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"},
} {
var err error
if *l.to, err = LoadFixture(dir, l.name); err != nil {
@ -270,23 +290,29 @@ func NewMutationEnv(dir string) (*MutationEnv, error) {
// timeOnly and timeAndKey return the time_only and time_and_key_portable
// fixtures of format f.
func (e *MutationEnv) timeOnly(f capsule.Format) *LoadedFixture {
if f == capsule.Format1 {
switch f {
case capsule.Format1:
return e.TimeOnly
case capsule.Format2:
return e.TimeOnly2
}
return e.TimeOnly2
return e.TimeOnly3
}
func (e *MutationEnv) timeAndKey(f capsule.Format) *LoadedFixture {
if f == capsule.Format1 {
switch f {
case capsule.Format1:
return e.TimeAndKey
case capsule.Format2:
return e.TimeAndKey2
}
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.Encrypt in format 2, and by Build, a generator of test vectors, in
// format 1.
// 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")
@ -296,6 +322,20 @@ func (e *MutationEnv) Sibling(f capsule.Format) (Parts, error) {
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()
@ -474,28 +514,40 @@ func b64(s string) string { return base64.RawURLEncoding.EncodeToString([]byte(s
// Mutations returns the mutation corpus: the thirty-three mutations of the
// first two lists of spec §64 on format 1 fixtures, further cases, the same
// thirty-three on format 2 fixtures, and the mutations of the third list of
// spec §64, of format 2, with further cases of their own.
// 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)...)
return append(out, format2Mutations()...)
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; those of format 2 are prefixed "format 2: ".
// 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 "format 2: " + 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.AreaLen, capsule.EncodeSecurity(), hb, []byte(content))
}
return built(b)
}
return []Mutation{
@ -607,7 +659,7 @@ func specMutations(f capsule.Format) []Mutation {
in, err := build(Build{Declared: capsule.TimeAndKey, Structure: capsule.TimeAndKey,
AccessRecipients: []age.Recipient{Stranger().Recipient()},
EditInner: func(fk []byte, s []*age.Stanza) []*age.Stanza {
if f == capsule.Format2 {
if f != capsule.Format1 {
s = s[:len(s)-1]
}
return append(s, &age.Stanza{Type: "scrypt", Args: []string{"c2FsdHNhbHRzYWx0c2FsdA", "10"}, Body: make([]byte, 32)})
@ -1098,6 +1150,15 @@ func (m Mutation) Check(in *MutationInput) error {
}
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")
@ -1146,6 +1207,9 @@ type MutationCase struct {
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.
@ -1223,6 +1287,9 @@ func (c *MutationCase) Check(dir string) error {
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)
}
@ -1237,6 +1304,9 @@ func (m Mutation) record(in *MutationInput, dir string, v Verdict) (MutationCase
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))

@ -0,0 +1,405 @@
package testkit
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/agewrap"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/internal/cbortest"
)
// The mutations of format 3 (spec §64): BODY, security and the head of the
// fixtures format3_single and format3_tree, sealed again as whoever knows
// I_PAYLOAD can, and their control when L changes, as anyone can seal the
// control of a time_only capsule (spec §36.1).
// Format3SpecMutations is the number of mutations of the list of format 3
// of spec §64 in the corpus, besides VERSION 4, which is "format 3: version
// changed": one for each value of a line with several, and the three cases
// that open with their verdict.
const Format3SpecMutations = 47
// resealPayload returns PAYLOAD_AGE of f with its plaintext replaced,
// sealed with FK_PAYLOAD and the nonce of the fixture.
func (f *LoadedFixture) resealPayload(plaintext []byte) ([]byte, error) {
raw, err := hex.DecodeString(f.PayloadIdentity)
if err != nil {
return nil, err
}
id, err := agewrap.NewPayloadIdentity(raw)
if err != nil {
return nil, err
}
stanzas, err := agewrap.Stanzas(bytes.NewReader(f.Parts.Payload))
if err != nil {
return nil, err
}
fk, err := id.Unwrap(stanzas)
if err != nil {
return nil, err
}
defer clear(fk)
return ResealAge(f.Parts.Payload, fk, plaintext)
}
// WithBody returns f, a time_only fixture of format 3 or 2, with the content
// of PAYLOAD_AGE replaced by body and followed by the zeros of its padding
// up to P = rule(len(body)); plain, when not nil, edits that plaintext. When
// L changes, the control is sealed again with the new L (spec §29.1, §29.2).
func (f *LoadedFixture) WithBody(body []byte, plain func(p []byte) []byte) (*MutationInput, error) {
l := uint64(len(body))
p, err := capsule.PaddedLength(l, capsule.Padding(f.Padding))
if err != nil {
return nil, err
}
plaintext := append(bytes.Clone(body), make([]byte, p-l)...)
if plain != nil {
plaintext = plain(plaintext)
}
parts := f.Parts
if l != f.PayloadLength {
in, err := f.WithControl(func(c map[uint64]any) { c[6] = payloadLength(l) })
if err != nil {
return nil, err
}
if parts, err = Split(in.DKC); err != nil {
return nil, err
}
}
payload, err := f.resealPayload(plaintext)
if err != nil {
return nil, err
}
return f.input(Join(parts.Prelude, parts.Header, parts.Sealed, payload)), nil
}
// body3 is BODY split into its parts (spec §29.2).
type body3 struct {
frame capsule.BodyFrame
area []byte
head []byte
content []byte
}
// readBody3 returns the BODY of the format 3 fixture f, from its plaintext
// file in dir, split.
func readBody3(f *LoadedFixture, dir string) (body3, error) {
b, err := os.ReadFile(filepath.Join(dir, f.PlaintextFile))
if err != nil {
return body3{}, err
}
frame, err := capsule.ParseBodyFrame(b[:capsule.BodyFrameSize], uint64(len(b)))
if err != nil {
return body3{}, err
}
a := capsule.BodyFrameSize + int(frame.AreaLen)
h := a + int(frame.HeadLen)
return body3{frame: frame, area: b[capsule.BodyFrameSize:a], head: b[a:h], content: b[h:]}, nil
}
// bytes joins the parts again, with the frame as it is.
func (b body3) bytes() []byte {
fb := b.frame.Bytes()
return Join(fb[:], b.area, b.head, b.content)
}
// withHeadBytes returns BODY with its head replaced, HEAD_LEN following it.
func (b body3) withHeadBytes(head []byte) []byte {
b.head, b.frame.HeadLen = head, uint32(len(head))
return b.bytes()
}
// body3Edit returns the format 3 fixture f with edit applied to its BODY,
// and plain, when not nil, to the plaintext of PAYLOAD_AGE.
func (e *MutationEnv) body3Edit(f *LoadedFixture, edit func(b body3) []byte, plain func(p []byte) []byte) (*MutationInput, error) {
b, err := readBody3(f, e.Dir)
if err != nil {
return nil, err
}
return f.WithBody(edit(b), plain)
}
// headEdit returns the format 3 fixture f with its head decoded as a map,
// edited and encoded again.
func (e *MutationEnv) headEdit(f *LoadedFixture, edit func(h map[uint64]any)) (*MutationInput, error) {
return e.body3Edit(f, func(b body3) []byte {
m, err := cbortest.UnmarshalMap(b.head)
if err != nil {
panic(err)
}
edit(m)
return b.withHeadBytes(mustMarshal(m))
}, nil)
}
// entry is a file entry of a head, of the content of format3_single when
// its size is not 0: the SHA-256 is that of its size first bytes.
func entry(path any, size, start, end uint64) map[uint64]any {
sum := sha256.Sum256([]byte(single3Content[:min(size, uint64(len(single3Content)))]))
return map[uint64]any{0: path, 1: size, 2: start, 3: end, 4: sum[:]}
}
// single3Content is the content of the only file of format3_single.
const single3Content = "Hola desde el pasado.\n"
// file0 sets key k of the first file of the head h to v.
func file0(h map[uint64]any, k uint64, v any) {
h[5].([]any)[0].(map[uint64]any)[k] = v
}
// securityV2 is SECURITY_CBOR of version 2.
func securityV2() []byte {
return mustMarshal(map[uint64]any{0: capsule.SecurityTypeTag, 1: uint64(2)})
}
// implicitFolders returns the entries of files of 0 bytes whose paths make
// n implicit folders: 31 for each path, "a3k/0/0/…/0/f", and the rest in a
// last one (spec §29.5, R9).
func implicitFolders(n int) []any {
var paths []string
for i := 0; n > 0; i++ {
depth := min(n, 31)
seg := []string{fmt.Sprintf("%04d", i)}
for range depth - 1 {
seg = append(seg, "0")
}
paths = append(paths, strings.Join(append(seg, "f"), "/"))
n -= depth
}
var out []any
for _, p := range paths {
out = append(out, entry(p, 0, 0, 0))
}
return out
}
// format3Mutations returns the mutations of the list of format 3 of spec
// §64, except VERSION 4, which is "format 3: version changed", and further
// cases of their own. Each derives from format3_single, or from
// format3_tree when it needs a head followed by another STREAM chunk.
func format3Mutations() []Mutation {
hi, nc, integ := datekeys.ErrHeadInvalid, datekeys.ErrNonCanonicalCBOR, datekeys.ErrIntegrity
ok := func(in *MutationInput) (*MutationInput, error) { return in, nil }
spec := func(name string, want *datekeys.Error, mk func(e *MutationEnv) (*MutationInput, error)) Mutation {
return Mutation{Name: name, Spec: true, Want: want, Step: 17, Network: true, Make: mk}
}
frame := func(at int, v uint32) func(e *MutationEnv) (*MutationInput, error) {
return func(e *MutationEnv) (*MutationInput, error) {
return e.body3Edit(e.TimeOnly3, func(b body3) []byte {
out := b.bytes()
binary.BigEndian.PutUint32(out[at:], v)
return out
}, nil)
}
}
head := func(edit func(h map[uint64]any)) func(e *MutationEnv) (*MutationInput, error) {
return func(e *MutationEnv) (*MutationInput, error) { return e.headEdit(e.TimeOnly3, edit) }
}
headBytes := func(edit func(h []byte) []byte) func(e *MutationEnv) (*MutationInput, error) {
return func(e *MutationEnv) (*MutationInput, error) {
return e.body3Edit(e.TimeOnly3, func(b body3) []byte { return b.withHeadBytes(edit(bytes.Clone(b.head))) }, nil)
}
}
path := func(p string) func(e *MutationEnv) (*MutationInput, error) {
return head(func(h map[uint64]any) { file0(h, 0, p) })
}
// The single file of format3_single, 22 bytes, then one of 0 bytes.
two := func(a, b string) func(e *MutationEnv) (*MutationInput, error) {
return head(func(h map[uint64]any) { h[5] = []any{entry(a, 22, 0, 22), entry(b, 0, 22, 22)} })
}
security := func(sec []byte, v capsule.Verdicts) Mutation {
return Mutation{Spec: true, Network: true, Verdicts: &v, Make: func(e *MutationEnv) (*MutationInput, error) {
return e.body3Edit(e.TimeOnly3, func(b body3) []byte {
b.area = append(bytes.Clone(sec), make([]byte, len(b.area)-len(sec))...)
b.frame.SecurityLen = uint32(len(sec))
return b.bytes()
}, nil)
}}
}
lastPadding := func(p []byte) []byte { p[len(p)-1] = 1; return p }
// The first path of format3_tree, carta.txt, becomes "..", which keeps
// the byte order of the paths, and the comment grows by the 7 bytes the
// path loses, so that the head keeps its length and only its bytes
// change; it is in the first STREAM chunk.
treeDotDot := func(e *MutationEnv) (*MutationInput, error) {
return e.headEdit(e.Tree3, func(h map[uint64]any) {
old := h[5].([]any)[0].(map[uint64]any)[0].(string)
file0(h, 0, "..")
h[3] = h[3].(string) + "\n" + strings.Repeat(".", len(old)-len("..")-1)
})
}
withPayload := func(mk func(e *MutationEnv) (*MutationInput, error), edit func(p []byte) ([]byte, error)) func(e *MutationEnv) (*MutationInput, error) {
return func(e *MutationEnv) (*MutationInput, error) {
in, err := mk(e)
if err != nil {
return nil, err
}
parts, err := Split(in.DKC)
if err != nil {
return nil, err
}
payload, err := edit(bytes.Clone(parts.Payload))
if err != nil {
return nil, err
}
in.DKC = Join(parts.Prelude, parts.Header, parts.Sealed, payload)
return in, nil
}
}
// chunk1 is the offset in PAYLOAD_AGE of its second STREAM chunk.
chunk1 := func(p []byte) (int, error) {
h, err := HeaderLen(p)
return h + streamNonceSize + 64<<10 + 16, err
}
sigAlg1 := mustMarshal(map[uint64]any{0: uint64(1), 1: fill(0x11, 32), 2: fill(0x22, 64)})
sealType1 := mustMarshal(map[uint64]any{0: uint64(1), 1: fill(0x33, 32)})
named := func(m Mutation, name string) Mutation { m.Name = name; return m }
withIdentity := func(f *LoadedFixture, in *MutationInput) (*MutationInput, error) { return f.withIdentity(in) }
relabelTK := func(e *MutationEnv) (*MutationInput, error) {
return withIdentity(e.TimeAndKey3, relabeled(e.TimeAndKey3, 2))
}
return []Mutation{
// Format 3 exists since v0.10: VERSION 2 on a format 3 capsule fails
// at step 14, where the schema version of the control is 3.
{Name: "format 3 time_only relabeled format 2", Spec: true, Want: datekeys.ErrUnsupportedVersion, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) { return ok(relabeled(e.TimeOnly3, 2)) }},
// 17.2: the frame of BODY and the area.
spec("AREA_LEN 0", integ, frame(0, 0)),
spec("AREA_LEN 511", integ, frame(0, 511)),
spec("AREA_LEN 513", integ, frame(0, 513)),
spec("AREA_LEN 66048", integ, frame(0, 66048)),
spec("SECURITY_LEN 0", integ, frame(4, 0)),
spec("SECURITY_LEN 513, larger than AREA_LEN", integ, frame(4, 513)),
spec("HEAD_LEN 0", integ, frame(8, 0)),
spec("HEAD_LEN 2^24 + 1", integ, frame(8, 1<<24+1)),
spec("12 + AREA_LEN + HEAD_LEN = L + 1", integ, func(e *MutationEnv) (*MutationInput, error) {
return e.body3Edit(e.TimeOnly3, func(b body3) []byte {
l := uint32(capsule.BodyFrameSize + len(b.area) + len(b.head) + len(b.content))
b.frame.HeadLen = l - capsule.BodyFrameSize - b.frame.AreaLen + 1
return b.bytes()
}, nil)
}),
spec("L < 12: 11", integ, func(e *MutationEnv) (*MutationInput, error) {
return e.body3Edit(e.TimeOnly3, func(b body3) []byte { return b.bytes()[:11] }, nil)
}),
spec("a byte of the area not zero", integ, func(e *MutationEnv) (*MutationInput, error) {
return e.body3Edit(e.TimeOnly3, func(b body3) []byte {
b.area = bytes.Clone(b.area)
b.area[len(b.area)-1] = 1
return b.bytes()
}, nil)
}),
// 17.4: the head, layers 2 and 3.
spec("head of version 2", datekeys.ErrUnsupportedVersion, head(func(h map[uint64]any) { h[1] = uint64(2) })),
spec("head of another type tag", nc, head(func(h map[uint64]any) { h[0] = "datekeys-heaD" })),
spec("head with a byte more within HEAD_LEN", nc, headBytes(func(h []byte) []byte { return append(h, 0) })),
spec("paths b and a, in that order", nc, two("b", "a")),
spec("paths b/.. and a, in that order", nc, two("b/..", "a")),
spec("path of 1025 bytes", nc, path(strings.Repeat("a/", 512)+"a")),
// 17.4: layer 4, in key order.
spec("path ..", hi, path("..")),
spec("path /a", hi, path("/a")),
spec("paths A.txt and a.txt", hi, two("A.txt", "a.txt")),
spec("paths ab and a, U+200C, b", hi, two("ab", "a\u200cb")),
spec("path CON.txt", hi, path("CON.txt")),
spec("path CON.txt in full-width forms", hi, path("\uff23\uff2f\uff2e.txt")),
spec("path ABCDEF~1", hi, path("ABCDEF~1")),
spec("path with U+202E", hi, path("a\u202eb.txt")),
spec("path .datekeys-x", hi, path(".datekeys-x")),
spec("comment with U+202E", hi, head(func(h map[uint64]any) { h[3] = "a\u202eb" })),
spec("path a.", hi, path("a.")),
spec("paths A and a/b", hi, two("A", "a/b")),
spec("65536 implicit folders", hi, head(func(h map[uint64]any) { h[5] = implicitFolders(65536) })),
spec("declared author with LF", hi, head(func(h map[uint64]any) { h[4] = "Ana\u000aLópez" })),
spec("start of the first entry not 0", hi, head(func(h map[uint64]any) { file0(h, 2, uint64(1)); file0(h, 3, uint64(23)) })),
spec("end - start not size", hi, head(func(h map[uint64]any) { file0(h, 3, uint64(21)) })),
spec("path a followed by VS16", hi, path("a\ufe0f")),
spec("path with two ZWJ in a row", hi, path("a\u200d\u200db")),
spec("comment with the tag U+E0041", hi, head(func(h map[uint64]any) { h[3] = "Hola\U000e0041" })),
spec("comment with the variation selector VS17", hi, head(func(h map[uint64]any) { h[3] = "\U0001f600\U000e0100" })),
spec("start of an entry not the end of the one before", hi, head(func(h map[uint64]any) {
h[5] = []any{entry("a", 11, 0, 11), entry("b", 11, 12, 23)}
})),
// 17.5, 17.7 and 17.8.
spec("end of the last file not C", integ, head(func(h map[uint64]any) { file0(h, 1, uint64(21)); file0(h, 3, uint64(21)) })),
spec("a byte of a file changed", integ, func(e *MutationEnv) (*MutationInput, error) {
return e.body3Edit(e.TimeOnly3, func(b body3) []byte {
b.content = bytes.Clone(b.content)
b.content[0] ^= 1
return b.bytes()
}, nil)
}),
// Precedence: the head fails first; what follows decides only when it
// is a failure of age or of the length.
spec("path .. and a padding byte not zero", hi, func(e *MutationEnv) (*MutationInput, error) {
return e.body3Edit(e.TimeOnly3, func(b body3) []byte {
m, err := cbortest.UnmarshalMap(b.head)
if err != nil {
return nil
}
file0(m, 0, "..")
return b.withHeadBytes(mustMarshal(m))
}, lastPadding)
}),
spec("path .. and the next STREAM chunk corrupt", integ, withPayload(treeDotDot, func(p []byte) ([]byte, error) {
at, err := chunk1(p)
if err != nil || at+100 >= len(p) {
return nil, errors.New("testkit: format3_tree has no second chunk")
}
p[at+100] ^= 1
return p, nil
})),
spec("path .. and a cut right after its chunk", integ, withPayload(treeDotDot, func(p []byte) ([]byte, error) {
at, err := chunk1(p)
if err != nil || at >= len(p) {
return nil, errors.New("testkit: format3_tree has no second chunk")
}
return p[:at], nil
})),
// Security never decides the opening: these open, without a code,
// with their verdicts (spec §29.3, §29.7).
named(security(securityV2(), capsule.Verdicts{Signature: capsule.VerdictUnreadable, Seal: capsule.VerdictUnreadable}), "security of version 2 opens with the verdict X"),
named(security(mustSecurity(sigAlg1, nil), capsule.Verdicts{Signature: capsule.VerdictSignatureUnchecked, Seal: capsule.VerdictNoSeal}), "a signature of alg 1 opens with the verdict F1"),
named(security(mustSecurity(nil, sealType1), capsule.Verdicts{Signature: capsule.VerdictNoSignature, Seal: capsule.VerdictSealUnsupported}), "a seal of seal_type 1 opens with the verdict S1"),
// Further cases: the other relabelings of a format 3 capsule.
{Name: "format 3 time_only relabeled format 1", Want: datekeys.ErrUnsupportedVersion, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) { return ok(relabeled(e.TimeOnly3, 1)) }},
{Name: "format 3 time_and_key relabeled format 2, with the identity", Want: datekeys.ErrUnsupportedVersion, Step: 14, Network: true, Make: relabelTK},
{Name: "format 3 time_and_key relabeled format 2, with the .dkk", Want: datekeys.ErrAccessInvalid, Step: 9,
Make: func(e *MutationEnv) (*MutationInput, error) {
in, err := relabelTK(e)
if err != nil {
return nil, err
}
in.DKK, in.Identities = e.TimeAndKey3.DKK, nil
return in, nil
}},
}
}
// mustSecurity is SECURITY_CBOR with the given contents of keys 2 and 3.
func mustSecurity(signature, seal []byte) []byte {
b, err := capsule.EncodeSecurityWith(signature, seal)
if err != nil {
panic(err)
}
return b
}

@ -6,7 +6,6 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
@ -247,23 +246,7 @@ func (f *LoadedFixture) WithControl(edit func(control map[uint64]any)) (*Mutatio
// replaced by plaintext, sealed with FK_PAYLOAD and the nonce of PAYLOAD_AGE,
// as whoever knows I_PAYLOAD can make it.
func (f *LoadedFixture) WithPayloadPlaintext(plaintext []byte) (*MutationInput, error) {
raw, err := hex.DecodeString(f.PayloadIdentity)
if err != nil {
return nil, err
}
id, err := agewrap.NewPayloadIdentity(raw)
if err != nil {
return nil, err
}
stanzas, err := agewrap.Stanzas(bytes.NewReader(f.Parts.Payload))
if err != nil {
return nil, err
}
fk, err := id.Unwrap(stanzas)
if err != nil {
return nil, err
}
payload, err := ResealAge(f.Parts.Payload, fk, plaintext)
payload, err := f.resealPayload(plaintext)
if err != nil {
return nil, err
}

@ -18,17 +18,31 @@ import (
func TestEdits(t *testing.T) {
base := []byte("0123456789")
long := "0123456789abcdefghijklmnopqrstuvwxyz"
for _, tc := range []struct {
base string
out string
want string // JSON of Splice(base, out)
}{
{"0123456789", `[]`},
{"01X3456789", `[[2,1,"58"]]`},
{"012", `[[3,7,""]]`},
{"", `[[0,10,""]]`},
{"01234567890", `[[10,0,"30"]]`},
{"0123XY456789", `[[4,0,"5859"]]`},
{"", "0123456789", `[]`},
{"", "01X3456789", `[[2,1,"58"]]`},
{"", "012", `[[3,7,""]]`},
{"", "", `[[0,10,""]]`},
{"", "01234567890", `[[10,0,"30"]]`},
{"", "0123XY456789", `[[4,0,"5859"]]`},
// Runs 16 or more equal bytes apart are edits of their own; closer
// ones are merged.
{long, "0X23456789abcdefghijklmnopqrstuvwxYz", `[[1,1,"58"],[34,1,"59"]]`},
{long, "0X23456789aXcdefghijklmnopqrstuvwxyz", `[[1,11,"5832333435363738396158"]]`},
// A change and a cut: an edit and a deletion.
{long, "0X23456789abcdefghijklmnopqrst", `[[1,1,"58"],[30,6,""]]`},
// A change and more bytes: an edit and an insertion.
{long, "0X23456789abcdefghijklmnopqrstuvwxyz!!", `[[1,1,"58"],[36,0,"2121"]]`},
} {
base := base
if tc.base != "" {
base = []byte(tc.base)
}
edits := testkit.Splice(base, []byte(tc.out))
if edits == nil {
edits = []testkit.Edit{}

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save

Powered by TurnKey Linux.