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

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

Powered by TurnKey Linux.