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.
234 lines
7.9 KiB
234 lines
7.9 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/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
|
|
}
|