//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, ", ") }