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.
497 lines
18 KiB
497 lines
18 KiB
// Writes src/lib/dkc/testing/signing-vectors.json, the interoperability of
|
|
// the hooks of the writer of this library with the Go reference at
|
|
// spec-v0.12: what capsule.EncryptFiles writes when it signs with alg 1 or
|
|
// alg 2 and seals with seal_type 2, and how Go reads what encryptFiles of
|
|
// this library writes.
|
|
//
|
|
// cases: for each recipe of this file, EncryptFiles runs while crypto/rand
|
|
// reads a ChaCha20 keystream under SHA-256(seed) and a zero nonce, and each
|
|
// draw is recorded in hexadecimal, in its order: the salt of the head,
|
|
// capsule_id, I_PAYLOAD, what age draws for the measured seal, the real seal
|
|
// and PAYLOAD_AGE. The hooks are those of the tests of package capsule
|
|
// (signed_test.go): an author key from a seed (alg 1), and the CMS signatures
|
|
// and RFC 3161 tokens of internal/cms/cmstest (alg 2 and seal_type 2), whose
|
|
// ECDSA and RSA draw from Go's internal generator, which only
|
|
// testing/cryptotest.SetGlobalRandom fixes, in a test binary: this file runs
|
|
// as a test. What each hook was given and returned is recorded, so that the
|
|
// tests of this library hand the writer the same signatures and tokens, check
|
|
// that it asks for them over the same messages, and write the same bytes with
|
|
// the same draws. For each capsule it records its length, its SHA-256, its
|
|
// SECURITY_CBOR and the size of its area, and what capsule.Open gives, with
|
|
// and without the author key saved under a label: the verdicts, the lines that
|
|
// show them, the head and the files. For each error, its text.
|
|
//
|
|
// samples: with -samples, the capsules that scripts/signing-ts-samples.mjs
|
|
// writes with encryptFiles of this library, with its own random values and
|
|
// its own certificates, opened with capsule.Open, with their verdicts and
|
|
// lines.
|
|
//
|
|
// It imports internal packages, so it runs as a test in an export of
|
|
// datekeys-go made with git archive, which it does not change, never in the
|
|
// repository itself: at the tag spec-v0.12 when it was written, and at
|
|
// 4f78854, the draft v0.16, since. From the root of this repository:
|
|
//
|
|
// node scripts/signing-ts-samples.mjs > /tmp/ts-signing.json
|
|
// commit=$(git -C ../datekeys-go rev-parse '4f78854^{commit}')
|
|
// tmp=$(mktemp -d)
|
|
// git -C ../datekeys-go archive "$commit" | tar -x -C "$tmp"
|
|
// mkdir "$tmp/signingvectors"
|
|
// cp scripts/signing-go-vectors_test.go "$tmp/signingvectors/"
|
|
// (cd "$tmp/signingvectors" && go test -run TestSigningVectors -count=1 \
|
|
// -args -source "$commit" -samples /tmp/ts-signing.json \
|
|
// -out "$OLDPWD/src/lib/dkc/testing/signing-vectors.json")
|
|
// rm -rf "$tmp"
|
|
//
|
|
// The cases are the same on every run with Go 1.26.8; the samples are
|
|
// random, and frozen with what Go gives for them. For v0.16 the frozen
|
|
// samples were read again, with their "ts" as this library reads them now,
|
|
// and the tokens of the sealer, without accuracy, give S5 (spec v0.16,
|
|
// §29.11).
|
|
package signingvectors
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"testing"
|
|
"testing/cryptotest"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/chacha20"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/authorkey"
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
"g.activething.com/go/DateKeys/datekey"
|
|
"g.activething.com/go/DateKeys/internal/cms/cmstest"
|
|
"g.activething.com/go/DateKeys/internal/testkit"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
"g.activething.com/go/DateKeys/provider"
|
|
)
|
|
|
|
var (
|
|
sourceFlag = flag.String("source", "", "the commit of datekeys-go that this tree exports")
|
|
outFlag = flag.String("out", "", "the JSON file to write")
|
|
samplesFlag = flag.String("samples", "", "the output of scripts/signing-ts-samples.mjs")
|
|
)
|
|
|
|
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 {
|
|
panic(err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
func unhex(s string) []byte { return must(hex.DecodeString(s)) }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The recipes
|
|
|
|
type authorIn struct {
|
|
Seed string `json:"seed"` // hex, 32 bytes
|
|
Bad string `json:"bad,omitempty"` // zero_signature, short_key
|
|
}
|
|
|
|
type cmsIn struct {
|
|
Signers []string `json:"signers"` // names of the certificates that sign
|
|
Extra string `json:"extra,omitempty"` // a required signer that does not sign
|
|
Unsealed bool `json:"unsealed,omitempty"` // no seal in the signatures
|
|
Junk int `json:"junk,omitempty"` // bytes of an unsigned attribute; -1 for the most that still fails as too large
|
|
Error string `json:"error,omitempty"` // Sign fails with this text
|
|
Garbage bool `json:"garbage,omitempty"` // Sign returns bytes that are no signature
|
|
}
|
|
|
|
type sealerIn struct {
|
|
Error string `json:"error,omitempty"` // Seal fails with this text
|
|
Late bool `json:"late,omitempty"` // the authority seals an hour after the time of the round
|
|
}
|
|
|
|
type fileIn struct {
|
|
Path string `json:"path"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
type recipe struct {
|
|
Name string `json:"name"`
|
|
Seed string `json:"seed"`
|
|
Files []fileIn `json:"files,omitempty"`
|
|
Comment string `json:"comment,omitempty"`
|
|
Author string `json:"author,omitempty"`
|
|
AuthorKey *authorIn `json:"author_key,omitempty"`
|
|
CMS *cmsIn `json:"cms,omitempty"`
|
|
Sealer *sealerIn `json:"sealer,omitempty"`
|
|
LargeArea bool `json:"large_area,omitempty"`
|
|
TestAreaLen uint32 `json:"test_area_len,omitempty"`
|
|
}
|
|
|
|
const authorSeed = "a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0"
|
|
|
|
func recipes() []recipe {
|
|
files := []fileIn{{"nota.txt", "Hola.\n"}, {"fotos/año 2026.txt", "Una foto que no es una foto.\n"}}
|
|
key := func() *authorIn { return &authorIn{Seed: authorSeed} }
|
|
var out []recipe
|
|
add := func(r recipe) {
|
|
r.Seed = "signing " + r.Name
|
|
out = append(out, r)
|
|
}
|
|
// Capsules.
|
|
add(recipe{Name: "alg 1", Files: files, Comment: "Firmado con mi clave.", Author: "Ana López", AuthorKey: key()})
|
|
add(recipe{Name: "alg 1 and a seal", Files: files, AuthorKey: key(), Sealer: &sealerIn{}})
|
|
add(recipe{Name: "a seal alone", Files: files[:1], Sealer: &sealerIn{}})
|
|
add(recipe{Name: "alg 2, two signers, sealed", Files: files, Comment: "Firmado por los dos.", CMS: &cmsIn{Signers: []string{"Ana López", "Luis Gómez"}}})
|
|
add(recipe{Name: "alg 2, a large area", Files: files[:1], CMS: &cmsIn{Signers: []string{"Ana López"}, Junk: 40000}, LargeArea: true})
|
|
add(recipe{Name: "alg 1, a large area that does not widen", Comment: "Solo un comentario.", AuthorKey: key(), LargeArea: true})
|
|
add(recipe{Name: "alg 1, an area of 512", Files: files[:1], AuthorKey: key(), TestAreaLen: 512})
|
|
add(recipe{Name: "a seal after the date", Files: files[:1], AuthorKey: key(), Sealer: &sealerIn{Late: true}})
|
|
// Errors.
|
|
add(recipe{Name: "AuthorKey and CMSSigner", Files: files, AuthorKey: key(), CMS: &cmsIn{Signers: []string{"Ana López"}}})
|
|
add(recipe{Name: "CMSSigner and Sealer", Files: files, CMS: &cmsIn{Signers: []string{"Ana López"}}, Sealer: &sealerIn{}})
|
|
add(recipe{Name: "a test area and LargeArea", Files: files, AuthorKey: key(), TestAreaLen: 512, LargeArea: true})
|
|
add(recipe{Name: "an author key of 31 bytes", Files: files, AuthorKey: &authorIn{Seed: authorSeed, Bad: "short_key"}})
|
|
add(recipe{Name: "a signature of zeros", Files: files, AuthorKey: &authorIn{Seed: authorSeed, Bad: "zero_signature"}})
|
|
add(recipe{Name: "a signature of zeros and a seal", Files: files, AuthorKey: &authorIn{Seed: authorSeed, Bad: "zero_signature"}, Sealer: &sealerIn{}})
|
|
add(recipe{Name: "alg 2 that does not fit", Files: files, CMS: &cmsIn{Signers: []string{"Ana López"}, Junk: 40000}})
|
|
add(recipe{Name: "the signing application fails", Files: files, CMS: &cmsIn{Signers: []string{"Ana López"}, Error: "la persona canceló la firma"}})
|
|
add(recipe{Name: "the authority fails", Files: files, AuthorKey: key(), Sealer: &sealerIn{Error: "the authority does not answer"}})
|
|
add(recipe{Name: "alg 2 without a required signer", Files: files, CMS: &cmsIn{Signers: []string{"Ana López"}, Extra: "Luis Gómez"}})
|
|
add(recipe{Name: "alg 2 without seals", Files: files, CMS: &cmsIn{Signers: []string{"Luis Gómez"}, Unsealed: true}})
|
|
add(recipe{Name: "alg 2 that is not a signature", Files: files, CMS: &cmsIn{Signers: []string{"Ana López"}, Garbage: true}})
|
|
add(recipe{Name: "SIGNERS empty", Files: files, CMS: &cmsIn{}})
|
|
add(recipe{Name: "SIGNERS twice", Files: files, CMS: &cmsIn{Signers: []string{"Ana López", "Ana López"}}})
|
|
// Last, since its search draws from the generator of the test.
|
|
add(recipe{Name: "alg 2 too large for any area", Files: files[:1], CMS: &cmsIn{Signers: []string{"Ana López"}, Junk: -1}, LargeArea: true})
|
|
return out
|
|
}
|
|
|
|
// seeded is the crypto/rand.Reader of a case: the ChaCha20 keystream under
|
|
// SHA-256(seed) and a zero nonce. It records every draw.
|
|
type seeded struct {
|
|
c *chacha20.Cipher
|
|
draws [][]byte
|
|
}
|
|
|
|
func newSeeded(seed string) *seeded {
|
|
key := sha256.Sum256([]byte(seed))
|
|
return &seeded{c: must(chacha20.NewUnauthenticatedCipher(key[:], make([]byte, chacha20.NonceSize)))}
|
|
}
|
|
|
|
func (s *seeded) Read(p []byte) (int, error) {
|
|
clear(p)
|
|
s.c.XORKeyStream(p, p)
|
|
s.draws = append(s.draws, bytes.Clone(p))
|
|
return len(p), nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The keys and the hooks
|
|
|
|
type certs struct {
|
|
byName map[string]cmstest.Signer
|
|
tsa cmstest.Signer
|
|
}
|
|
|
|
var certFrom, certTo = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2040, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
func newCerts() *certs {
|
|
c := &certs{byName: map[string]cmstest.Signer{}}
|
|
c.byName["Ana López"] = cmstest.NewECDSA("Ana López", elliptic.P256(), certFrom, certTo)
|
|
c.byName["Luis Gómez"] = cmstest.NewRSA("Luis Gómez", 2048, certFrom, certTo)
|
|
c.tsa = cmstest.NewECDSA("TSA de prueba", elliptic.P256(), certFrom, certTo)
|
|
return c
|
|
}
|
|
|
|
type hooks struct{ rec obj }
|
|
|
|
func (k *hooks) set(name string, v any) { k.rec[name] = v }
|
|
|
|
type authorHook struct {
|
|
key *authorkey.Key
|
|
bad string
|
|
k *hooks
|
|
}
|
|
|
|
func (a *authorHook) Public() []byte {
|
|
pub := a.key.Public()
|
|
if a.bad == "short_key" {
|
|
pub = pub[:31]
|
|
}
|
|
a.k.set("author_public", h(pub))
|
|
return pub
|
|
}
|
|
|
|
func (a *authorHook) Sign(msg []byte) []byte {
|
|
sig := a.key.Sign(msg)
|
|
if a.bad == "zero_signature" {
|
|
sig = make([]byte, ed25519.SignatureSize)
|
|
}
|
|
a.k.set("author_message", h(msg))
|
|
a.k.set("author_signature", h(sig))
|
|
return sig
|
|
}
|
|
|
|
type cmsHook struct {
|
|
in cmsIn
|
|
c *certs
|
|
when time.Time
|
|
k *hooks
|
|
}
|
|
|
|
func (s *cmsHook) signers() []cmstest.Signer {
|
|
var out []cmstest.Signer
|
|
for _, n := range s.in.Signers {
|
|
out = append(out, s.c.byName[n])
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *cmsHook) Signers() [][32]byte {
|
|
out := [][32]byte{}
|
|
for _, x := range s.signers() {
|
|
out = append(out, sha256.Sum256(x.Cert.Raw))
|
|
}
|
|
if s.in.Extra != "" {
|
|
out = append(out, sha256.Sum256(s.c.byName[s.in.Extra].Cert.Raw))
|
|
}
|
|
list := []string{}
|
|
for _, x := range out {
|
|
list = append(list, h(x[:]))
|
|
}
|
|
s.k.set("cms_signers", list)
|
|
return out
|
|
}
|
|
|
|
func (s *cmsHook) der(msg []byte, junk int) []byte {
|
|
o := cmstest.Options{Junk: junk}
|
|
if !s.in.Unsealed {
|
|
o.Token = func(sig []byte) []byte {
|
|
return cmstest.Token(sig, s.when, cmstest.TokenOptions{Accuracy: time.Second}, s.c.tsa)
|
|
}
|
|
}
|
|
return cmstest.Signature(msg, o, s.signers()...)
|
|
}
|
|
|
|
func (s *cmsHook) Sign(msg []byte) ([]byte, error) {
|
|
s.k.set("cms_message", h(msg))
|
|
if s.in.Error != "" {
|
|
return nil, errors.New(s.in.Error)
|
|
}
|
|
var der []byte
|
|
switch {
|
|
case s.in.Garbage:
|
|
der = []byte("not a signature")
|
|
case s.in.Junk < 0:
|
|
// The most junk whose signature still fits in key 2 of the
|
|
// reader, 64 KiB, while SECURITY_CBOR is more than 64 KiB.
|
|
base := len(s.der(msg, 1))
|
|
junk := 65536 - 45 - base
|
|
for der = s.der(msg, junk); len(der) > 65536-45; der = s.der(msg, junk) {
|
|
junk--
|
|
}
|
|
default:
|
|
der = s.der(msg, s.in.Junk)
|
|
}
|
|
s.k.set("cms_der", h(der))
|
|
return der, nil
|
|
}
|
|
|
|
type sealHook struct {
|
|
in sealerIn
|
|
c *certs
|
|
when time.Time
|
|
k *hooks
|
|
}
|
|
|
|
func (s *sealHook) Seal(subject [32]byte) ([]byte, error) {
|
|
s.k.set("seal_subject", h(subject[:]))
|
|
if s.in.Error != "" {
|
|
return nil, errors.New(s.in.Error)
|
|
}
|
|
token := cmstest.Token(subject[:], s.when, cmstest.TokenOptions{}, s.c.tsa)
|
|
s.k.set("seal_token", h(token))
|
|
return token, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Writing and opening
|
|
|
|
func sourceOf(f fileIn) capsule.Source {
|
|
return capsule.Source{Path: f.Path, Size: int64(len(f.Text)), Open: func() (io.ReadCloser, error) {
|
|
return io.NopCloser(bytes.NewReader([]byte(f.Text))), nil
|
|
}}
|
|
}
|
|
|
|
// The genesis of Quicknet: the capsules open at round 1000.
|
|
var genesis = time.Unix(profile.Quicknet().GenesisTime, 0).UTC()
|
|
|
|
func run(r recipe, c *certs) (*capsule.Result, []byte, *seeded, obj, error) {
|
|
q := profile.Quicknet()
|
|
opts := capsule.EncryptOptions{
|
|
Profile: q, UnlockAt: must(datekey.RoundTime(q, 1000)), Now: func() time.Time { return genesis },
|
|
Comment: r.Comment, Author: r.Author, LargeArea: r.LargeArea,
|
|
TestVectors: r.TestAreaLen != 0, TestAreaLen: r.TestAreaLen,
|
|
}
|
|
k := &hooks{rec: obj{}}
|
|
if r.AuthorKey != nil {
|
|
opts.AuthorKey = &authorHook{key: must(authorkey.NewFromSeed(unhex(r.AuthorKey.Seed))), bad: r.AuthorKey.Bad, k: k}
|
|
}
|
|
if r.CMS != nil {
|
|
opts.CMSSigner = &cmsHook{in: *r.CMS, c: c, when: genesis, k: k}
|
|
}
|
|
if r.Sealer != nil {
|
|
when := genesis
|
|
if r.Sealer.Late {
|
|
when = opts.UnlockAt.Add(time.Hour)
|
|
}
|
|
opts.Sealer = &sealHook{in: *r.Sealer, c: c, when: when, k: k}
|
|
}
|
|
var sources []capsule.Source
|
|
for _, f := range r.Files {
|
|
sources = append(sources, sourceOf(f))
|
|
}
|
|
s := newSeeded(r.Seed)
|
|
old := rand.Reader
|
|
rand.Reader = s
|
|
var dst bytes.Buffer
|
|
res, err := capsule.EncryptFiles(&dst, sources, opts)
|
|
rand.Reader = old
|
|
return res, dst.Bytes(), s, k.rec, err
|
|
}
|
|
|
|
func releases() provider.ReleaseSource {
|
|
return testkit.NewSource(testkit.Release(1000))
|
|
}
|
|
|
|
// opened is what capsule.Open gives for dkc, with the author keys saved.
|
|
func opened(dkc []byte, keys map[string]string) obj {
|
|
files := &testkit.MemorySink{}
|
|
o := capsule.OpenOptions{
|
|
Registry: testkit.Registry(), Source: releases(), Sink: files, AuthorKeys: keys,
|
|
Now: func() time.Time { return time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC) },
|
|
}
|
|
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()
|
|
}
|
|
return v
|
|
}
|
|
v["result"] = "ok"
|
|
fs := []obj{}
|
|
for i, f := range res.Head.Files {
|
|
fs = append(fs, obj{"path": f.Path, "size": f.Size, "sha256": sum(files.Files[i])})
|
|
}
|
|
v["files"] = fs
|
|
v["head"] = h(must(capsule.EncodeHead(res.Head)))
|
|
v["verdicts"] = []string{string(res.Verdicts.Signature), string(res.Verdicts.Seal)}
|
|
if res.Verdicts.AuthorKey != ([32]byte{}) {
|
|
v["author_key"] = h(res.Verdicts.AuthorKey[:])
|
|
}
|
|
v["lines"] = res.Verdicts.Lines()
|
|
v["area_len"] = res.AreaLen
|
|
v["length"] = res.PayloadLength
|
|
return v
|
|
}
|
|
|
|
func caseOf(r recipe, c *certs) obj {
|
|
res, dkc, s, rec, err := run(r, c)
|
|
draws := []obj{}
|
|
for _, d := range s.draws {
|
|
draws = append(draws, obj{"n": len(d), "hex": h(d)})
|
|
}
|
|
out := obj{"recipe": r, "draws": draws}
|
|
if len(rec) > 0 {
|
|
out["hooks"] = rec
|
|
}
|
|
if err != nil {
|
|
out["error"] = err.Error()
|
|
if len(dkc) != 0 {
|
|
panic(r.Name + ": an error after writing")
|
|
}
|
|
return out
|
|
}
|
|
out["written"] = obj{"length": len(dkc), "sha256": sum(dkc), "capsule_id": h(res.CapsuleID[:]), "body_length": res.Length}
|
|
out["opened"] = opened(dkc, nil)
|
|
if r.AuthorKey != nil {
|
|
pub := must(authorkey.PublicString(must(authorkey.NewFromSeed(unhex(r.AuthorKey.Seed))).Public()))
|
|
out["opened_saved"] = opened(dkc, map[string]string{pub: "mi clave de 2026"})
|
|
}
|
|
return out
|
|
}
|
|
|
|
type sampleIn struct {
|
|
Name string `json:"name"`
|
|
DKC string `json:"dkc"`
|
|
AuthorKeys map[string]string `json:"author_keys,omitempty"`
|
|
TS obj `json:"ts"`
|
|
}
|
|
|
|
func TestSigningVectors(t *testing.T) {
|
|
if *sourceFlag == "" || *outFlag == "" {
|
|
t.Skip("run with -args -source COMMIT -out FILE [-samples FILE]")
|
|
}
|
|
cryptotest.SetGlobalRandom(t, 20261006)
|
|
c := newCerts()
|
|
cases := []obj{}
|
|
for _, r := range recipes() {
|
|
v := caseOf(r, c)
|
|
cases = append(cases, v)
|
|
if e, ok := v["error"]; ok {
|
|
t.Logf("%s: %s", r.Name, e)
|
|
} else {
|
|
t.Logf("%s: %d bytes, %v", r.Name, v["written"].(obj)["length"], v["opened"].(obj)["verdicts"])
|
|
}
|
|
}
|
|
samples := []obj{}
|
|
if *samplesFlag != "" {
|
|
var in struct {
|
|
Samples []sampleIn `json:"samples"`
|
|
}
|
|
must(0, json.Unmarshal(must(os.ReadFile(*samplesFlag)), &in))
|
|
for _, s := range in.Samples {
|
|
dkc := unhex(s.DKC)
|
|
v := obj{"name": s.Name, "dkc": s.DKC, "ts": s.TS, "opened": opened(dkc, nil)}
|
|
if s.AuthorKeys != nil {
|
|
v["author_keys"] = s.AuthorKeys
|
|
v["opened_saved"] = opened(dkc, s.AuthorKeys)
|
|
}
|
|
samples = append(samples, v)
|
|
t.Logf("sample %s: %v", s.Name, v["opened"].(obj)["verdicts"])
|
|
}
|
|
}
|
|
var buf bytes.Buffer
|
|
enc := json.NewEncoder(&buf)
|
|
enc.SetEscapeHTML(false)
|
|
enc.SetIndent("", " ")
|
|
must(0, enc.Encode(obj{
|
|
"description": "What capsule.EncryptFiles of datekeys-go writes when it signs and seals, for each recipe, while crypto/rand reads the keystream of ChaCha20 under SHA-256(seed) with a zero nonce, with each draw and what each hook was given and returned, and how capsule.Open reads it; the text of each error; and how capsule.Open reads the samples that encryptFiles of datekeys-ts wrote (scripts/signing-go-vectors_test.go). The capsules are time_only for round 1000 of Quicknet, written at its genesis.",
|
|
"source": *sourceFlag,
|
|
"go": runtime.Version(),
|
|
"release": h(testkit.Release(1000).Signature),
|
|
"cases": cases,
|
|
"samples": samples,
|
|
}))
|
|
must(0, os.WriteFile(*outFlag, buf.Bytes(), 0o644))
|
|
t.Logf("wrote %s, %d bytes", *outFlag, buf.Len())
|
|
}
|