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-dart/tool/capsule_interop_go_verdicts.go

377 lines
13 KiB

//go:build ignore
// Opens with Go the capsules that the writer of datekeys-dart wrote, stage
// 6b of docs/PLAN_dart.md, and writes test/vectors/capsule_interop.json and
// capsule_interop.g.dart: each recipe of
// tool/capsule_interop_dart_samples.dart with the length and the SHA-256 of
// its capsule and its .dkk, and
//
// - inspect: capsule.Inspect, its format, policy, DateKey, public note and
// the number of stanzas of INNER_ACCESS_AGE;
// - opens: capsule.Open with the published release of the round and each
// credential alone (the identity of each label, the key of words of the
// capsule_id of its header, the .dkk), all together and none: the result
// and the step, and of a capsule opened, its files with their SHA-256,
// the head encoded again, the verdicts and the area;
// - reencoded: whether PUBLIC_HEADER, CONTROL_CBOR, opened layer by layer
// with agewrap, and the .dkk encode again to the bytes written;
// - go_same: for a sample written with the seeded source, whether
// capsule.EncryptFiles writes the same bytes while crypto/rand reads the
// same keystream.
//
// It imports internal/testkit, so it runs in an export of datekeys-go made
// with git archive, without changing the repository, on the branch v0.12 at
// c531e93, after the Dart tool wrote the samples:
//
// commit=$(git -C ../datekeys-go rev-parse v0.12)
// root=$PWD
// tmp=$(mktemp -d)
// dart run tool/capsule_interop_dart_samples.dart "$tmp/samples"
// git -C ../datekeys-go archive "$commit" | tar -x -C "$tmp"
// cp tool/capsule_interop_go_verdicts.go "$tmp"
// (cd "$tmp" && go run ./capsule_interop_go_verdicts.go -source "$commit" \
// -samples "$tmp/samples" -out "$root/test/vectors")
// rm -rf "$tmp"
package main
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
"time"
"filippo.io/age"
"golang.org/x/crypto/chacha20"
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/datekey"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/wordkey"
)
type obj = map[string]any
func h(b []byte) string { return hex.EncodeToString(b) }
func sum(b []byte) string {
s := sha256.Sum256(b)
return h(s[:])
}
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func unhex(s string) []byte { return must(hex.DecodeString(s)) }
type extIn struct {
ID string `json:"id"`
Version uint64 `json:"version"`
Data *string `json:"data,omitempty"`
}
type fileIn struct {
Path string `json:"path"`
Text *string `json:"text,omitempty"`
Pattern int `json:"pattern,omitempty"`
MTime []int64 `json:"mtime,omitempty"`
}
// sample is a recipe of test/capsule_interop_support.dart, in the form of
// the recipes of tool/capsule_writer_go_vectors_test.go, with its files.
type sample struct {
Name string `json:"name"`
Seed string `json:"seed"`
Random string `json:"random"`
Round uint64 `json:"round"`
Policy string `json:"policy,omitempty"`
Identities []string `json:"identities,omitempty"`
Portable bool `json:"portable,omitempty"`
Words []string `json:"words,omitempty"`
Padding int `json:"padding,omitempty"`
Files []fileIn `json:"files,omitempty"`
Comment string `json:"comment,omitempty"`
Author string `json:"author,omitempty"`
Note string `json:"note,omitempty"`
Critical []extIn `json:"critical,omitempty"`
Noncritical []extIn `json:"noncritical,omitempty"`
ControlCritical []extIn `json:"control_critical,omitempty"`
ControlNoncritical []extIn `json:"control_noncritical,omitempty"`
HeadCritical []extIn `json:"head_critical,omitempty"`
HeadNoncritical []extIn `json:"head_noncritical,omitempty"`
TestVectors bool `json:"test_vectors,omitempty"`
TestAreaLen uint32 `json:"test_area_len,omitempty"`
Node *bool `json:"node,omitempty"`
File string `json:"file"`
DKK string `json:"dkk,omitempty"`
}
func identity(label string) *age.X25519Identity {
s := sha256.Sum256([]byte("identity " + label))
return must(agewrap.X25519IdentityFromRaw(s[:]))
}
func exts(list []extIn) []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)
}
out = append(out, x)
}
return out
}
func pattern(n int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = byte(31*i + 7)
}
return b
}
func extSet(s sample) extension.Set {
set := extension.Set{}
for _, l := range [][]extIn{s.Critical, s.Noncritical, s.ControlCritical, s.ControlNoncritical, s.HeadCritical, s.HeadNoncritical} {
for _, e := range l {
set[e.ID] = append(set[e.ID], e.Version)
}
}
return set
}
// seeded is crypto/rand.Reader reading the keystream of SeededRandomSource.
type seeded struct{ c *chacha20.Cipher }
func (s *seeded) Read(p []byte) (int, error) {
clear(p)
s.c.XORKeyStream(p, p)
return len(p), nil
}
// goWrites writes the sample with capsule.EncryptFiles while crypto/rand
// reads the keystream of its seed.
func goWrites(s sample) []byte {
q := profile.Quicknet()
opts := capsule.EncryptOptions{
Profile: q, UnlockAt: must(datekey.RoundTime(q, s.Round)),
Now: func() time.Time { return time.Unix(q.GenesisTime, 0) },
NewPortableKey: s.Portable, Words: s.Words, Padding: capsule.Padding(s.Padding),
Critical: exts(s.Critical), Noncritical: exts(s.Noncritical),
ControlCritical: exts(s.ControlCritical), ControlNoncritical: exts(s.ControlNoncritical),
HeadCritical: exts(s.HeadCritical), HeadNoncritical: exts(s.HeadNoncritical),
Comment: s.Comment, Author: s.Author, PublicNote: s.Note,
TestVectors: s.TestVectors, TestAreaLen: s.TestAreaLen,
}
if s.Policy == "time_and_key" {
opts.Policy = capsule.TimeAndKey
}
for _, l := range s.Identities {
opts.Recipients = append(opts.Recipients, identity(l).Recipient())
}
var sources []capsule.Source
for _, f := range s.Files {
content := pattern(f.Pattern)
if f.Text != nil {
content = []byte(*f.Text)
}
var mtime time.Time
if f.MTime != nil {
mtime = time.Unix(f.MTime[0], f.MTime[1])
}
sources = append(sources, capsule.Source{Path: f.Path, Size: int64(len(content)), ModTime: mtime,
Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(content)), nil }})
}
key := sha256.Sum256([]byte(s.Seed))
old := rand.Reader
rand.Reader = &seeded{must(chacha20.NewUnauthenticatedCipher(key[:], make([]byte, chacha20.NonceSize)))}
defer func() { rand.Reader = old }()
var b bytes.Buffer
must(capsule.EncryptFiles(&b, sources, opts))
return b.Bytes()
}
func opened(s sample, dkc []byte, ids []age.Identity, dkk []byte) obj {
files := &testkit.MemorySink{}
o := capsule.OpenOptions{
Registry: testkit.Registry(), Extensions: extSet(s),
Source: testkit.NewSource(testkit.Release(1000), testkit.Release(1001)),
Identities: ids, Sink: files,
Now: func() time.Time { return time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC) },
}
if dkk != nil {
o.AccessKeyFile = bytes.NewReader(dkk)
}
res, err := capsule.Open(context.Background(), nil, bytes.NewReader(dkc), o)
v := obj{}
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["result"] = "ok"
fs := []obj{}
for i, f := range res.Head.Files {
fj := obj{"path": f.Path, "size": f.Size, "sha256": sum(files.Files[i])}
if f.HasMTime {
fj["mtime"] = f.MTime
}
fs = append(fs, fj)
}
v["files"] = fs
v["comment"] = res.Head.Comment
v["author"] = res.Head.Author
v["head"] = h(must(capsule.EncodeHead(res.Head)))
v["verdicts"] = []string{string(res.Verdicts.Signature), string(res.Verdicts.Seal)}
v["area_len"] = res.AreaLen
v["length"] = res.PayloadLength
v["padded_length"] = res.PaddedLength
return v
}
func main() {
out := flag.String("out", "", "where the vectors go")
src := flag.String("source", "", "the commit of datekeys-go")
dir := flag.String("samples", "", "the directory of the samples")
flag.Parse()
if *out == "" || *src == "" || *dir == "" {
log.Fatal("usage: -source <commit> -samples <dir> -out <dir>")
}
var in struct {
Samples []sample `json:"samples"`
}
must(0, json.Unmarshal(must(os.ReadFile(filepath.Join(*dir, "samples.json"))), &in))
q := profile.Quicknet()
var results, node []obj
for _, s := range in.Samples {
dkc := must(os.ReadFile(filepath.Join(*dir, s.File)))
var dkk []byte
if s.DKK != "" {
dkk = must(os.ReadFile(filepath.Join(*dir, s.DKK)))
}
insp, err := capsule.Inspect(bytes.NewReader(dkc), capsule.InspectOptions{Registry: testkit.Registry(), Extensions: extSet(s)})
if err != nil {
log.Fatalf("%s: %v", s.Name, err)
}
note, _ := insp.Header.PublicNote()
capsuleID := insp.Header.CapsuleID
r := obj{
"name": s.Name, "length_dkc": len(dkc), "sha256": sum(dkc),
"inspect": obj{
"format": int(insp.Prelude.Format), "policy": insp.Header.Policy.String(),
"datekey": insp.Header.DateKey.String(), "note": note,
},
}
if dkk != nil {
r["dkk_sha256"] = sum(dkk)
}
var opens []obj
with := func(name string, ids []age.Identity, k []byte) {
v := opened(s, dkc, ids, k)
v["with"] = name
opens = append(opens, v)
}
round := insp.Header.DateKey.Round
tid := must(agewrap.NewTimeIdentity(q, round, testkit.Release(round)))
pre := insp.Prelude
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 := bytes.Equal(must(capsule.EncodeHeader(insp.Header)), header)
control := must(io.ReadAll(must(age.Decrypt(bytes.NewReader(sealed), tid))))
if s.Policy != "time_and_key" {
with("time", nil, nil)
} else {
var all []age.Identity
for _, l := range s.Identities {
with("identity "+l, []age.Identity{identity(l)}, nil)
all = append(all, identity(l))
}
if len(s.Words) != 0 {
id := must(wordkey.Identity(s.Words, q.ChainHash[:], round, capsuleID[:]))
with("words", []age.Identity{id}, nil)
all = append(all, id)
}
if dkk != nil {
with("portable", nil, dkk)
}
with("all", all, dkk)
with("none", nil, nil)
stanzas := must(agewrap.Stanzas(bytes.NewReader(control)))
r["inner_stanzas"] = len(stanzas)
var id age.Identity
if len(all) > 0 {
id = all[0]
} else {
k := must(accesskey.Decode(bytes.NewReader(dkk)))
id = must(agewrap.X25519IdentityFromRaw(k.Material))
}
control = must(io.ReadAll(must(age.Decrypt(bytes.NewReader(control), must(agewrap.NewAccessIdentity(agewrap.AccessSlots, id))))))
}
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)
r["dkk_capsule_digest"] = bytes.Equal(k.Verification.CapsuleDigest, must(hex.DecodeString(sum(dkc))))
}
r["reencoded"] = same
r["opens"] = opens
if s.Random == "seeded" {
r["go_same"] = bytes.Equal(goWrites(s), dkc)
}
results = append(results, r)
if s.Node == nil || *s.Node {
node = append(node, r)
}
fmt.Printf("%s: %d bytes\n", s.Name, len(dkc))
}
doc := func(results []obj) []byte {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
must(0, enc.Encode(obj{
"description": "Capsules that datekeys-dart writes from the recipes of test/capsule_interop_support.dart, with SeededRandomSource or the CSPRNG of the platform, inspected and opened by capsule.Open of datekeys-go with each credential, all together and none, their layers encoded again, and, for a seeded one, whether capsule.EncryptFiles writes the same bytes (tool/capsule_interop_go_verdicts.go).",
"source": *src,
"go": runtime.Version(),
"samples": results,
}))
return buf.Bytes()
}
full := doc(results)
must(0, os.WriteFile(filepath.Join(*out, "capsule_interop.json"), full, 0o644))
dart := "// Generated by tool/capsule_interop_go_verdicts.go: the samples of\n" +
"// test/vectors/capsule_interop.json that are not marked node false, for the\n" +
"// tests that also run compiled to JavaScript. Do not edit.\n\n" +
"/// Part of test/vectors/capsule_interop.json.\n" +
"const capsuleInteropJson = r'''\n" + string(doc(node)) + "''';\n"
must(0, os.WriteFile(filepath.Join(*out, "capsule_interop.g.dart"), []byte(dart), 0o644))
fmt.Printf("wrote %d samples\n", len(results))
}

Powered by TurnKey Linux.