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