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

303 lines
10 KiB

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/cms"
"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(capsule.AlgTest, 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
}
f.Seal, err = recordSeal(f, security, hb, v)
return err
}
// commitmentsOf returns control_commit and head_digest of the fixture f, from
// its control and its head (spec v0.11, §29.8).
func commitmentsOf(f *testkit.DKCFixture, head []byte) (cc, hd [32]byte, err error) {
cb, err := hex.DecodeString(f.ControlCBOR)
if err != nil {
return cc, hd, err
}
c, err := capsule.DecodeControl(cb, capsule.Format(f.Format))
if err != nil {
return cc, hd, err
}
defer clear(c.PayloadIdentity[:])
if cc, err = capsule.ControlCommit(c, capsule.Format(f.Format)); err != nil {
return cc, hd, err
}
return cc, capsule.HeadDigest(head), nil
}
func signerResults(lines []capsule.SignerLine) []testkit.FixtureSignerResult {
var out []testkit.FixtureSignerResult
for _, l := range lines {
r := testkit.FixtureSignerResult{Holder: l.Holder, Issuer: l.Issuer, Result: l.Result, Before: l.Before, SealReason: string(l.Reason)}
if !l.SealTime.IsZero() {
r.SealTime = l.SealTime.UTC().Format(time.RFC3339)
}
out = append(out, r)
}
return out
}
// recordSignature returns what the record of a fixture says about its
// signature: with 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);
// with alg 2, the same without a seed, and the certificates, SIGNERS and the
// result of each signer that Open gave. Nil when the fixture has no valid
// signature of alg 1 and no signature of alg 2 that Open judged.
func recordSignature(f *testkit.DKCFixture, security, head []byte, v capsule.Verdicts) (*testkit.FixtureSignature, error) {
content, value, err := capsule.SecurityKey2(security)
if err != nil {
return nil, nil // no signature
}
alg, key, _, err := capsule.DecodeAuthorSignature(content)
if err != nil {
return nil, nil
}
switch {
case alg == capsule.AlgEd25519 && (v.Signature == capsule.VerdictSignedOther || v.Signature == capsule.VerdictSignedSaved):
if f.Signature == nil {
return nil, errors.New("the fixture has a valid signature and its record no seed of the key")
}
case alg == capsule.AlgCMS && v.Detail != nil:
default:
return nil, nil
}
cc, hd, err := commitmentsOf(f, head)
if err != nil {
return nil, err
}
var sd [32]byte
sig := &testkit.FixtureSignature{Alg: int(alg), SignatureValue: hex.EncodeToString(value), SecurityKey2: hex.EncodeToString(content)}
if alg == capsule.AlgEd25519 {
sd = capsule.SignersDigest(capsule.AlgEd25519, nil)
sig.SecretSeed = f.Signature.SecretSeed
if sig.AuthorKey, err = bech32.Encode("dkauthor", v.AuthorKey[:]); err != nil {
return nil, err
}
} else {
sd = capsule.SignersDigest(capsule.AlgCMS, key)
sig.Signers = hex.EncodeToString(key)
parsed, err := cms.ParseSignature(value)
if err != nil {
return nil, err
}
for _, c := range parsed.Certs {
sig.Certificates = append(sig.Certificates, hex.EncodeToString(c.Raw))
}
sig.Results, sig.Foreign = signerResults(v.Detail.Signers), signerResults(v.Detail.Foreign)
}
msg := capsule.AuthorMessage(cc, hd, sd)
sig.ControlCommit, sig.HeadDigest, sig.SignersDigest = hex.EncodeToString(cc[:]), hex.EncodeToString(hd[:]), hex.EncodeToString(sd[:])
sig.AuthorMessage, sig.AuthorCode = string(msg), capsule.AuthorCode(msg)
return sig, nil
}
// recordSeal returns what the record of a fixture says about its seal of
// seal_type 2 when Open found it valid (S4 or S5): SEAL_SUBJECT, the token
// and the authority and the time that the verdict names.
func recordSeal(f *testkit.DKCFixture, security, head []byte, v capsule.Verdicts) (*testkit.FixtureSeal, error) {
if (v.Seal != capsule.VerdictSealed && v.Seal != capsule.VerdictSealedLate) || v.Detail == nil {
return nil, nil
}
typ, token, err := capsule.SecurityKey3(security)
if err != nil {
return nil, err
}
cc, hd, err := commitmentsOf(f, head)
if err != nil {
return nil, err
}
var part []byte
if content, _, err := capsule.SecurityKey2(security); err == nil {
part = content
}
subject := capsule.SealSubject(cc, hd, capsule.SigPart(part))
return &testkit.FixtureSeal{
SealType: int(typ), SealSubject: hex.EncodeToString(subject[:]), Token: hex.EncodeToString(token),
Holder: v.Detail.SealHolder, Time: v.Detail.SealTime.UTC().Format(time.RFC3339),
}, 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
}

Powered by TurnKey Linux.