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/genfixtures/format3.go

175 lines
5.7 KiB

Format 3, step 6b: the nine fixtures of format 3 The fixtures of spec 67 for format 3, each with its record, its BODY, its inspect output and, for time_and_key, its .dkk: - format3_single, format3_tree (five files in three folders, one over two STREAM chunks, one without mtime, a comment and a declared author), format3_comment_only (no files; a TAB in the comment), format3_bloque256 and format3_time_and_key_portable, written with EncryptFiles; - format3_area_1024, format3_security_v2 (verdict X), format3_signature_unsupported (an author-signature of alg 1 with a random key of 32 bytes and a random signature of 64: F1) and format3_seal_unsupported (that and a seal of seal_type 1: F1 and S1), which only a generator of test vectors writes (62.1 rule 13), built with testkit.Build. The record of a format 3 fixture adds the area, SECURITY_CBOR, HEAD_CBOR, the salt, the comment, the declared author, the head extensions, the offset of CONTENT in BODY, each file with its layout, SHA-256 and mtime, and the verdicts with their lines; its plaintext file is BODY. The generator writes, then recovers every value by opening layer by layer for the three formats alike, and refreshes the records of format 3 through a Sink. Tests: the conformance test checks BODY, the head, security and every file, and opens through a MemorySink; the .dkk tests take the .dkk of formats 2 and 3 too (spec 68); the CLI decrypts five of the fixtures into folders; the control fuzz target decodes with the three schema versions. The differential corpus gains two bases, format3_single and format3_time_and_key_portable, one per policy: 5110 cases, the earlier ones unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
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/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 alg 1, with a random key of 32 bytes and a random
// signature of 64 (verdict F1: this version implements no alg).
func unsupportedSignature() ([]byte, error) {
return capsule.EncodeAuthorSignature(1, 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) 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)
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()}
return 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 !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
}

Powered by TurnKey Linux.