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.
526 lines
22 KiB
526 lines
22 KiB
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/authorkey"
|
|
"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 four cases
|
|
// that open with their verdict.
|
|
const Format3SpecMutations = 48
|
|
|
|
// V011SpecMutations is the number of mutations of the list "Firma, sello,
|
|
// nota y llave de palabras (v0.11)" of spec §64 in the corpus, besides those
|
|
// of alg 2 and seal_type 2, which need certificates and are vectors of
|
|
// security_cms.json: the signature of alg 1 of format3_signed altered,
|
|
// removed, made again with another key and transplanted, a key and a
|
|
// signature of another length, the area widened after signing, and the
|
|
// public note of format3_note changed.
|
|
const V011SpecMutations = 8
|
|
|
|
// 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)})
|
|
sigAlgMax := mustMarshal(map[uint64]any{0: uint64(capsule.AlgTest), 1: fill(0x11, 32), 2: fill(0x22, 64)})
|
|
sealTypeMax := mustMarshal(map[uint64]any{0: uint64(capsule.SealTypeTest), 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(sigAlgMax, nil), capsule.Verdicts{Signature: capsule.VerdictSignatureUnchecked, Seal: capsule.VerdictNoSeal}), "a signature of alg 4294967295 opens with the verdict F1"),
|
|
named(security(mustSecurity(sigAlg1, nil), capsule.Verdicts{Signature: capsule.VerdictSignatureInvalid, Seal: capsule.VerdictNoSeal}), "a signature of alg 1 that does not verify opens with the verdict F2"),
|
|
named(security(mustSecurity(nil, sealTypeMax), capsule.Verdicts{Signature: capsule.VerdictNoSignature, Seal: capsule.VerdictSealUnsupported}), "a seal of seal_type 4294967295 opens with the verdict S1"),
|
|
|
|
// The signature of alg 1 of format3_signed (spec v0.11 §29.8, §29.9, §64):
|
|
// it covers the control without L, the head and SIGNERS, never the area.
|
|
signed1("the signature of alg 1 altered opens with the verdict F2", invalid, func(e *MutationEnv, s signedParts) ([]byte, *LoadedFixture) {
|
|
sig := s.key.Sign(s.message)
|
|
sig[10] ^= 1
|
|
return s.security(s.key.Public(), sig), e.Signed3
|
|
}),
|
|
signed1("the signature of alg 1 removed opens with the verdict F0", capsule.Verdicts{Signature: capsule.VerdictNoSignature, Seal: capsule.VerdictNoSeal}, func(e *MutationEnv, s signedParts) ([]byte, *LoadedFixture) {
|
|
return capsule.EncodeSecurity(), e.Signed3
|
|
}),
|
|
signed1("the signature of alg 1 made again with another key opens with the verdict F4 of that key", signedBy(otherAuthorKey().Public()), func(e *MutationEnv, s signedParts) ([]byte, *LoadedFixture) {
|
|
k := otherAuthorKey()
|
|
return s.security(k.Public(), k.Sign(s.message)), e.Signed3
|
|
}),
|
|
signed1("the signature of alg 1 transplanted to another capsule opens with the verdict F2", invalid, func(e *MutationEnv, s signedParts) ([]byte, *LoadedFixture) {
|
|
return s.security(s.key.Public(), s.key.Sign(s.message)), e.Unsigned3
|
|
}),
|
|
signed1("a key of 31 bytes in a signature of alg 1 opens with the verdict F1", unchecked, func(e *MutationEnv, s signedParts) ([]byte, *LoadedFixture) {
|
|
return s.security(s.key.Public()[:31], s.key.Sign(s.message)), e.Signed3
|
|
}),
|
|
signed1("a signature of 65 bytes of alg 1 opens with the verdict F1", unchecked, func(e *MutationEnv, s signedParts) ([]byte, *LoadedFixture) {
|
|
return s.security(s.key.Public(), append(s.key.Sign(s.message), 0)), e.Signed3
|
|
}),
|
|
{Name: "the area widened to 64 KiB after signing opens with the verdict F4 and the same AUTHOR_MESSAGE", Spec: true, Network: true, Verdicts: func() *capsule.Verdicts { v := signedBy(fixtureAuthorKey().Public()); return &v }(),
|
|
Make: func(e *MutationEnv) (*MutationInput, error) {
|
|
return e.body3Edit(e.Signed3, func(b body3) []byte {
|
|
b.area = append(bytes.Clone(b.area), make([]byte, capsule.LargeAreaLen-len(b.area))...)
|
|
b.frame.AreaLen = capsule.LargeAreaLen
|
|
return b.bytes()
|
|
}, nil)
|
|
}},
|
|
// The public note of format3_note (spec v0.11 §24.1): header_binding
|
|
// ties it to the capsule.
|
|
{Name: "the public note changed in PUBLIC_HEADER", Want: datekeys.ErrHeaderBinding, Step: 15, Spec: true, Network: true,
|
|
Make: func(e *MutationEnv) (*MutationInput, error) {
|
|
dkc := bytes.Replace(e.Note3.DKC, []byte("Lisboa"), []byte("Lisbon"), 1)
|
|
if bytes.Equal(dkc, e.Note3.DKC) {
|
|
return nil, errors.New("testkit: format3_note has no note in clear")
|
|
}
|
|
return e.Note3.input(dkc), nil
|
|
}},
|
|
|
|
// 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
|
|
}
|
|
|
|
// signedParts is what the mutations of the signature of format3_signed need:
|
|
// its key, its AUTHOR_MESSAGE, recomputed from the commitments of its record.
|
|
type signedParts struct {
|
|
key *authorkey.Key
|
|
message []byte
|
|
}
|
|
|
|
// security returns SECURITY_CBOR with a signature of alg 1 of key and sig,
|
|
// whatever their lengths.
|
|
func (signedParts) security(key, sig []byte) []byte {
|
|
content := mustMarshal(map[uint64]any{0: uint64(capsule.AlgEd25519), 1: key, 2: sig})
|
|
sec, err := capsule.EncodeSecurityWith(content, nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return sec
|
|
}
|
|
|
|
// fixtureAuthorKey is the test key of format3_signed, whose seed its record
|
|
// gives; otherAuthorKey is another one.
|
|
func fixtureAuthorKey() *authorkey.Key { return seededKey("DateKeys fixture author key 1") }
|
|
func otherAuthorKey() *authorkey.Key { return seededKey("DateKeys mutation author key 2") }
|
|
|
|
func seededKey(text string) *authorkey.Key {
|
|
seed := sha256.Sum256([]byte(text))
|
|
k, err := authorkey.NewFromSeed(seed[:])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return k
|
|
}
|
|
|
|
var (
|
|
invalid = capsule.Verdicts{Signature: capsule.VerdictSignatureInvalid, Seal: capsule.VerdictNoSeal}
|
|
unchecked = capsule.Verdicts{Signature: capsule.VerdictSignatureUnchecked, Seal: capsule.VerdictNoSeal}
|
|
)
|
|
|
|
// signedBy is F4 with the key pub, without a seal.
|
|
func signedBy(pub []byte) capsule.Verdicts {
|
|
return capsule.Verdicts{Signature: capsule.VerdictSignedOther, Seal: capsule.VerdictNoSeal, AuthorKey: [32]byte(pub)}
|
|
}
|
|
|
|
// signed1 is a mutation of the security area: mk returns SECURITY_CBOR, from
|
|
// the key and the message of format3_signed, and the fixture whose BODY gets
|
|
// it in its area.
|
|
func signed1(name string, v capsule.Verdicts, mk func(e *MutationEnv, s signedParts) ([]byte, *LoadedFixture)) Mutation {
|
|
return Mutation{Name: name, Spec: true, Network: true, Verdicts: &v, Make: func(e *MutationEnv) (*MutationInput, error) {
|
|
sig := e.Signed3.Signature
|
|
if sig == nil {
|
|
return nil, errors.New("testkit: format3_signed has no signature in its record")
|
|
}
|
|
cc, err1 := hex.DecodeString(sig.ControlCommit)
|
|
hd, err2 := hex.DecodeString(sig.HeadDigest)
|
|
if err := errors.Join(err1, err2); err != nil || len(cc) != 32 || len(hd) != 32 {
|
|
return nil, fmt.Errorf("testkit: the commitments of format3_signed: %v", err)
|
|
}
|
|
s := signedParts{key: fixtureAuthorKey(), message: capsule.AuthorMessage([32]byte(cc), [32]byte(hd), capsule.SignersDigest(capsule.AlgEd25519, nil))}
|
|
sec, f := mk(e, s)
|
|
return e.body3Edit(f, 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)
|
|
}}
|
|
}
|