package main import ( "bytes" "crypto/rand" "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "reflect" "slices" "strings" "time" "g.activething.com/go/DateKeys/capsule" "g.activething.com/go/DateKeys/codec" "g.activething.com/go/DateKeys/codec/bech32" "g.activething.com/go/DateKeys/internal/testkit" ) // file3 is a file of a format 3 fixture; a zero mtime is none. type file3 struct { path string content []byte mtime time.Time } // sources returns the files as the Sources of capsule.EncryptFiles. func sources(files []file3) []capsule.Source { var out []capsule.Source for _, f := range files { content := f.content out = append(out, capsule.Source{Path: f.path, Size: int64(len(content)), ModTime: f.mtime, Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(content)), nil }}) } return out } // body3 returns the BODY of a capsule that only a generator of test vectors // writes (spec §62.1 rule 13): the files, in the byte order of their paths, // with a fresh salt, and security in an area of area bytes. The reader must // accept its head. func body3(area uint32, security []byte, comment, author string, files []file3) ([]byte, error) { files = slices.Clone(files) slices.SortFunc(files, func(a, b file3) int { return strings.Compare(a.path, b.path) }) h := &capsule.Head{Comment: comment, Author: author} _, _ = rand.Read(h.Salt[:]) var contents [][]byte var end uint64 for _, f := range files { n := uint64(len(f.content)) e := capsule.File{Path: f.path, Size: n, Start: end, End: end + n, SHA256: sha256.Sum256(f.content)} if !f.mtime.IsZero() { e.MTime, e.HasMTime = uint64(f.mtime.Unix()), true } h.Files = append(h.Files, e) contents = append(contents, f.content) end += n } hb, err := capsule.EncodeHead(h) if err != nil { return nil, err } if _, err := capsule.DecodeHead(hb, nil); err != nil { return nil, fmt.Errorf("the reader rejects the head: %w", err) } return testkit.Body3(area, security, hb, contents...), nil } // securityV2 is SECURITY_CBOR of version 2, {0: "datekeys-security", 1: 2}, // which a reader of this version cannot read (verdict X). func securityV2() ([]byte, error) { var e codec.Encoder e.Map(2) e.Uint(0) e.Text(capsule.SecurityTypeTag) e.Uint(1) e.Uint(2) return e.Out() } // randomBytes returns n bytes of a CSPRNG. func randomBytes(n int) []byte { b := make([]byte, n) _, _ = rand.Read(b) return b } // unsupportedSignature is the content of key 2 of security: an // author-signature of an alg that no version defines, 4294967295, with a // random key of 32 bytes and a random signature of 64 (verdict F1: this // reader does not implement that alg; spec v0.11, §29.7). func unsupportedSignature() ([]byte, error) { return capsule.EncodeAuthorSignature(4294967295, randomBytes(32), randomBytes(64)) } // patterned returns n bytes that look like the content of a binary file: // SHA-256 in counter mode over seed. func patterned(seed string, n int) []byte { var out []byte for i := 0; len(out) < n; i++ { s := sha256.Sum256(fmt.Appendf(nil, "%s %d", seed, i)) out = append(out, s[:]...) } return out[:n] } // record3 fills the fields of format 3 of f from BODY, the first L bytes of // the plaintext of PAYLOAD_AGE, checking it with the rules of the reader. func record3(f *testkit.DKCFixture, body []byte, verdicts *capsule.Verdicts) error { l := uint64(len(body)) if l < capsule.BodyFrameSize { return errors.New("BODY shorter than its frame") } frame, err := capsule.ParseBodyFrame(body[:capsule.BodyFrameSize], l) if err != nil { return err } area := body[capsule.BodyFrameSize : capsule.BodyFrameSize+uint64(frame.AreaLen)] if err := capsule.CheckArea(area, frame.SecurityLen); err != nil { return err } security := area[:frame.SecurityLen] offset := capsule.BodyFrameSize + uint64(frame.AreaLen) + uint64(frame.HeadLen) hb := body[capsule.BodyFrameSize+uint64(frame.AreaLen) : offset] h, err := capsule.DecodeHead(hb, nil) if err != nil { return err } if err := capsule.CheckHeadEnd(h, frame.ContentLength(l)); err != nil { return err } v := capsule.EvaluateSecurity(security) if verdicts != nil { v = *verdicts } f.AreaLen = frame.AreaLen f.Security = hex.EncodeToString(security) f.Head = hex.EncodeToString(hb) f.Salt = hex.EncodeToString(h.Salt[:]) f.Comment, f.Author = h.Comment, h.Author f.HeadExtensions = exts(false, h.Noncritical) f.ContentOffset = offset f.Files = nil for _, e := range h.Files { content := body[offset+e.Start : offset+e.End] if sha256.Sum256(content) != e.SHA256 { return fmt.Errorf("file %q: its SHA-256 is not the one of the head", e.Path) } ff := testkit.FixtureFile{Path: e.Path, Size: e.Size, Start: e.Start, End: e.End, SHA256: hex.EncodeToString(e.SHA256[:])} if e.HasMTime { m := e.MTime ff.MTime = &m } f.Files = append(f.Files, ff) } f.Verdicts = &testkit.FixtureVerdicts{Signature: string(v.Signature), Seal: string(v.Seal), Lines: v.Lines()} f.Signature, err = recordSignature(f, security, hb, v) if err != nil { return err } if f.Signature != nil { f.Verdicts.AuthorKey = f.Signature.AuthorKey } return nil } // recordSignature returns what the record of a fixture says about its // signature of alg 1: the seed of its test key, which the generator wrote and // this keeps, and the commitments and the message, which it computes from the // control of the fixture, its head and its security (spec v0.11, §29.8). Nil // when the fixture has no valid signature. func recordSignature(f *testkit.DKCFixture, security, head []byte, v capsule.Verdicts) (*testkit.FixtureSignature, error) { if v.Signature != capsule.VerdictSignedOther && v.Signature != capsule.VerdictSignedSaved { return nil, nil } if f.Signature == nil { return nil, errors.New("the fixture has a valid signature and its record no seed of the key") } cb, err := hex.DecodeString(f.ControlCBOR) if err != nil { return nil, err } c, err := capsule.DecodeControl(cb, capsule.Format(f.Format)) if err != nil { return nil, err } defer clear(c.PayloadIdentity[:]) cc, err := capsule.ControlCommit(c, capsule.Format(f.Format)) if err != nil { return nil, err } hd := capsule.HeadDigest(head) sd := capsule.SignersDigest(capsule.AlgEd25519, nil) msg := capsule.AuthorMessage(cc, hd, sd) content, value, err := capsule.SecurityKey2(security) if err != nil { return nil, err } key, err := bech32.Encode("dkauthor", v.AuthorKey[:]) if err != nil { return nil, err } return &testkit.FixtureSignature{ Alg: capsule.AlgEd25519, SecretSeed: f.Signature.SecretSeed, AuthorKey: key, ControlCommit: hex.EncodeToString(cc[:]), HeadDigest: hex.EncodeToString(hd[:]), SignersDigest: hex.EncodeToString(sd[:]), AuthorMessage: string(msg), AuthorCode: capsule.AuthorCode(msg), SignatureValue: hex.EncodeToString(value), SecurityKey2: hex.EncodeToString(content), }, nil } // check3 checks what Open delivered for the format 3 fixture f, whose BODY // is body: the files in its sink, the head and the verdicts. func check3(f *testkit.DKCFixture, body []byte, opened *capsule.Opened, sink *testkit.MemorySink) error { if !sink.Committed || sink.Aborted || opened.Head == nil || len(sink.Files) != len(f.Files) { return errors.New("Open did not deliver the files") } for i, ff := range f.Files { want := body[f.ContentOffset+ff.Start : f.ContentOffset+ff.End] if !bytes.Equal(sink.Files[i], want) && (len(want) != 0 || sink.Files[i] != nil) { return fmt.Errorf("file %q differs", ff.Path) } } want := &testkit.FixtureVerdicts{Signature: string(opened.Verdicts.Signature), Seal: string(opened.Verdicts.Seal), Lines: opened.Verdicts.Lines()} if f.Verdicts != nil && f.Verdicts.AuthorKey != "" { want.AuthorKey, _ = bech32.Encode("dkauthor", opened.Verdicts.AuthorKey[:]) } if !reflect.DeepEqual(want, f.Verdicts) || opened.AreaLen != f.AreaLen || opened.Head.Comment != f.Comment || opened.Head.Author != f.Author { return errors.New("Open reports another head or other verdicts") } return nil }