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-App/scripts/capsule-go-verdicts.go

594 lines
21 KiB

//go:build ignore
// Prints src/lib/dkc/testing/capsule-vectors.json: the verdicts of the Go
// reference on what scripts/capsule-ts-samples.mjs wrote with the writer of
// this repository (plan of phase 3, decision 11 and section 8, point 9):
//
// - samples: each .dkc, of format 2 or 3, is inspected with capsule.Inspect
// and opened with capsule.Open and the published signature of its round,
// with each credential alone and with all of them, and the verdict, the
// format, L, the padding rule and P are recorded; in format 2 the SHA-256
// of the content, and in format 3 the files that a Sink receives, with
// their SHA-256, the head encoded again with capsule.EncodeHead, the
// verdicts of the security area and the size of the area. Its
// PUBLIC_HEADER, its CONTROL_CBOR, opened layer by layer with the
// identities of agewrap, and its .dkk are encoded again by the reference
// and compared with the bytes written, the public note that
// Header.PublicNote reads in its PUBLIC_HEADER is recorded, null without
// one, and so is the number of stanzas of its INNER_ACCESS_AGE.
// - mixes: the code and the step at which capsule.Open fails.
// - encoders: capsule.EncodeHeader, capsule.EncodeControl (format 2) and
// AccessKey.MarshalBody on every input, compared with the bytes of the
// TypeScript library.
// - recipients: age.ParseX25519Recipient, then agewrap.CheckX25519Recipient,
// on every string.
// - errors: the text of capsule.Encrypt for each invalid option, as a
// generator of test vectors, the only writer of format 2 (spec §62.1
// rule 1).
// - note_errors: the text of capsule.EncryptFiles for each public note that
// breaks the rules of spec §24.1.
//
// Run it from a scratch module that requires the reference implementation
// (replace g.activething.com/go/DateKeys => ../datekeys-go, GOFLAGS=-mod=mod
// and the go directive of the reference, so that its toolchain is used),
// passing the output of capsule-ts-samples.mjs:
//
// go run capsule-go-verdicts.go ts-samples.json > capsule-vectors.json
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"maps"
"os"
"runtime/debug"
"slices"
"strings"
"time"
"filippo.io/age"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/accesskey"
"g.activething.com/go/DateKeys/agewrap"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/codec/bech32"
"g.activething.com/go/DateKeys/datekey"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/provider"
)
// The published Quicknet signatures of the rounds of the samples, as in the
// fixtures; provider.Verify checks them in capsule.Open.
var published = map[uint64]string{
1000: "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39",
1001: "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41",
2000: "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e",
}
type extJSON struct {
ID string `json:"id"`
Version uint64 `json:"version"`
Data *string `json:"data,omitempty"`
}
type sampleIn struct {
Name string `json:"name"`
Round uint64 `json:"round"`
Policy string `json:"policy"`
Format int `json:"format"`
Length uint64 `json:"length"`
Padding int `json:"padding"`
PaddedLength uint64 `json:"padded_length"`
// Format 2: the SHA-256 of the content. Format 3: the head written and
// its files.
ContentSHA256 string `json:"content_sha256,omitempty"`
HeadCBOR string `json:"head_cbor,omitempty"`
Files []fileJSON `json:"files,omitempty"`
Identities []string `json:"identities"`
DKK string `json:"dkk,omitempty"`
Known []extJSON `json:"known"`
// PublicNote is the public note that the writer was given, if any.
PublicNote *string `json:"public_note,omitempty"`
DKC string `json:"dkc"`
}
// A file of a format 3 capsule: its path, size, SHA-256 and mtime.
type fileJSON struct {
Path string `json:"path"`
Size uint64 `json:"size"`
SHA256 string `json:"sha256"`
MTime *uint64 `json:"mtime,omitempty"`
}
type encoderIn struct {
Header struct {
CapsuleID string `json:"capsule_id"`
Round uint64 `json:"round"`
Policy int `json:"policy"`
Critical []extJSON `json:"critical"`
Noncritical []extJSON `json:"noncritical"`
} `json:"header"`
Control struct {
HeaderBinding string `json:"header_binding"`
PayloadIdentity string `json:"payload_identity"`
PayloadLength uint64 `json:"payload_length"`
Padding int `json:"padding"`
Critical []extJSON `json:"critical"`
Noncritical []extJSON `json:"noncritical"`
} `json:"control"`
DKK struct {
CredentialID string `json:"credential_id"`
CapsuleID string `json:"capsule_id"`
Material string `json:"material"`
CapsuleDigest *string `json:"capsule_digest,omitempty"`
Critical []extJSON `json:"critical"`
Noncritical []extJSON `json:"noncritical"`
} `json:"dkk"`
HeaderCBOR string `json:"header_cbor"`
ControlCBOR string `json:"control_cbor"`
DKKBody string `json:"dkk_body"`
}
type errorCase struct {
Name string `json:"name"`
Policy int `json:"policy"`
Recipients []string `json:"recipients,omitempty"`
Portable bool `json:"portable,omitempty"`
UnlockAt string `json:"unlock_at"`
Now string `json:"now"`
Length int64 `json:"length"`
Padding int `json:"padding,omitempty"`
Critical []extJSON `json:"critical,omitempty"`
Noncritical []extJSON `json:"noncritical,omitempty"`
ControlCritical []extJSON `json:"control_critical,omitempty"`
ControlNoncritical []extJSON `json:"control_noncritical,omitempty"`
ChainHashFlip bool `json:"chain_hash_flip,omitempty"`
PublicNote string `json:"public_note,omitempty"`
TS string `json:"ts"`
Go string `json:"go"`
}
// noteError is a public note that breaks the rules of spec §24.1, and the
// texts of capsule.EncryptFiles and of the TypeScript library for it.
type noteError struct {
Name string `json:"name"`
Note string `json:"note"`
TS string `json:"ts"`
Go string `json:"go"`
}
type input struct {
Generator string `json:"generator"`
Samples []sampleIn `json:"samples"`
Mixes []struct {
Name string `json:"name"`
Round uint64 `json:"round"`
DKC string `json:"dkc"`
} `json:"mixes"`
Encoders struct {
Seed int `json:"seed"`
Count int `json:"count"`
SHA256 string `json:"sha256"`
Cases []encoderIn `json:"cases"`
} `json:"encoders"`
Recipients []string `json:"recipients"`
Errors []errorCase `json:"errors"`
NoteErrors []noteError `json:"note_errors"`
}
// The verdict of one opening: "ok" with what was delivered, or the code and
// the step of the failure.
type openVerdict struct {
Credentials string `json:"credentials"`
Result string `json:"result"`
Step int `json:"step,omitempty"`
Opened *openedOut `json:"opened,omitempty"`
}
type openedOut struct {
ContentSHA256 string `json:"content_sha256,omitempty"`
Format int `json:"format"`
Length uint64 `json:"length"`
Padding int `json:"padding"`
PaddedLength uint64 `json:"padded_length"`
Files []fileJSON `json:"files,omitempty"`
HeadCBOR string `json:"head_cbor,omitempty"`
Verdicts []string `json:"verdicts,omitempty"`
AreaLen uint32 `json:"area_len,omitempty"`
}
// sink keeps the files of a format 3 capsule in memory.
type sink struct {
files []*bytes.Buffer
}
type buffer struct{ *bytes.Buffer }
func (buffer) Close() error { return nil }
func (s *sink) Begin(*capsule.Head) error { return nil }
func (s *sink) Create(int) (io.WriteCloser, error) {
b := &bytes.Buffer{}
s.files = append(s.files, b)
return buffer{b}, nil
}
func (s *sink) Commit() error { return nil }
func (s *sink) Abort() { s.files = nil }
type sampleOut struct {
sampleIn
Go struct {
Inspect string `json:"inspect"`
Opens []openVerdict `json:"opens"`
InnerStanzas int `json:"inner_stanzas"`
Reencoded bool `json:"reencoded"`
PublicNote *string `json:"public_note"`
} `json:"go"`
}
type mixOut struct {
Name string `json:"name"`
Round uint64 `json:"round"`
DKC string `json:"dkc"`
Go struct {
Code string `json:"code"`
Step int `json:"step"`
} `json:"go"`
}
type recipientOut struct {
String string `json:"string"`
Parse string `json:"parse"`
Check string `json:"check,omitempty"`
}
type output struct {
Description string `json:"description"`
Generator string `json:"generator"`
SamplesGenerator string `json:"samples_generator"`
Libraries string `json:"libraries"`
Releases []releaseOut `json:"releases"`
Samples []sampleOut `json:"samples"`
Mixes []mixOut `json:"mixes"`
Encoders encodersOut `json:"encoders"`
Recipients []recipientOut `json:"recipients"`
Errors []errorCase `json:"errors"`
NoteErrors []noteError `json:"note_errors"`
}
type releaseOut struct {
Round uint64 `json:"round"`
Signature string `json:"signature"`
}
type encodersOut struct {
Seed int `json:"seed"`
Count int `json:"count"`
SHA256 string `json:"sha256"`
Equal int `json:"equal"`
Differences []string `json:"differences"`
}
func must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
func unhex(s string) []byte { return must(hex.DecodeString(s)) }
func exts(list []extJSON) []extension.Extension {
var out []extension.Extension
for _, e := range list {
x := extension.Extension{ID: e.ID, Version: e.Version}
if e.Data != nil {
x.Data = unhex(*e.Data)
if x.Data == nil {
x.Data = []byte{}
}
}
out = append(out, x)
}
return out
}
func source(round uint64) provider.ReleaseSource {
return provider.ReleaseSourceFunc(func(_ context.Context, _ *profile.Profile, c provider.Condition) (provider.Release, error) {
return provider.Release{Round: c.Round, Signature: unhex(published[c.Round])}, nil
})
}
func opened(dkc []byte, round uint64, reg profile.Registry, ex extension.Registry, ids []age.Identity, dkk []byte) openVerdict {
var out bytes.Buffer
files := &sink{}
o := capsule.OpenOptions{Registry: reg, Extensions: ex, Source: source(round), Identities: ids, Sink: files, Now: func() time.Time {
return time.Unix(1692803367+int64(round-1)*3, 0)
}}
if dkk != nil {
o.AccessKeyFile = bytes.NewReader(dkk)
}
res, err := capsule.Open(context.Background(), &out, bytes.NewReader(dkc), o)
v := openVerdict{Result: "ok"}
if err != nil {
v.Result = datekeys.Code(err)
if v.Result == "" {
v.Result = "error: " + err.Error()
}
if res != nil && res.Inspection != nil && len(res.Inspection.Checks) > 0 {
v.Step = res.Inspection.Checks[len(res.Inspection.Checks)-1].Step
}
return v
}
v.Opened = &openedOut{Format: int(res.Format), Length: res.PayloadLength, Padding: int(res.Padding), PaddedLength: res.PaddedLength}
if res.Format != capsule.Format3 {
sum := sha256.Sum256(out.Bytes())
v.Opened.ContentSHA256 = hex.EncodeToString(sum[:])
return v
}
for i, f := range res.Head.Files {
sum := sha256.Sum256(files.files[i].Bytes())
fj := fileJSON{Path: f.Path, Size: f.Size, SHA256: hex.EncodeToString(sum[:])}
if f.HasMTime {
mtime := f.MTime
fj.MTime = &mtime
}
v.Opened.Files = append(v.Opened.Files, fj)
}
v.Opened.HeadCBOR = hex.EncodeToString(must(capsule.EncodeHead(res.Head)))
v.Opened.Verdicts = []string{string(res.Verdicts.Signature), string(res.Verdicts.Seal)}
v.Opened.AreaLen = res.AreaLen
return v
}
func main() {
var in input
must(0, json.Unmarshal(must(os.ReadFile(os.Args[1])), &in))
reg := must(profile.Default())
p := profile.Quicknet()
out := output{
Description: "Verdicts of the Go reference on capsules, mixes, encodings, recipients and invalid options of the TypeScript writer (plan of phase 3, section 8, point 9); see the header of scripts/capsule-go-verdicts.go.",
Generator: "scripts/capsule-go-verdicts.go",
SamplesGenerator: in.Generator,
Libraries: libraries(),
}
for _, r := range slices.Sorted(maps.Keys(published)) {
out.Releases = append(out.Releases, releaseOut{r, published[r]})
}
for _, s := range in.Samples {
dkc := unhex(s.DKC)
known := extension.Set{}
for _, k := range s.Known {
known[k.ID] = append(known[k.ID], k.Version)
}
so := sampleOut{sampleIn: s}
if _, err := capsule.Inspect(bytes.NewReader(dkc), capsule.InspectOptions{Registry: reg, Extensions: known}); err != nil {
so.Go.Inspect = datekeys.Code(err)
} else {
so.Go.Inspect = "ok"
}
var ids []age.Identity
for i, raw := range s.Identities {
id := must(agewrap.X25519IdentityFromRaw(unhex(raw)))
ids = append(ids, id)
v := opened(dkc, s.Round, reg, known, []age.Identity{id}, nil)
v.Credentials = fmt.Sprintf("identity %d", i)
so.Go.Opens = append(so.Go.Opens, v)
}
var dkk []byte
if s.DKK != "" {
dkk = unhex(s.DKK)
v := opened(dkc, s.Round, reg, known, nil, dkk)
v.Credentials = ".dkk"
so.Go.Opens = append(so.Go.Opens, v)
}
v := opened(dkc, s.Round, reg, known, ids, dkk)
v.Credentials = "all"
if ids == nil && dkk == nil {
v.Credentials = "none"
}
so.Go.Opens = append(so.Go.Opens, v)
so.Go.InnerStanzas, so.Go.Reencoded, so.Go.PublicNote = layers(dkc, s, p, dkk)
out.Samples = append(out.Samples, so)
}
for _, m := range in.Mixes {
v := opened(unhex(m.DKC), m.Round, reg, extension.Set{}, nil, nil)
mo := mixOut{Name: m.Name, Round: m.Round, DKC: m.DKC}
mo.Go.Code, mo.Go.Step = v.Result, v.Step
out.Mixes = append(out.Mixes, mo)
}
out.Encoders = encodersOut{Seed: in.Encoders.Seed, Count: in.Encoders.Count, SHA256: in.Encoders.SHA256, Differences: []string{}}
for i, c := range in.Encoders.Cases {
if d := encoders(c); d != "" {
out.Encoders.Differences = append(out.Encoders.Differences, fmt.Sprintf("case %d: %s", i, d))
} else {
out.Encoders.Equal++
}
}
for _, s := range in.Recipients {
r := recipientOut{String: s, Parse: "ok"}
x, err := age.ParseX25519Recipient(s)
if err != nil {
r.Parse = err.Error()
} else if err := agewrap.CheckX25519Recipient(x); err != nil {
r.Check = err.Error()
} else {
r.Check = "ok"
}
out.Recipients = append(out.Recipients, r)
}
for _, c := range in.Errors {
c.Go = encryptError(c)
out.Errors = append(out.Errors, c)
}
for _, c := range in.NoteErrors {
c.Go = noteText(c.Note)
out.NoteErrors = append(out.NoteErrors, c)
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
must(0, enc.Encode(out))
}
// layers opens SEALED_CONTROL of a sample with the published release and, in
// time_and_key, with its first credential, and encodes PUBLIC_HEADER,
// CONTROL_CBOR and the .dkk again. It returns the number of stanzas of
// INNER_ACCESS_AGE, whether every encoding equals the bytes written, and the
// public note of PUBLIC_HEADER, nil when there is none that is usable.
func layers(dkc []byte, s sampleIn, p *profile.Profile, dkk []byte) (int, bool, *string) {
pre := must(capsule.ParsePrelude(dkc))
header := dkc[capsule.PreludeSize : capsule.PreludeSize+int(pre.PublicHeaderLen)]
sealed := dkc[capsule.PreludeSize+int(pre.PublicHeaderLen) : capsule.PreludeSize+int(pre.PublicHeaderLen)+int(pre.SealedControlLen)]
same := true
h := must(capsule.DecodeHeader(header))
same = same && bytes.Equal(must(capsule.EncodeHeader(h)), header)
var note *string
if text, ok := h.PublicNote(); ok {
note = &text
}
tid := must(agewrap.NewTimeIdentity(p, s.Round, provider.Release{Round: s.Round, Signature: unhex(published[s.Round])}))
inner := must(io.ReadAll(must(age.Decrypt(bytes.NewReader(sealed), tid))))
control := inner
stanzas := 0
if s.Policy == "time_and_key" {
st := must(agewrap.Stanzas(bytes.NewReader(inner)))
stanzas = len(st)
var id age.Identity
if len(s.Identities) > 0 {
id = must(agewrap.X25519IdentityFromRaw(unhex(s.Identities[0])))
} else {
k := must(accesskey.Decode(bytes.NewReader(dkk)))
id = must(agewrap.X25519IdentityFromRaw(k.Material))
}
aid := must(agewrap.NewAccessIdentity(agewrap.AccessSlots, id))
control = must(io.ReadAll(must(age.Decrypt(bytes.NewReader(inner), aid))))
}
c := must(capsule.DecodeControl(control, pre.Format))
same = same && bytes.Equal(must(capsule.EncodeControl(c, pre.Format)), control)
if dkk != nil {
k := must(accesskey.Decode(bytes.NewReader(dkk)))
var b bytes.Buffer
must(0, accesskey.Encode(&b, k))
same = same && bytes.Equal(b.Bytes(), dkk)
}
return stanzas, same, note
}
// encoders encodes one input with the reference and names the first encoding
// that differs from the bytes of the TypeScript library, or returns "".
func encoders(c encoderIn) string {
var h capsule.Header
copy(h.CapsuleID[:], unhex(c.Header.CapsuleID))
h.DateKey = datekey.DateKey{ProfileID: "datekeys:quicknet:v1", Round: c.Header.Round}
h.Policy = capsule.Policy(c.Header.Policy)
h.Critical, h.Noncritical = exts(c.Header.Critical), exts(c.Header.Noncritical)
hb, err := capsule.EncodeHeader(&h)
if err != nil || hex.EncodeToString(hb) != c.HeaderCBOR {
return fmt.Sprintf("PUBLIC_HEADER (%v)", err)
}
var ctrl capsule.Control
copy(ctrl.HeaderBinding[:], unhex(c.Control.HeaderBinding))
copy(ctrl.PayloadIdentity[:], unhex(c.Control.PayloadIdentity))
ctrl.PayloadLength, ctrl.Padding = c.Control.PayloadLength, capsule.Padding(c.Control.Padding)
ctrl.Critical, ctrl.Noncritical = exts(c.Control.Critical), exts(c.Control.Noncritical)
cb, err := capsule.EncodeControl(&ctrl, capsule.Format2)
if err != nil || hex.EncodeToString(cb) != c.ControlCBOR {
return fmt.Sprintf("CONTROL_CBOR (%v)", err)
}
k := accesskey.AccessKey{Type: accesskey.TypeX25519, Material: unhex(c.DKK.Material)}
copy(k.CredentialID[:], unhex(c.DKK.CredentialID))
copy(k.CapsuleID[:], unhex(c.DKK.CapsuleID))
if c.DKK.CapsuleDigest != nil {
k.Verification = &accesskey.Verification{CapsuleDigest: unhex(*c.DKK.CapsuleDigest)}
}
k.Critical, k.Noncritical = exts(c.DKK.Critical), exts(c.DKK.Noncritical)
kb, err := k.MarshalBody()
if err != nil || hex.EncodeToString(kb) != c.DKKBody {
return fmt.Sprintf(".dkk BODY_CBOR (%v)", err)
}
return ""
}
// encryptError runs capsule.Encrypt with the options of a case and returns
// its error text, or "ok".
func encryptError(c errorCase) string {
p := profile.Quicknet()
if c.ChainHashFlip {
p.ChainHash[0] ^= 1
}
var recipients []age.Recipient
for _, raw := range c.Recipients {
s := must(bech32.Encode("age", unhex(raw)))
recipients = append(recipients, must(age.ParseX25519Recipient(s)))
}
opts := capsule.EncryptOptions{
Profile: p,
UnlockAt: must(time.Parse(time.RFC3339Nano, c.UnlockAt)),
Policy: capsule.Policy(c.Policy),
Recipients: recipients,
NewPortableKey: c.Portable,
Length: c.Length,
Padding: capsule.Padding(c.Padding),
Critical: exts(c.Critical),
Noncritical: exts(c.Noncritical),
ControlCritical: exts(c.ControlCritical),
ControlNoncritical: exts(c.ControlNoncritical),
PublicNote: c.PublicNote,
TestVectors: true,
Now: func() time.Time { return must(time.Parse(time.RFC3339Nano, c.Now)) },
}
src := bytes.NewReader(make([]byte, min(c.Length, 1)))
if _, err := capsule.Encrypt(io.Discard, src, opts); err != nil {
return err.Error()
}
return "ok"
}
// noteText runs capsule.EncryptFiles on a file of one byte, for round 1000
// with now at the genesis, with the public note given, and returns its error
// text, or "ok".
func noteText(note string) string {
genesis := time.Unix(1692803367, 0).UTC()
opts := capsule.EncryptOptions{
Profile: profile.Quicknet(),
UnlockAt: genesis.Add(999 * 3 * time.Second),
Policy: capsule.TimeOnly,
PublicNote: note,
Now: func() time.Time { return genesis },
}
src := []capsule.Source{{Path: "a.txt", Size: 1, Open: func() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("x")), nil }}}
if _, err := capsule.EncryptFiles(io.Discard, src, opts); err != nil {
return err.Error()
}
return "ok"
}
func libraries() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return ""
}
var parts []string
for _, d := range info.Deps {
switch d.Path {
case "filippo.io/age", "github.com/drand/drand/v2", "github.com/drand/kyber", "github.com/drand/kyber-bls12381", "github.com/drand/tlock":
parts = append(parts, d.Path+" "+d.Version)
}
}
return strings.Join(parts, ", ")
}

Powered by TurnKey Linux.