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.
594 lines
21 KiB
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, ", ")
|
|
}
|