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.
377 lines
13 KiB
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))
|
|
}
|