|
|
//go:build ignore
|
|
|
|
|
|
// Writes the vectors of the security area of datekeys-dart, stage 5b of
|
|
|
// docs/PLAN_dart.md: test/vectors/security_vectors.json, and a part of it as
|
|
|
// a Dart constant, security_vectors.g.dart, for the tests that also run
|
|
|
// compiled to JavaScript. Every expected value is computed here by package
|
|
|
// capsule of the Go reference at the draft v0.12; none is written by hand.
|
|
|
//
|
|
|
// - commitments: PayloadCommit; ControlCommit of the control of every
|
|
|
// fixture of the synced testdata and of controls built here with
|
|
|
// extensions, in their format and in the others, with the text of the
|
|
|
// error where EncodeControl refuses CONTROL_SIG; HeadDigest,
|
|
|
// SignersDigest, AuthorMessage with its AuthorCode, AuthorCode of
|
|
|
// messages that AuthorMessage never writes, SigPart and SealSubject.
|
|
|
// - encode: EncodeSecurity, EncodeSecurityWith, EncodeAuthorSignature and
|
|
|
// EncodeSeal, at the edges of the lengths of CBOR.
|
|
|
// - evaluate: EvaluateSecurityIn in the contexts of the file, and
|
|
|
// EvaluateSecurity without one, on SECURITY_CBOR: the structure of the
|
|
|
// outer map, of author-signature and of seal, each broken in every way
|
|
|
// its schema can be, at its limits; signatures of alg 1, valid and
|
|
|
// invalid, among them the cases of «Taming the many EdDSAs» made over
|
|
|
// AUTHOR_MESSAGE, whose context is searched so that k = SHA-512(R || A ||
|
|
|
// AUTHOR_MESSAGE) mod ℓ is what each case needs; and mutations of a few
|
|
|
// bases drawn from a fixed seed, as edits of the base (see "Edited
|
|
|
// files" in testdata/README.md). Each case gives the verdicts, the key
|
|
|
// and the label of alg 1, the lines as indices into texts, and alg and
|
|
|
// seal_type as SecurityKey2, DecodeAuthorSignature and SecurityKey3 read
|
|
|
// them; cms names the parts that only the reader of CMS evaluates: a
|
|
|
// signature of alg 2, and a seal of seal_type 2, in a context.
|
|
|
// - lines: Verdicts.Lines and SealedAt of verdicts built here, every pair
|
|
|
// of verdicts and the details of signers and seals that alg 2 and
|
|
|
// seal_type 2 give.
|
|
|
// - holder: holderText, how §29.7 shows the name of a certificate, on
|
|
|
// names at its limits and drawn from the seed. Package capsule does not
|
|
|
// export it: this program reaches it with go:linkname, which Go allows
|
|
|
// for a package outside the standard library.
|
|
|
//
|
|
|
// The Ed25519 arithmetic that makes the cases of «Taming the many EdDSAs»
|
|
|
// is restated from internal/testkit (ed25519vectors.go), with math/big:
|
|
|
// only the inputs come from it, and the verdicts from capsule.
|
|
|
//
|
|
|
// Binary values are lower-case hexadecimal. The seed is fixed: every run
|
|
|
// writes the same bytes. Run it in the module of the reference
|
|
|
// implementation, which it imports, without changing anything there, from
|
|
|
// the datekeys-go next to this repository, on the branch v0.12 at c531e93:
|
|
|
//
|
|
|
// cd ../datekeys-go && go run ../datekeys-dart/tool/security_go_vectors.go \
|
|
|
// -testdata ../datekeys-dart/testdata -out ../datekeys-dart/test/vectors
|
|
|
package main
|
|
|
|
|
|
import (
|
|
|
"bytes"
|
|
|
"crypto/ed25519"
|
|
|
"crypto/sha256"
|
|
|
"crypto/sha512"
|
|
|
"encoding/binary"
|
|
|
"encoding/hex"
|
|
|
"encoding/json"
|
|
|
"flag"
|
|
|
"fmt"
|
|
|
"math"
|
|
|
"math/big"
|
|
|
"math/rand/v2"
|
|
|
"os"
|
|
|
"path/filepath"
|
|
|
"slices"
|
|
|
"sort"
|
|
|
"strings"
|
|
|
"time"
|
|
|
"unicode/utf16"
|
|
|
_ "unsafe"
|
|
|
|
|
|
"g.activething.com/go/DateKeys/authorkey"
|
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
|
"g.activething.com/go/DateKeys/extension"
|
|
|
)
|
|
|
|
|
|
//go:linkname holderText g.activething.com/go/DateKeys/capsule.holderText
|
|
|
func holderText(name string, hash [32]byte) string
|
|
|
|
|
|
const specVersion = "0.11"
|
|
|
|
|
|
// The fixed seed of every random choice.
|
|
|
var rng = rand.New(rand.NewPCG(0x5b0a052026, 0x5ec))
|
|
|
|
|
|
func must[T any](v T, err error) T {
|
|
|
if err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
return v
|
|
|
}
|
|
|
|
|
|
func hx(b []byte) string { return hex.EncodeToString(b) }
|
|
|
|
|
|
func randBytes(n int) []byte {
|
|
|
b := make([]byte, n)
|
|
|
for i := range b {
|
|
|
b[i] = byte(rng.IntN(256))
|
|
|
}
|
|
|
return b
|
|
|
}
|
|
|
|
|
|
func digest(s string) [32]byte { return sha256.Sum256([]byte(s)) }
|
|
|
|
|
|
// Case is one vector: a JSON object.
|
|
|
type Case map[string]any
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// CBOR written byte by byte, in the profile and out of it
|
|
|
|
|
|
func head(major byte, n uint64) []byte {
|
|
|
switch {
|
|
|
case n < 24:
|
|
|
return []byte{major<<5 | byte(n)}
|
|
|
case n <= 0xff:
|
|
|
return []byte{major<<5 | 24, byte(n)}
|
|
|
case n <= 0xffff:
|
|
|
return binary.BigEndian.AppendUint16([]byte{major<<5 | 25}, uint16(n))
|
|
|
case n <= 0xffffffff:
|
|
|
return binary.BigEndian.AppendUint32([]byte{major<<5 | 26}, uint32(n))
|
|
|
default:
|
|
|
return binary.BigEndian.AppendUint64([]byte{major<<5 | 27}, n)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// wide is the head with an argument of 1 << (info - 24) bytes, in its
|
|
|
// shortest form or not.
|
|
|
func wide(major byte, n uint64, info byte) []byte {
|
|
|
b := []byte{major<<5 | info}
|
|
|
size := 1 << (info - 24)
|
|
|
arg := binary.BigEndian.AppendUint64(nil, n)
|
|
|
return append(b, arg[8-size:]...)
|
|
|
}
|
|
|
|
|
|
func uintOf(n uint64) []byte { return head(0, n) }
|
|
|
func bstr(b []byte) []byte { return append(head(2, uint64(len(b))), b...) }
|
|
|
func text(s string) []byte { return append(head(3, uint64(len(s))), s...) }
|
|
|
func cat(bs ...[]byte) []byte { return slices.Concat(bs...) }
|
|
|
|
|
|
// entry is a key and a value, each already encoded.
|
|
|
type entry struct{ k, v []byte }
|
|
|
|
|
|
func mapOf(es ...entry) []byte {
|
|
|
out := head(5, uint64(len(es)))
|
|
|
for _, e := range es {
|
|
|
out = append(append(out, e.k...), e.v...)
|
|
|
}
|
|
|
return out
|
|
|
}
|
|
|
|
|
|
func outer(signature, seal []byte) []byte {
|
|
|
es := []entry{{uintOf(0), text(capsule.SecurityTypeTag)}, {uintOf(1), uintOf(1)}}
|
|
|
if signature != nil {
|
|
|
es = append(es, entry{uintOf(2), bstr(signature)})
|
|
|
}
|
|
|
if seal != nil {
|
|
|
es = append(es, entry{uintOf(3), bstr(seal)})
|
|
|
}
|
|
|
return mapOf(es...)
|
|
|
}
|
|
|
|
|
|
func authorSig(alg uint64, key, value []byte) []byte {
|
|
|
return mapOf(entry{uintOf(0), uintOf(alg)}, entry{uintOf(1), bstr(key)}, entry{uintOf(2), bstr(value)})
|
|
|
}
|
|
|
|
|
|
func sealOf(sealType uint64, token []byte) []byte {
|
|
|
return mapOf(entry{uintOf(0), uintOf(sealType)}, entry{uintOf(1), bstr(token)})
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// Ed25519 on math/big, restated from internal/testkit, only to build inputs
|
|
|
|
|
|
var (
|
|
|
edP = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 255), big.NewInt(19))
|
|
|
edL = func() *big.Int {
|
|
|
l, _ := new(big.Int).SetString("7237005577332262213973186563042994240857116359379907606001950938285454250989", 10)
|
|
|
return l
|
|
|
}()
|
|
|
edD = func() *big.Int {
|
|
|
d := new(big.Int).Mul(big.NewInt(-121665), edInv(big.NewInt(121666)))
|
|
|
return d.Mod(d, edP)
|
|
|
}()
|
|
|
edSqrtM1 = new(big.Int).Exp(big.NewInt(2), new(big.Int).Rsh(new(big.Int).Sub(edP, big.NewInt(1)), 2), edP)
|
|
|
)
|
|
|
|
|
|
type edPoint struct{ x, y *big.Int }
|
|
|
|
|
|
func edInv(x *big.Int) *big.Int {
|
|
|
return new(big.Int).Exp(x, new(big.Int).Sub(edP, big.NewInt(2)), edP)
|
|
|
}
|
|
|
|
|
|
func edAdd(a, b edPoint) edPoint {
|
|
|
t := new(big.Int).Mul(edD, a.x)
|
|
|
t.Mul(t, b.x).Mul(t, a.y).Mul(t, b.y).Mod(t, edP)
|
|
|
x := new(big.Int).Add(new(big.Int).Mul(a.x, b.y), new(big.Int).Mul(b.x, a.y))
|
|
|
x.Mul(x, edInv(new(big.Int).Add(big.NewInt(1), t))).Mod(x, edP)
|
|
|
y := new(big.Int).Add(new(big.Int).Mul(a.y, b.y), new(big.Int).Mul(a.x, b.x))
|
|
|
y.Mul(y, edInv(new(big.Int).Mod(new(big.Int).Sub(big.NewInt(1), t), edP))).Mod(y, edP)
|
|
|
return edPoint{x, y}
|
|
|
}
|
|
|
|
|
|
func edMul(k *big.Int, a edPoint) edPoint {
|
|
|
r := edPoint{big.NewInt(0), big.NewInt(1)}
|
|
|
for i := k.BitLen() - 1; i >= 0; i-- {
|
|
|
r = edAdd(r, r)
|
|
|
if k.Bit(i) == 1 {
|
|
|
r = edAdd(r, a)
|
|
|
}
|
|
|
}
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
// edX recovers x from y and its sign bit, or nil when y is not on the curve.
|
|
|
func edX(y *big.Int, sign uint) *big.Int {
|
|
|
yy := new(big.Int).Mul(y, y)
|
|
|
num := new(big.Int).Sub(yy, big.NewInt(1))
|
|
|
den := new(big.Int).Add(new(big.Int).Mul(edD, yy), big.NewInt(1))
|
|
|
xx := new(big.Int).Mul(num, edInv(den.Mod(den, edP)))
|
|
|
xx.Mod(xx, edP)
|
|
|
if xx.Sign() == 0 {
|
|
|
return big.NewInt(0)
|
|
|
}
|
|
|
x := new(big.Int).Exp(xx, new(big.Int).Rsh(new(big.Int).Add(edP, big.NewInt(3)), 3), edP)
|
|
|
if new(big.Int).Mod(new(big.Int).Sub(new(big.Int).Mul(x, x), xx), edP).Sign() != 0 {
|
|
|
x.Mul(x, edSqrtM1).Mod(x, edP)
|
|
|
}
|
|
|
if new(big.Int).Mod(new(big.Int).Sub(new(big.Int).Mul(x, x), xx), edP).Sign() != 0 {
|
|
|
return nil
|
|
|
}
|
|
|
if x.Bit(0) != sign {
|
|
|
x.Sub(edP, x)
|
|
|
}
|
|
|
return x
|
|
|
}
|
|
|
|
|
|
func edBase() edPoint {
|
|
|
y := new(big.Int).Mul(big.NewInt(4), edInv(big.NewInt(5)))
|
|
|
y.Mod(y, edP)
|
|
|
return edPoint{edX(y, 0), y}
|
|
|
}
|
|
|
|
|
|
func leInt(b []byte) *big.Int {
|
|
|
be := slices.Clone(b)
|
|
|
slices.Reverse(be)
|
|
|
return new(big.Int).SetBytes(be)
|
|
|
}
|
|
|
|
|
|
func leBytes(x *big.Int) []byte {
|
|
|
b := x.FillBytes(make([]byte, 32))
|
|
|
slices.Reverse(b)
|
|
|
return b
|
|
|
}
|
|
|
|
|
|
func edEncode(a edPoint) []byte {
|
|
|
b := leBytes(a.y)
|
|
|
b[31] |= byte(a.x.Bit(0)) << 7
|
|
|
return b
|
|
|
}
|
|
|
|
|
|
// edTorsion returns the eight points of small order, [i]T for a point T of
|
|
|
// order 8 and i from 0 to 7.
|
|
|
func edTorsion() []edPoint {
|
|
|
for y := int64(2); ; y++ {
|
|
|
x := edX(big.NewInt(y), 0)
|
|
|
if x == nil {
|
|
|
continue
|
|
|
}
|
|
|
t := edMul(edL, edPoint{x, big.NewInt(y)})
|
|
|
if q := edMul(big.NewInt(4), t); q.x.Sign() == 0 && q.y.Cmp(big.NewInt(1)) == 0 {
|
|
|
continue
|
|
|
}
|
|
|
out := make([]edPoint, 8)
|
|
|
out[0] = edPoint{big.NewInt(0), big.NewInt(1)}
|
|
|
for i := 1; i < 8; i++ {
|
|
|
out[i] = edAdd(out[i-1], t)
|
|
|
}
|
|
|
return out
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func hramScalar(r, a, m []byte) *big.Int {
|
|
|
h := sha512.Sum512(slices.Concat(r, a, m))
|
|
|
return new(big.Int).Mod(leInt(h[:]), edL)
|
|
|
}
|
|
|
|
|
|
// nonCanonicalZero is y = p, the non-canonical encoding of y = 0, with the
|
|
|
// sign bit given.
|
|
|
func nonCanonicalZero(sign byte) []byte {
|
|
|
b := make([]byte, 32)
|
|
|
b[0] = 0xed
|
|
|
for i := 1; i < 31; i++ {
|
|
|
b[i] = 0xff
|
|
|
}
|
|
|
b[31] = 0x7f | sign
|
|
|
return b
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// Contexts and keys
|
|
|
|
|
|
type context struct {
|
|
|
name string
|
|
|
cc, hd [32]byte
|
|
|
round time.Time // zero: none
|
|
|
keys map[string]string
|
|
|
security *capsule.SecurityContext
|
|
|
}
|
|
|
|
|
|
var contexts []*context
|
|
|
|
|
|
func addContext(c *context) int {
|
|
|
c.security = &capsule.SecurityContext{ControlCommit: c.cc, HeadDigest: c.hd, RoundTime: c.round, AuthorKeys: c.keys}
|
|
|
contexts = append(contexts, c)
|
|
|
return len(contexts) - 1
|
|
|
}
|
|
|
|
|
|
func (c *context) json() Case {
|
|
|
out := Case{"name": c.name, "control_commit": hx(c.cc[:]), "head_digest": hx(c.hd[:])}
|
|
|
if !c.round.IsZero() {
|
|
|
out["round_time"] = c.round.UTC().Format(time.RFC3339Nano)
|
|
|
}
|
|
|
if len(c.keys) > 0 {
|
|
|
out["author_keys"] = c.keys
|
|
|
}
|
|
|
return out
|
|
|
}
|
|
|
|
|
|
func (c *context) message() []byte {
|
|
|
return capsule.AuthorMessage(c.cc, c.hd, capsule.SignersDigest(capsule.AlgEd25519, nil))
|
|
|
}
|
|
|
|
|
|
func keyOf(seed string) *authorkey.Key {
|
|
|
s := digest(seed)
|
|
|
return must(authorkey.NewFromSeed(s[:]))
|
|
|
}
|
|
|
|
|
|
func pub(k *authorkey.Key) string { return must(authorkey.PublicString(k.Public())) }
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// The evaluation of a case
|
|
|
|
|
|
var texts []string
|
|
|
var textIndex = map[string]int{}
|
|
|
|
|
|
func lineIndices(lines []string) []int {
|
|
|
out := []int{}
|
|
|
for _, l := range lines {
|
|
|
i, ok := textIndex[l]
|
|
|
if !ok {
|
|
|
i = len(texts)
|
|
|
texts = append(texts, l)
|
|
|
textIndex[l] = i
|
|
|
}
|
|
|
out = append(out, i)
|
|
|
}
|
|
|
return out
|
|
|
}
|
|
|
|
|
|
// evaluate records the verdicts of security b in the context ctx, -1 for
|
|
|
// none.
|
|
|
func evaluate(c Case, b []byte, ctx int) Case {
|
|
|
var sc *capsule.SecurityContext
|
|
|
if ctx >= 0 {
|
|
|
c["context"] = ctx
|
|
|
sc = contexts[ctx].security
|
|
|
} else {
|
|
|
c["context"] = nil
|
|
|
}
|
|
|
v := capsule.EvaluateSecurityIn(b, sc)
|
|
|
if v.Detail != nil && (v.Signature != capsule.VerdictSignedComplete && v.Signature != capsule.VerdictSignedIncomplete &&
|
|
|
v.Signature != capsule.VerdictSignatureInvalid && v.Seal != capsule.VerdictSealed && v.Seal != capsule.VerdictSealedLate) {
|
|
|
panic("a detail without a verdict that names it")
|
|
|
}
|
|
|
c["signature"], c["seal"] = string(v.Signature), string(v.Seal)
|
|
|
if v.Signature == capsule.VerdictSignedSaved || v.Signature == capsule.VerdictSignedOther {
|
|
|
c["author_key"] = must(authorkey.PublicString(v.AuthorKey[:]))
|
|
|
}
|
|
|
if v.Signature == capsule.VerdictSignedSaved {
|
|
|
c["author_label"] = v.AuthorLabel
|
|
|
}
|
|
|
c["lines"] = lineIndices(v.Lines())
|
|
|
var cms []string
|
|
|
if content, _, err := capsule.SecurityKey2(b); err == nil {
|
|
|
alg, _, _, err := capsule.DecodeAuthorSignature(content)
|
|
|
if err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
c["alg"] = alg
|
|
|
if alg == capsule.AlgCMS && sc != nil {
|
|
|
cms = append(cms, "signature")
|
|
|
}
|
|
|
}
|
|
|
if st, _, err := capsule.SecurityKey3(b); err == nil {
|
|
|
c["seal_type"] = st
|
|
|
if st == capsule.SealTypeRFC3161 && sc != nil {
|
|
|
cms = append(cms, "seal")
|
|
|
}
|
|
|
}
|
|
|
if cms != nil {
|
|
|
c["cms"] = cms
|
|
|
}
|
|
|
return c
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// The sections
|
|
|
|
|
|
func commitments(testdata string) Case {
|
|
|
var payload, controls, heads, signers, messages, codes, sigParts, subjects []Case
|
|
|
for i := range 6 {
|
|
|
var id [32]byte
|
|
|
switch i {
|
|
|
case 0:
|
|
|
case 1:
|
|
|
for j := range id {
|
|
|
id[j] = 0xff
|
|
|
}
|
|
|
default:
|
|
|
id = digest(fmt.Sprintf("I_PAYLOAD %d", i))
|
|
|
}
|
|
|
pc := capsule.PayloadCommit(id)
|
|
|
payload = append(payload, Case{"identity": hx(id[:]), "commit": hx(pc[:])})
|
|
|
}
|
|
|
|
|
|
// The control of every fixture, and controls built here, each in its
|
|
|
// format and in the others.
|
|
|
type control struct {
|
|
|
name string
|
|
|
b []byte
|
|
|
format capsule.Format
|
|
|
}
|
|
|
var cs []control
|
|
|
names := must(filepath.Glob(filepath.Join(testdata, "fixtures", "*.json")))
|
|
|
sort.Strings(names)
|
|
|
for _, path := range names {
|
|
|
if strings.HasSuffix(path, ".inspect.json") || strings.HasSuffix(path, ".dkk.json") {
|
|
|
continue
|
|
|
}
|
|
|
var rec struct {
|
|
|
Format int `json:"format"`
|
|
|
Control string `json:"control_cbor"`
|
|
|
}
|
|
|
if err := json.Unmarshal(must(os.ReadFile(path)), &rec); err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
cs = append(cs, control{strings.TrimSuffix(filepath.Base(path), ".json"), must(hex.DecodeString(rec.Control)), capsule.Format(rec.Format)})
|
|
|
}
|
|
|
exts := func(prefix string) [][]extension.Extension {
|
|
|
return [][]extension.Extension{
|
|
|
nil,
|
|
|
{{ID: prefix + ".a", Version: 1}},
|
|
|
{must(extension.New(prefix+".b", 7, []byte{1, 2, 3}))},
|
|
|
{{ID: prefix, Version: 1}, must(extension.New(prefix+".z", 1<<32-1, randBytes(40)))},
|
|
|
}
|
|
|
}
|
|
|
critical, noncritical := exts("org.example.c"), exts("org.example.n")
|
|
|
for i := range 12 {
|
|
|
f := capsule.Format(1 + i%3)
|
|
|
c := capsule.Control{
|
|
|
HeaderBinding: [32]byte(randBytes(32)),
|
|
|
PayloadIdentity: [32]byte(randBytes(32)),
|
|
|
Critical: critical[i%4],
|
|
|
Noncritical: noncritical[(i/4+1)%4],
|
|
|
}
|
|
|
if f != capsule.Format1 {
|
|
|
c.Padding = capsule.Padding(1 + i%2)
|
|
|
c.PayloadLength = []uint64{0, 1, 78000, capsule.MaxPayloadLength}[i%4]
|
|
|
}
|
|
|
cs = append(cs, control{fmt.Sprintf("built %d", i), must(capsule.EncodeControl(&c, f)), f})
|
|
|
}
|
|
|
for _, c := range cs {
|
|
|
decoded := must(capsule.DecodeControl(c.b, c.format))
|
|
|
for _, f := range []capsule.Format{capsule.Format1, capsule.Format2, capsule.Format3} {
|
|
|
out := Case{"name": c.name, "control": hx(c.b), "decode_format": int(c.format), "format": int(f)}
|
|
|
if cc, err := capsule.ControlCommit(decoded, f); err != nil {
|
|
|
out["error"] = err.Error()
|
|
|
} else {
|
|
|
out["commit"] = hx(cc[:])
|
|
|
}
|
|
|
controls = append(controls, out)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
for _, n := range []int{0, 1, 55, 56, 63, 64, 65, 1000} {
|
|
|
h := randBytes(n)
|
|
|
d := capsule.HeadDigest(h)
|
|
|
heads = append(heads, Case{"head": hx(h), "digest": hx(d[:])})
|
|
|
}
|
|
|
|
|
|
for _, s := range []struct {
|
|
|
alg uint64
|
|
|
signers []byte
|
|
|
}{
|
|
|
{1, nil}, {1, []byte{}}, {2, must(capsule.EncodeSigners([][32]byte{digest("a")}))},
|
|
|
{2, must(capsule.EncodeSigners([][32]byte{digest("a"), digest("b"), digest("c")}))},
|
|
|
{0, nil}, {3, randBytes(10)}, {0xffffffff, randBytes(33)}, {0x01020304, nil},
|
|
|
} {
|
|
|
d := capsule.SignersDigest(uint32(s.alg), s.signers)
|
|
|
c := Case{"alg": s.alg, "digest": hx(d[:])}
|
|
|
if s.signers != nil {
|
|
|
c["signers"] = hx(s.signers)
|
|
|
}
|
|
|
signers = append(signers, c)
|
|
|
}
|
|
|
|
|
|
for range 8 {
|
|
|
cc, hd, sd := [32]byte(randBytes(32)), [32]byte(randBytes(32)), [32]byte(randBytes(32))
|
|
|
m := capsule.AuthorMessage(cc, hd, sd)
|
|
|
messages = append(messages, Case{"control_commit": hx(cc[:]), "head_digest": hx(hd[:]),
|
|
|
"signers_digest": hx(sd[:]), "message": string(m), "code": capsule.AuthorCode(m)})
|
|
|
}
|
|
|
|
|
|
// AuthorCode of messages of other lengths and of bytes that AuthorMessage
|
|
|
// never writes: the code is Go's string of them, given as its bytes and
|
|
|
// as JSON writes it, U+FFFD for each byte that is not UTF-8.
|
|
|
valid := capsule.AuthorMessage(digest("cc"), digest("hd"), capsule.SignersDigest(1, nil))
|
|
|
at := len(capsule.AuthorMessagePrefix) + 1
|
|
|
edit := func(b []byte, off int, with ...byte) []byte {
|
|
|
out := slices.Clone(b)
|
|
|
copy(out[off:], with)
|
|
|
return out
|
|
|
}
|
|
|
for _, m := range [][]byte{
|
|
|
valid, valid[:98], append(slices.Clone(valid), '\n'), {}, bytes.Repeat([]byte{'a'}, 99),
|
|
|
make([]byte, 99),
|
|
|
edit(valid, at, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'),
|
|
|
edit(valid, at, 0xff, 0xfe, 'a', 'b', 0x80, 'c', 'd', 'e'),
|
|
|
edit(valid, at+3, 0xc3, 0xa9),
|
|
|
edit(valid, at, 0xef, 0xbb, 0xbf, 'x', 0xe2, 0x82, 0xac, 'y'),
|
|
|
edit(valid, at, 0xf0, 0x9f, 0x98, 0x80, 0xed, 0xa0, 0x80, 'z'),
|
|
|
edit(valid, at, 0xe2, 0x82, '-', '-', 0xf4, 0x90, 0x80, 0x80),
|
|
|
} {
|
|
|
code := capsule.AuthorCode(m)
|
|
|
codes = append(codes, Case{"message": hx(m), "code": code, "code_hex": hx([]byte(code))})
|
|
|
}
|
|
|
|
|
|
for _, s := range [][]byte{nil, {}, {0}, randBytes(1), randBytes(105), randBytes(1000)} {
|
|
|
p := capsule.SigPart(s)
|
|
|
c := Case{"sig_part": hx(p)}
|
|
|
if s != nil {
|
|
|
c["signature"] = hx(s)
|
|
|
}
|
|
|
sigParts = append(sigParts, c)
|
|
|
}
|
|
|
|
|
|
for _, p := range [][]byte{capsule.SigPart(nil), capsule.SigPart(randBytes(105)), {}, randBytes(7)} {
|
|
|
cc, hd := [32]byte(randBytes(32)), [32]byte(randBytes(32))
|
|
|
s := capsule.SealSubject(cc, hd, p)
|
|
|
subjects = append(subjects, Case{"control_commit": hx(cc[:]), "head_digest": hx(hd[:]), "sig_part": hx(p), "seal_subject": hx(s[:])})
|
|
|
}
|
|
|
return Case{"payload_commit": payload, "control_commit": controls, "head_digest": heads, "signers_digest": signers,
|
|
|
"author_message": messages, "author_code": codes, "sig_part": sigParts, "seal_subject": subjects}
|
|
|
}
|
|
|
|
|
|
func encodeCases() []Case {
|
|
|
var out []Case
|
|
|
out = append(out, Case{"what": "security", "hex": hx(capsule.EncodeSecurity())})
|
|
|
for _, s := range [][2][]byte{
|
|
|
{nil, nil}, {{1}, nil}, {nil, {2}}, {{1}, {2}}, {randBytes(23), randBytes(24)},
|
|
|
{randBytes(255), randBytes(256)}, {bytes.Repeat([]byte{0xab}, 65535), nil}, {nil, randBytes(300)},
|
|
|
} {
|
|
|
c := Case{"what": "security_with"}
|
|
|
blob(c, "hex", must(capsule.EncodeSecurityWith(s[0], s[1])))
|
|
|
if s[0] != nil {
|
|
|
blob(c, "signature", s[0])
|
|
|
}
|
|
|
if s[1] != nil {
|
|
|
blob(c, "seal", s[1])
|
|
|
}
|
|
|
out = append(out, c)
|
|
|
}
|
|
|
for _, alg := range []uint64{1, 2, 23, 24, 255, 256, 65535, 65536, 0xffffffff} {
|
|
|
key, value := randBytes(rng.IntN(40)), randBytes(rng.IntN(300))
|
|
|
out = append(out, Case{"what": "author_signature", "alg": alg, "key": hx(key), "value": hx(value),
|
|
|
"hex": hx(must(capsule.EncodeAuthorSignature(alg, key, value)))})
|
|
|
}
|
|
|
for _, st := range []uint64{1, 2, 3, 0xffffffff} {
|
|
|
token := randBytes(rng.IntN(300))
|
|
|
out = append(out, Case{"what": "seal", "seal_type": st, "token": hx(token), "hex": hx(must(capsule.EncodeSeal(st, token)))})
|
|
|
}
|
|
|
return out
|
|
|
}
|
|
|
|
|
|
// blob records b in c as name, its hex, or as name_parts when it is long.
|
|
|
func blob(c Case, name string, b []byte) {
|
|
|
if len(b) > 4096 {
|
|
|
c[name+"_parts"] = parts(b)
|
|
|
} else {
|
|
|
c[name] = hx(b)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// parts writes b as [hex, count] runs, so that a case of 64 KiB is short.
|
|
|
func parts(b []byte) [][]any {
|
|
|
var out [][]any
|
|
|
for i := 0; i < len(b); {
|
|
|
j := i + 1
|
|
|
for j < len(b) && b[j] == b[i] {
|
|
|
j++
|
|
|
}
|
|
|
if j-i >= 16 {
|
|
|
out = append(out, []any{hx(b[i : i+1]), j - i})
|
|
|
i = j
|
|
|
continue
|
|
|
}
|
|
|
start := i
|
|
|
for i < len(b) {
|
|
|
k := i + 1
|
|
|
for k < len(b) && b[k] == b[i] {
|
|
|
k++
|
|
|
}
|
|
|
if k-i >= 16 {
|
|
|
break
|
|
|
}
|
|
|
i = k
|
|
|
}
|
|
|
out = append(out, []any{hx(b[start:i]), 1})
|
|
|
}
|
|
|
return out
|
|
|
}
|
|
|
|
|
|
func evaluateCases() (bases []string, cases []Case) {
|
|
|
k0, k1 := keyOf("datekeys-dart: author 0"), keyOf("datekeys-dart: author 1")
|
|
|
round := time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
cc0, hd0 := digest("datekeys-dart: control 0"), digest("datekeys-dart: head 0")
|
|
|
c0 := addContext(&context{name: "a capsule", cc: cc0, hd: hd0, round: round})
|
|
|
c1 := addContext(&context{name: "the same, with keys saved", cc: cc0, hd: hd0, round: round,
|
|
|
keys: map[string]string{pub(k0): "Ana", pub(k1): "Luis"}})
|
|
|
c2 := addContext(&context{name: "another capsule, with keys saved", cc: digest("datekeys-dart: control 1"), hd: hd0, round: round,
|
|
|
keys: map[string]string{pub(k0): "Ana"}})
|
|
|
c3 := addContext(&context{name: "the same, with a key saved in upper case", cc: cc0, hd: hd0, round: round,
|
|
|
keys: map[string]string{strings.ToUpper(pub(k0)): "ANA", pub(k1): "Luis"}})
|
|
|
c4 := addContext(&context{name: "the same, without a round time, with a key saved without a label", cc: cc0, hd: hd0,
|
|
|
keys: map[string]string{pub(k0): ""}})
|
|
|
c5 := addContext(&context{name: "the same, with a label that is not ASCII", cc: cc0, hd: hd0, round: round,
|
|
|
keys: map[string]string{pub(k0): "Mam\u00e1 \U0001f30d \u00abx\u00bb"}})
|
|
|
c6 := addContext(&context{name: "another head", cc: cc0, hd: digest("datekeys-dart: head 1"), round: round})
|
|
|
ctxs := []int{-1, c0, c1, c2, c3, c4, c5, c6}
|
|
|
|
|
|
m0 := contexts[c0].message()
|
|
|
sig0 := k0.Sign(m0)
|
|
|
sig1 := k1.Sign(m0)
|
|
|
signers := must(capsule.EncodeSigners([][32]byte{digest("a certificate")}))
|
|
|
auth0 := authorSig(1, k0.Public(), sig0)
|
|
|
// The bases, as the contents of their keys 2 and 3, nil when absent.
|
|
|
pairs := [][2][]byte{
|
|
|
{nil, nil},
|
|
|
{auth0, nil},
|
|
|
{auth0, sealOf(1, randBytes(32))},
|
|
|
{auth0, sealOf(2, randBytes(40))},
|
|
|
{authorSig(2, signers, randBytes(60)), nil},
|
|
|
{authorSig(capsule.AlgTest, randBytes(32), randBytes(64)), sealOf(capsule.SealTypeTest, randBytes(32))},
|
|
|
{nil, sealOf(1, randBytes(20))},
|
|
|
{authorSig(1, k1.Public(), sig1), nil},
|
|
|
{authorSig(1, randBytes(31), randBytes(64)), sealOf(3, nil)},
|
|
|
}
|
|
|
var baseList [][]byte
|
|
|
for _, p := range pairs {
|
|
|
b := outer(p[0], p[1])
|
|
|
if !bytes.Equal(b, must(capsule.EncodeSecurityWith(p[0], p[1]))) {
|
|
|
panic("a base is not what EncodeSecurityWith writes")
|
|
|
}
|
|
|
baseList = append(baseList, b)
|
|
|
bases = append(bases, hx(b))
|
|
|
}
|
|
|
add := func(name string, b []byte, ctx int) {
|
|
|
c := Case{"name": name}
|
|
|
if len(b) > 4096 {
|
|
|
c["parts"] = parts(b)
|
|
|
} else {
|
|
|
c["hex"] = hx(b)
|
|
|
}
|
|
|
cases = append(cases, evaluate(c, b, ctx))
|
|
|
}
|
|
|
|
|
|
// Each base in every context.
|
|
|
for i, b := range baseList {
|
|
|
for _, ctx := range ctxs {
|
|
|
c := Case{"name": fmt.Sprintf("base %d", i), "base": i, "edits": [][]any{}}
|
|
|
cases = append(cases, evaluate(c, b, ctx))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// The outer map, broken in every way.
|
|
|
seal1 := sealOf(1, randBytes(8))
|
|
|
std := []entry{{uintOf(0), text(capsule.SecurityTypeTag)}, {uintOf(1), uintOf(1)}}
|
|
|
key2 := entry{uintOf(2), bstr(auth0)}
|
|
|
key3 := entry{uintOf(3), bstr(seal1)}
|
|
|
full := mapOf(append(slices.Clone(std), key2, key3)...)
|
|
|
for _, o := range []struct {
|
|
|
name string
|
|
|
b []byte
|
|
|
}{
|
|
|
{"the outer map", full},
|
|
|
{"without key 0", mapOf(std[1], key2, key3)},
|
|
|
{"without key 1", mapOf(std[0], key2, key3)},
|
|
|
{"without keys 0 and 1", mapOf(key2, key3)},
|
|
|
{"keys 0 and 1 swapped", mapOf(std[1], std[0], key2, key3)},
|
|
|
{"the type tag of the head", mapOf(entry{uintOf(0), text(capsule.HeadTypeTag)}, std[1], key2)},
|
|
|
{"an empty type tag", mapOf(entry{uintOf(0), text("")}, std[1], key2)},
|
|
|
{"a type tag of 18 bytes", mapOf(entry{uintOf(0), text("datekeys-securityy")}, std[1], key2)},
|
|
|
{"the type tag in upper case", mapOf(entry{uintOf(0), text("DATEKEYS-SECURITY")}, std[1], key2)},
|
|
|
{"the type tag as a byte string", mapOf(entry{uintOf(0), bstr([]byte(capsule.SecurityTypeTag))}, std[1], key2)},
|
|
|
{"the type tag with its length in two bytes", mapOf(entry{uintOf(0), cat(wide(3, 17, 24), []byte(capsule.SecurityTypeTag))}, std[1], key2)},
|
|
|
{"version 0", mapOf(std[0], entry{uintOf(1), uintOf(0)}, key2)},
|
|
|
{"version 2", mapOf(std[0], entry{uintOf(1), uintOf(2)}, key2)},
|
|
|
{"version 1 in two bytes", mapOf(std[0], entry{uintOf(1), wide(0, 1, 24)}, key2)},
|
|
|
{"version 1 as a byte string", mapOf(std[0], entry{uintOf(1), bstr([]byte{1})}, key2)},
|
|
|
{"version 2^53", mapOf(std[0], entry{uintOf(1), uintOf(1 << 53)}, key2)},
|
|
|
{"key 1 in two bytes", mapOf(std[0], entry{wide(0, 1, 24), uintOf(1)}, key2)},
|
|
|
{"key 4, a byte string", mapOf(append(slices.Clone(std), key2, key3, entry{uintOf(4), bstr([]byte{1})})...)},
|
|
|
{"key 4 alone", mapOf(append(slices.Clone(std), entry{uintOf(4), bstr([]byte{1})})...)},
|
|
|
{"key 2 twice", mapOf(append(slices.Clone(std), key2, key2)...)},
|
|
|
{"keys 3 and 2 out of order", mapOf(append(slices.Clone(std), key3, key2)...)},
|
|
|
{"key 2 a text string", mapOf(append(slices.Clone(std), entry{uintOf(2), text("x")})...)},
|
|
|
{"key 2 a map", mapOf(append(slices.Clone(std), entry{uintOf(2), auth0})...)},
|
|
|
{"key 2 an empty byte string", mapOf(append(slices.Clone(std), entry{uintOf(2), bstr(nil)})...)},
|
|
|
{"key 3 an empty byte string", mapOf(append(slices.Clone(std), entry{uintOf(3), bstr(nil)})...)},
|
|
|
{"key 2 with its length not in its shortest form", mapOf(append(slices.Clone(std), entry{uintOf(2), cat(wide(2, uint64(len(auth0)), 25), auth0)})...)},
|
|
|
{"key 2 with a length past the end", cat(head(5, 3), std[0].k, std[0].v, std[1].k, std[1].v, uintOf(2), head(2, 200), auth0)},
|
|
|
{"a map of three entries with two", cat(head(5, 3), std[0].k, std[0].v, std[1].k, std[1].v)},
|
|
|
{"a map of one entry with two", cat(head(5, 1), std[0].k, std[0].v, std[1].k, std[1].v)},
|
|
|
{"a map of five entries", mapOf(append(slices.Clone(std), key2, key3, entry{uintOf(4), uintOf(0)})...)},
|
|
|
{"the head of the map in two bytes", cat(wide(5, 4, 24), full[1:])},
|
|
|
{"an indefinite map", cat([]byte{0xbf}, full[1:], []byte{0xff})},
|
|
|
{"a tag around the map", cat([]byte{0xc1}, full)},
|
|
|
{"an array", cat(head(4, 4), std[0].v, std[1].v, bstr(auth0), bstr(seal1))},
|
|
|
{"a byte more", cat(full, []byte{0})},
|
|
|
{"a byte less", full[:len(full)-1]},
|
|
|
{"a negative key", mapOf(append(slices.Clone(std), entry{[]byte{0x20}, uintOf(0)})...)},
|
|
|
{"a text key", mapOf(append(slices.Clone(std), entry{text("2"), bstr(auth0)})...)},
|
|
|
{"nothing", nil},
|
|
|
{"a byte", []byte{0}},
|
|
|
{"an empty map", []byte{0xa0}},
|
|
|
{"key 2 of 65 536 bytes", outer(make([]byte, 65536), nil)},
|
|
|
{"key 2 of 65 537 bytes", outer(make([]byte, 65537), nil)},
|
|
|
{"key 3 of 65 536 bytes", outer(nil, make([]byte, 65536))},
|
|
|
{"key 3 of 65 537 bytes", outer(nil, make([]byte, 65537))},
|
|
|
} {
|
|
|
add(o.name, o.b, c0)
|
|
|
}
|
|
|
|
|
|
// author-signature, broken in every way, beside a seal of seal_type 1.
|
|
|
for _, s := range []struct {
|
|
|
name string
|
|
|
b []byte
|
|
|
}{
|
|
|
{"alg 1 that verifies", auth0},
|
|
|
{"alg 0", authorSig(0, k0.Public(), sig0)},
|
|
|
{"alg 2^32 - 1", authorSig(0xffffffff, k0.Public(), sig0)},
|
|
|
{"alg 2^32", authorSig(1<<32, k0.Public(), sig0)},
|
|
|
{"alg 2^53", authorSig(1<<53, k0.Public(), sig0)},
|
|
|
{"alg 2^64 - 1", authorSig(math.MaxUint64, k0.Public(), sig0)},
|
|
|
{"alg 3", authorSig(3, k0.Public(), sig0)},
|
|
|
{"alg 23", authorSig(23, k0.Public(), sig0)},
|
|
|
{"alg 24", authorSig(24, k0.Public(), sig0)},
|
|
|
{"alg 256", authorSig(256, k0.Public(), sig0)},
|
|
|
{"alg 1 in two bytes", mapOf(entry{uintOf(0), wide(0, 1, 24)}, entry{uintOf(1), bstr(k0.Public())}, entry{uintOf(2), bstr(sig0)})},
|
|
|
{"alg 1 in nine bytes", mapOf(entry{uintOf(0), wide(0, 1, 27)}, entry{uintOf(1), bstr(k0.Public())}, entry{uintOf(2), bstr(sig0)})},
|
|
|
{"alg as a byte string", mapOf(entry{uintOf(0), bstr([]byte{1})}, entry{uintOf(1), bstr(k0.Public())}, entry{uintOf(2), bstr(sig0)})},
|
|
|
{"an empty key", authorSig(1, nil, sig0)},
|
|
|
{"a key of 31 bytes", authorSig(1, k0.Public()[:31], sig0)},
|
|
|
{"a key of 33 bytes", authorSig(1, append(k0.Public(), 0), sig0)},
|
|
|
{"a key of 64 bytes", authorSig(1, append(k0.Public(), k0.Public()...), sig0)},
|
|
|
{"an empty signature", authorSig(1, k0.Public(), nil)},
|
|
|
{"a signature of 63 bytes", authorSig(1, k0.Public(), sig0[:63])},
|
|
|
{"a signature of 65 bytes", authorSig(1, k0.Public(), append(slices.Clone(sig0), 0))},
|
|
|
{"a signature of 128 bytes", authorSig(1, k0.Public(), append(slices.Clone(sig0), sig0...))},
|
|
|
{"the key as a text string", mapOf(entry{uintOf(0), uintOf(1)}, entry{uintOf(1), text(string(k0.Public()[:8]))}, entry{uintOf(2), bstr(sig0)})},
|
|
|
{"without key 0", mapOf(entry{uintOf(1), bstr(k0.Public())}, entry{uintOf(2), bstr(sig0)})},
|
|
|
{"without key 1", mapOf(entry{uintOf(0), uintOf(1)}, entry{uintOf(2), bstr(sig0)})},
|
|
|
{"without key 2", mapOf(entry{uintOf(0), uintOf(1)}, entry{uintOf(1), bstr(k0.Public())})},
|
|
|
{"a key 3 more", mapOf(entry{uintOf(0), uintOf(1)}, entry{uintOf(1), bstr(k0.Public())}, entry{uintOf(2), bstr(sig0)}, entry{uintOf(3), bstr(nil)})},
|
|
|
{"keys 1 and 0 swapped", mapOf(entry{uintOf(1), bstr(k0.Public())}, entry{uintOf(0), uintOf(1)}, entry{uintOf(2), bstr(sig0)})},
|
|
|
{"key 0 twice", mapOf(entry{uintOf(0), uintOf(1)}, entry{uintOf(0), uintOf(1)}, entry{uintOf(2), bstr(sig0)})},
|
|
|
{"the map head in two bytes", cat(wide(5, 3, 24), auth0[1:])},
|
|
|
{"an indefinite map", cat([]byte{0xbf}, auth0[1:], []byte{0xff})},
|
|
|
{"a byte more", cat(auth0, []byte{0})},
|
|
|
{"a byte less", auth0[:len(auth0)-1]},
|
|
|
{"an array", cat(head(4, 3), uintOf(1), bstr(k0.Public()), bstr(sig0))},
|
|
|
{"a tag", cat([]byte{0xc1}, auth0)},
|
|
|
{"an empty map", []byte{0xa0}},
|
|
|
{"not CBOR", []byte{0xff}},
|
|
|
{"alg 2 with SIGNERS and a SignedData that is not DER", authorSig(2, signers, randBytes(30))},
|
|
|
{"alg 2 with SIGNERS empty", authorSig(2, []byte{0x80}, randBytes(30))},
|
|
|
{"alg 2 with an empty key", authorSig(2, nil, nil)},
|
|
|
} {
|
|
|
add("author-signature: "+s.name, outer(s.b, seal1), c0)
|
|
|
if strings.HasPrefix(s.name, "alg 1 that") || strings.HasPrefix(s.name, "alg 2 ") {
|
|
|
add("author-signature without a context: "+s.name, outer(s.b, seal1), -1)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// seal, broken in every way, beside a signature of alg 1 that verifies.
|
|
|
for _, s := range []struct {
|
|
|
name string
|
|
|
b []byte
|
|
|
}{
|
|
|
{"seal_type 1", sealOf(1, randBytes(16))},
|
|
|
{"seal_type 2", sealOf(2, randBytes(16))},
|
|
|
{"seal_type 2 with an empty token", sealOf(2, nil)},
|
|
|
{"seal_type 3", sealOf(3, randBytes(16))},
|
|
|
{"seal_type 0", sealOf(0, randBytes(16))},
|
|
|
{"seal_type 2^32 - 1", sealOf(0xffffffff, randBytes(16))},
|
|
|
{"seal_type 2^32", sealOf(1<<32, randBytes(16))},
|
|
|
{"seal_type 2^53 - 1", sealOf(1<<53-1, randBytes(16))},
|
|
|
{"seal_type 1 in two bytes", mapOf(entry{uintOf(0), wide(0, 1, 24)}, entry{uintOf(1), bstr(nil)})},
|
|
|
{"an empty token", sealOf(1, nil)},
|
|
|
{"a token of 65 536 bytes", sealOf(1, make([]byte, 65530))},
|
|
|
{"without key 0", mapOf(entry{uintOf(1), bstr(nil)})},
|
|
|
{"without key 1", mapOf(entry{uintOf(0), uintOf(1)})},
|
|
|
{"a key 2 more", mapOf(entry{uintOf(0), uintOf(1)}, entry{uintOf(1), bstr(nil)}, entry{uintOf(2), uintOf(0)})},
|
|
|
{"an unknown seal_type in a map that breaks its schema", mapOf(entry{uintOf(0), uintOf(99)}, entry{uintOf(1), bstr(nil)}, entry{uintOf(2), uintOf(0)})},
|
|
|
{"keys 1 and 0 swapped", mapOf(entry{uintOf(1), bstr(nil)}, entry{uintOf(0), uintOf(1)})},
|
|
|
{"the token as a text string", mapOf(entry{uintOf(0), uintOf(1)}, entry{uintOf(1), text("x")})},
|
|
|
{"an array", cat(head(4, 2), uintOf(1), bstr(nil))},
|
|
|
{"a byte more", cat(sealOf(1, nil), []byte{0})},
|
|
|
{"a tag", cat([]byte{0xc1}, sealOf(1, nil))},
|
|
|
{"an empty map", []byte{0xa0}},
|
|
|
{"not CBOR", []byte{0xff}},
|
|
|
} {
|
|
|
add("seal: "+s.name, outer(auth0, s.b), c0)
|
|
|
if strings.HasPrefix(s.name, "seal_type 2") {
|
|
|
add("seal without a context: "+s.name, outer(auth0, s.b), -1)
|
|
|
add("seal without a signature: "+s.name, outer(nil, s.b), c0)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Signatures of alg 1 that verify, or not, over AUTHOR_MESSAGE.
|
|
|
sigCase := func(name string, key, sig []byte, ctx int) {
|
|
|
add("alg 1: "+name, outer(authorSig(1, key, sig), nil), ctx)
|
|
|
}
|
|
|
sigCase("valid, saved in another capsule", k0.Public(), sig0, c2)
|
|
|
sigCase("valid, in the context of another head", k0.Public(), sig0, c6)
|
|
|
sigCase("valid, by another key", k1.Public(), sig1, c1)
|
|
|
for i := 0; i < 64; i += 7 {
|
|
|
bad := slices.Clone(sig0)
|
|
|
bad[i] ^= 1 << (i % 8)
|
|
|
sigCase(fmt.Sprintf("a bit of byte %d of the signature flipped", i), k0.Public(), bad, c0)
|
|
|
}
|
|
|
for i := 0; i < 32; i += 9 {
|
|
|
bad := k0.Public()
|
|
|
bad[i] ^= 0x10
|
|
|
sigCase(fmt.Sprintf("a bit of byte %d of the key flipped", i), bad, sig0, c0)
|
|
|
}
|
|
|
s := leInt(sig0[32:])
|
|
|
s.Add(s, edL)
|
|
|
sigCase("S + \u2113", k0.Public(), slices.Concat(sig0[:32], leBytes(s)), c0)
|
|
|
high := slices.Clone(sig0)
|
|
|
high[63] |= 0x20
|
|
|
sigCase("S with bit 253 set", k0.Public(), high, c0)
|
|
|
high = slices.Clone(sig0)
|
|
|
high[63] |= 0x80
|
|
|
sigCase("S with bit 255 set", k0.Public(), high, c0)
|
|
|
r := slices.Clone(sig0)
|
|
|
copy(r[:32], nonCanonicalZero(0))
|
|
|
sigCase("R not canonical", k0.Public(), r, c0)
|
|
|
for y := int64(2); ; y++ {
|
|
|
if edX(big.NewInt(y), 0) == nil {
|
|
|
sigCase(fmt.Sprintf("A not on the curve, y = %d", y), leBytes(big.NewInt(y)), sig0, c0)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// The cases of «Taming the many EdDSAs» over AUTHOR_MESSAGE: the
|
|
|
// context of each is searched so that k is what the case needs.
|
|
|
search := func(name string, rEnc, a []byte, ok func(k *big.Int) bool, keys map[string]string) int {
|
|
|
hd := digest("datekeys-dart: the head of " + name)
|
|
|
for n := 0; ; n++ {
|
|
|
cc := digest(fmt.Sprintf("datekeys-dart: the control of %s, %d", name, n))
|
|
|
m := capsule.AuthorMessage(cc, hd, capsule.SignersDigest(capsule.AlgEd25519, nil))
|
|
|
if ok(hramScalar(rEnc, a, m)) {
|
|
|
return addContext(&context{name: name, cc: cc, hd: hd, round: round, keys: keys})
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
eightDivides := func(k *big.Int) bool { return new(big.Int).Mod(k, big.NewInt(8)).Sign() == 0 }
|
|
|
identity := edEncode(edPoint{big.NewInt(0), big.NewInt(1)})
|
|
|
forged := slices.Concat(identity, make([]byte, 32))
|
|
|
for i, t := range edTorsion() {
|
|
|
a := edEncode(t)
|
|
|
name := fmt.Sprintf("A of small order, the point %d of the torsion, R the identity and S = 0", i)
|
|
|
sigCase(name, a, forged, search(name, identity, a, eightDivides, nil))
|
|
|
}
|
|
|
for _, sign := range []byte{0, 0x80} {
|
|
|
a := nonCanonicalZero(sign)
|
|
|
name := fmt.Sprintf("A not canonical, y = p, sign %d, R the identity and S = 0", sign>>7)
|
|
|
sigCase(name, a, forged, search(name, identity, a, eightDivides, nil))
|
|
|
}
|
|
|
negZero := slices.Clone(identity)
|
|
|
negZero[31] |= 0x80
|
|
|
sigCase("A the identity with the sign bit, R the identity and S = 0", negZero, forged,
|
|
|
search("negZero", identity, negZero, func(*big.Int) bool { return true }, nil))
|
|
|
|
|
|
seed := digest("datekeys-dart: a key of mixed order")
|
|
|
a := new(big.Int).Mod(leInt(seed[:]), edL)
|
|
|
mixed := edEncode(edAdd(edMul(a, edBase()), edTorsion()[1]))
|
|
|
rr := new(big.Int).Mod(leInt(slices.Concat(seed[:], seed[:])), edL)
|
|
|
rp := edEncode(edMul(rr, edBase()))
|
|
|
mixedKey := must(authorkey.PublicString(mixed))
|
|
|
for _, holds := range []bool{true, false} {
|
|
|
name := "A of mixed order, 8 divides k: the equation without the cofactor holds"
|
|
|
if !holds {
|
|
|
name = "A of mixed order, 8 does not divide k: only the equation with the cofactor holds"
|
|
|
}
|
|
|
for _, keys := range []map[string]string{nil, {mixedKey: "Mixta"}} {
|
|
|
n := name
|
|
|
if keys != nil {
|
|
|
n += ", the key saved"
|
|
|
}
|
|
|
ctx := search(n, rp, mixed, func(k *big.Int) bool { return eightDivides(k) == holds }, keys)
|
|
|
k := hramScalar(rp, mixed, contexts[ctx].message())
|
|
|
sv := new(big.Int).Mod(new(big.Int).Add(rr, new(big.Int).Mul(k, a)), edL)
|
|
|
sigCase(n, mixed, slices.Concat(rp, leBytes(sv)), ctx)
|
|
|
}
|
|
|
}
|
|
|
ap := edEncode(edMul(a, edBase()))
|
|
|
ctx := search("R the identity, A of prime order", identity, ap, func(*big.Int) bool { return true }, nil)
|
|
|
k := hramScalar(identity, ap, contexts[ctx].message())
|
|
|
sigCase("R the identity, A of prime order", ap, slices.Concat(identity, leBytes(new(big.Int).Mod(new(big.Int).Mul(k, a), edL))), ctx)
|
|
|
if !ed25519.Verify(ap, contexts[ctx].message(), slices.Concat(identity, leBytes(new(big.Int).Mod(new(big.Int).Mul(k, a), edL)))) {
|
|
|
panic("R the identity: the signature does not verify")
|
|
|
}
|
|
|
|
|
|
// Mutations of the bases, drawn from the seed, one edit each: of
|
|
|
// SECURITY_CBOR itself, or of the content of its key 2 or 3, written
|
|
|
// again with EncodeSecurityWith; such a case gives the key it edits and
|
|
|
// the first 8 bytes of the SHA-256 of the area, which a reader that makes
|
|
|
// it again checks.
|
|
|
for range 1500 {
|
|
|
i := rng.IntN(len(baseList))
|
|
|
b := baseList[i]
|
|
|
sig, seal := pairs[i][0], pairs[i][1]
|
|
|
key := 0
|
|
|
if rng.IntN(3) != 0 {
|
|
|
switch {
|
|
|
case sig != nil && (seal == nil || rng.IntN(2) == 0):
|
|
|
key = 2
|
|
|
case seal != nil:
|
|
|
key = 3
|
|
|
}
|
|
|
}
|
|
|
target := b
|
|
|
switch key {
|
|
|
case 2:
|
|
|
target = sig
|
|
|
case 3:
|
|
|
target = seal
|
|
|
}
|
|
|
e := randomEdit(target)
|
|
|
// slices.Concat gives nil for nothing: an empty content is still
|
|
|
// there, and its key is written with an empty byte string.
|
|
|
mutated := append([]byte{}, slices.Concat(target[:e[0].(int)], must(hex.DecodeString(e[2].(string))), target[e[0].(int)+e[1].(int):])...)
|
|
|
c := Case{"base": i, "edits": [][]any{e}}
|
|
|
if key != 0 {
|
|
|
if key == 2 {
|
|
|
sig = mutated
|
|
|
} else {
|
|
|
seal = mutated
|
|
|
}
|
|
|
mutated = must(capsule.EncodeSecurityWith(sig, seal))
|
|
|
sum := sha256.Sum256(mutated)
|
|
|
c["key"], c["sha256"] = key, hx(sum[:8])
|
|
|
}
|
|
|
cases = append(cases, evaluate(c, mutated, ctxs[rng.IntN(len(ctxs))]))
|
|
|
}
|
|
|
return bases, cases
|
|
|
}
|
|
|
|
|
|
// randomEdit draws one edit of b: a bit flipped, a byte replaced, bytes
|
|
|
// inserted or deleted, a cut, or a byte of the heads of CBOR replaced.
|
|
|
func randomEdit(b []byte) []any {
|
|
|
switch rng.IntN(6) {
|
|
|
case 0:
|
|
|
at := rng.IntN(len(b))
|
|
|
return []any{at, 1, hx([]byte{b[at] ^ 1<<rng.IntN(8)})}
|
|
|
case 1:
|
|
|
at := rng.IntN(len(b))
|
|
|
return []any{at, 1, hx([]byte{byte(rng.IntN(256))})}
|
|
|
case 2:
|
|
|
return []any{rng.IntN(len(b) + 1), 0, hx(randBytes(1 + rng.IntN(4)))}
|
|
|
case 3:
|
|
|
at := rng.IntN(len(b))
|
|
|
return []any{at, min(1+rng.IntN(4), len(b)-at), ""}
|
|
|
case 4:
|
|
|
at := rng.IntN(len(b))
|
|
|
return []any{at, len(b) - at, ""}
|
|
|
default:
|
|
|
heads := []int{0, 1, 2, 3, 4, 5, 19, 20, 21, 22, 23, 24, 25, 38, 39, 40}
|
|
|
at := min(heads[rng.IntN(len(heads))], len(b)-1)
|
|
|
return []any{at, 1, hx([]byte{pickByte()})}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// pickByte draws a byte among those of the heads of CBOR that change a
|
|
|
// type or a length, and any other.
|
|
|
func pickByte() byte {
|
|
|
special := []byte{0x00, 0x01, 0x02, 0x03, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1f, 0x20, 0x40, 0x41, 0x58, 0x59, 0x5f, 0x60, 0x71, 0x78,
|
|
|
0x80, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xb8, 0xbf, 0xc0, 0xc1, 0xd8, 0xf4, 0xf5, 0xf6, 0xf7, 0xf9, 0xff}
|
|
|
if rng.IntN(4) == 0 {
|
|
|
return byte(rng.IntN(256))
|
|
|
}
|
|
|
return special[rng.IntN(len(special))]
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// Lines
|
|
|
|
|
|
func signerJSON(s capsule.SignerLine) Case {
|
|
|
c := Case{"holder": s.Holder, "issuer": s.Issuer, "result": s.Result, "seal_holder": s.SealHolder, "before": s.Before}
|
|
|
if !s.SealTime.IsZero() {
|
|
|
c["seal_time"] = s.SealTime.UTC().Format(time.RFC3339Nano)
|
|
|
}
|
|
|
return c
|
|
|
}
|
|
|
|
|
|
func lineCase(name string, v capsule.Verdicts) Case {
|
|
|
c := Case{"name": name, "signature": string(v.Signature), "seal": string(v.Seal), "author_key": hx(v.AuthorKey[:]),
|
|
|
"author_label": v.AuthorLabel, "lines": v.Lines()}
|
|
|
if d := v.Detail; d != nil {
|
|
|
dj := Case{"signers": []Case{}, "foreign": []Case{}, "seal_holder": d.SealHolder}
|
|
|
for _, s := range d.Signers {
|
|
|
dj["signers"] = append(dj["signers"].([]Case), signerJSON(s))
|
|
|
}
|
|
|
for _, s := range d.Foreign {
|
|
|
dj["foreign"] = append(dj["foreign"].([]Case), signerJSON(s))
|
|
|
}
|
|
|
if !d.SealTime.IsZero() {
|
|
|
dj["seal_time"] = d.SealTime.UTC().Format(time.RFC3339Nano)
|
|
|
}
|
|
|
c["detail"] = dj
|
|
|
}
|
|
|
if t, ok := v.SealedAt(); ok {
|
|
|
c["sealed_at"] = t.UTC().Format(time.RFC3339Nano)
|
|
|
}
|
|
|
return c
|
|
|
}
|
|
|
|
|
|
func lines() []Case {
|
|
|
var out []Case
|
|
|
key := [32]byte(keyOf("datekeys-dart: author 0").Public())
|
|
|
sigs := []capsule.Verdict{"X", "F0", "F1", "F2", "F3", "F4", "F5", "F6"}
|
|
|
seals := []capsule.Verdict{"X", "S0", "S1", "S2", "S3", "S4", "S5"}
|
|
|
for _, s := range sigs {
|
|
|
for _, l := range seals {
|
|
|
out = append(out, lineCase(fmt.Sprintf("%s and %s", s, l), capsule.Verdicts{Signature: s, Seal: l, AuthorKey: key, AuthorLabel: "Ana"}))
|
|
|
}
|
|
|
}
|
|
|
for _, label := range []string{"", "Ana", "Mam\u00e1 \U0001f30d", "a b", "\u00abx\u00bb", "Firmado con la clave"} {
|
|
|
out = append(out, lineCase("F3 with the label "+label, capsule.Verdicts{Signature: "F3", Seal: "S0", AuthorKey: key, AuthorLabel: label}))
|
|
|
}
|
|
|
out = append(out, lineCase("F4 with the zero key", capsule.Verdicts{Signature: "F4", Seal: "S1"}))
|
|
|
|
|
|
t := func(s string) time.Time { return must(time.Parse(time.RFC3339Nano, s)) }
|
|
|
at := []time.Time{t("2026-09-30T12:00:00Z"), t("2026-09-30T12:00:00.5Z"), t("2026-09-30T12:00:00.123456789Z"),
|
|
|
t("2023-08-23T15:09:27.000001Z"), t("2029-12-31T23:59:59.1Z"), t("2030-01-01T00:00:00Z")}
|
|
|
holders := []string{"Ana L\u00f3pez", "Luis G\u00f3mez", "JUAN ESPA\u00d1OL ESPA\u00d1OL", "TSA", "\u674e\u5c0f\u9f99", "\U0001f600",
|
|
|
hx(randBytes(32)), "Autoridad de Sellado de prueba"}
|
|
|
signer := func(i int, result string) capsule.SignerLine {
|
|
|
l := capsule.SignerLine{Holder: holders[i%len(holders)], Issuer: holders[(i+3)%len(holders)], Result: result}
|
|
|
if result == "valid" {
|
|
|
l.SealHolder, l.SealTime, l.Before = holders[(i+7)%len(holders)], at[i%len(at)], i%3 != 2
|
|
|
}
|
|
|
if result == "absent" {
|
|
|
l.Issuer = ""
|
|
|
}
|
|
|
return l
|
|
|
}
|
|
|
results := []string{"valid", "invalid", "absent", "not verifiable", "without seal", "invalid seal", "out of validity"}
|
|
|
for _, n := range []int{1, 2, 3, 16} {
|
|
|
for _, before := range []string{"all", "some", "none"} {
|
|
|
d := &capsule.Detail{}
|
|
|
for i := range n {
|
|
|
l := signer(i, "valid")
|
|
|
switch before {
|
|
|
case "all":
|
|
|
l.Before = true
|
|
|
case "none":
|
|
|
l.Before = false
|
|
|
}
|
|
|
d.Signers = append(d.Signers, l)
|
|
|
}
|
|
|
out = append(out, lineCase(fmt.Sprintf("F6, %d signers, %s before", n, before), capsule.Verdicts{Signature: "F6", Seal: "S0", Detail: d}))
|
|
|
}
|
|
|
}
|
|
|
foreign := &capsule.Detail{Signers: []capsule.SignerLine{signer(0, "valid"), signer(1, "valid")}}
|
|
|
for i, r := range results {
|
|
|
foreign.Foreign = append(foreign.Foreign, signer(i+2, r))
|
|
|
}
|
|
|
out = append(out, lineCase("F6 with foreign signers of every result", capsule.Verdicts{Signature: "F6", Seal: "S0", Detail: foreign}))
|
|
|
for i, r := range results {
|
|
|
d := &capsule.Detail{Signers: []capsule.SignerLine{signer(i, r), signer(i+1, "valid")}, Foreign: []capsule.SignerLine{signer(i+2, r)}}
|
|
|
for _, sig := range []capsule.Verdict{"F2", "F5"} {
|
|
|
out = append(out, lineCase(fmt.Sprintf("%s with a signer %s and a foreign one", sig, r), capsule.Verdicts{Signature: sig, Seal: "S0", Detail: d}))
|
|
|
}
|
|
|
}
|
|
|
for i, tm := range at {
|
|
|
for _, sig := range []capsule.Verdict{"F0", "F1", "F2", "F4", "F6"} {
|
|
|
d := &capsule.Detail{SealHolder: holders[i], SealTime: tm}
|
|
|
if sig == "F6" {
|
|
|
d.Signers = []capsule.SignerLine{signer(i, "valid")}
|
|
|
}
|
|
|
for _, seal := range []capsule.Verdict{"S4", "S5"} {
|
|
|
out = append(out, lineCase(fmt.Sprintf("%s and %s, sealed at %s", sig, seal, tm.Format(time.RFC3339Nano)),
|
|
|
capsule.Verdicts{Signature: sig, Seal: seal, AuthorKey: key, Detail: d}))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
out = append(out, lineCase("S4 with an empty detail", capsule.Verdicts{Signature: "F0", Seal: "S4", Detail: &capsule.Detail{}}))
|
|
|
out = append(out, lineCase("F6 without a detail", capsule.Verdicts{Signature: "F6", Seal: "S4"}))
|
|
|
out = append(out, lineCase("F6 with no signer", capsule.Verdicts{Signature: "F6", Seal: "S0", Detail: &capsule.Detail{}}))
|
|
|
// SealedAt: the earliest valid seal, of key 3 or of a valid required
|
|
|
// signer; never that of another signer, or of a seal that is not S4 or
|
|
|
// S5.
|
|
|
early := &capsule.Detail{SealHolder: "TSA", SealTime: at[4],
|
|
|
Signers: []capsule.SignerLine{signer(0, "valid"), signer(1, "invalid"), signer(3, "valid")}}
|
|
|
early.Signers[1].SealTime = t("2000-01-01T00:00:00Z")
|
|
|
early.Foreign = []capsule.SignerLine{signer(5, "valid")}
|
|
|
early.Foreign[0].SealTime = t("2001-01-01T00:00:00Z")
|
|
|
for _, seal := range []capsule.Verdict{"S0", "S3", "S4", "S5"} {
|
|
|
out = append(out, lineCase("the earliest seal with "+string(seal), capsule.Verdicts{Signature: "F5", Seal: seal, Detail: early}))
|
|
|
}
|
|
|
return out
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// The names of certificates
|
|
|
|
|
|
func holders() []Case {
|
|
|
var out []Case
|
|
|
add := func(name string) {
|
|
|
h := sha256.Sum256([]byte(name))
|
|
|
out = append(out, Case{"name": name, "hash": hx(h[:]), "result": holderText(name, h)})
|
|
|
}
|
|
|
// A name with lone surrogates, given as UTF-16 code units: its bytes are
|
|
|
// their generalized UTF-8, which is not UTF-8.
|
|
|
addUnits := func(units []uint16) {
|
|
|
var b []byte
|
|
|
for _, u := range units {
|
|
|
b = utf16Append(b, u)
|
|
|
}
|
|
|
h := sha256.Sum256(b)
|
|
|
out = append(out, Case{"units": units, "hash": hx(h[:]), "result": holderText(string(b), h)})
|
|
|
}
|
|
|
fixed := []string{
|
|
|
"", "Ana L\u00f3pez", " Ana", "Ana ", "Ana L\u00f3pez", "Ana\u00a0L\u00f3pez", "Ana\u00a0\u00a0L\u00f3pez", "Ana\tL\u00f3pez", "Ana\nL\u00f3pez",
|
|
|
"Ana\u202eL\u00f3pez", "Ana\u200bL\u00f3pez", "\ufeffAna", "Ana\uffff", "Ana\u0378", "Ana\ue000", "Ana\u3000L\u00f3pez", "Ana\u2028",
|
|
|
"\u0000", "Ana\u007f", "Ana\u0085", "ESPA\u00d1OL ESPA\u00d1OL JUAN - 12345678Z", "e\u0301", "\U0001f3f3\ufe0f\u200d\U0001f308",
|
|
|
"a\ufe0f", "TSA" + strings.Repeat(" ", 50) + "Firmado con la clave que guardaste como Banco", "\u00abAna\u00bb", "-", ".", "a b c",
|
|
|
}
|
|
|
for _, n := range fixed {
|
|
|
add(n)
|
|
|
}
|
|
|
for _, unit := range []string{"a", "\u00e9", "\u4e2d", "\U0001f600", "e\u0301"} {
|
|
|
for _, n := range []int{63, 64, 65} {
|
|
|
// e + U+0301 is two code points.
|
|
|
count := n
|
|
|
if unit == "e\u0301" {
|
|
|
count = n / 2
|
|
|
}
|
|
|
add(strings.Repeat(unit, count))
|
|
|
}
|
|
|
}
|
|
|
add(strings.Repeat("a", 63) + " ")
|
|
|
add(strings.Repeat("ab ", 21) + "a")
|
|
|
addUnits([]uint16{'A', 0xd800, 'B'})
|
|
|
addUnits([]uint16{0xdc00})
|
|
|
addUnits([]uint16{'A', 0xd83d})
|
|
|
// Names drawn from the seed, near the limit of 64 code points: mostly
|
|
|
// letters and spaces, now and then a character that the rules refuse.
|
|
|
good := []string{"a", "Z", "\u00f1", "\u00e9", "\u03a9", "\u4e2d", "\U0001f600", " ", "-", ".", "'", "e\u0301", "\u00a0"}
|
|
|
bad := []string{" ", "\u200b", "\u202e", "\t", "\n", "\ufeff", "\uffff", "\U000e0001", "\u0378", "\ue000", "\u3000",
|
|
|
"\u0000", "\u007f", "\u200d", "\ufe0f", "\U0001f3f3\ufe0f\u200d\U0001f308"}
|
|
|
for range 160 {
|
|
|
var sb strings.Builder
|
|
|
n := 50 + rng.IntN(20)
|
|
|
if rng.IntN(4) == 0 {
|
|
|
n = 1 + rng.IntN(12)
|
|
|
}
|
|
|
for range n {
|
|
|
if rng.IntN(40) == 0 {
|
|
|
sb.WriteString(bad[rng.IntN(len(bad))])
|
|
|
} else {
|
|
|
sb.WriteString(good[rng.IntN(len(good))])
|
|
|
}
|
|
|
}
|
|
|
add(sb.String())
|
|
|
}
|
|
|
return out
|
|
|
}
|
|
|
|
|
|
// utf16Append appends the generalized UTF-8 of the code unit u, a lone
|
|
|
// surrogate included.
|
|
|
func utf16Append(b []byte, u uint16) []byte {
|
|
|
r := rune(u)
|
|
|
switch {
|
|
|
case r < 0x80:
|
|
|
return append(b, byte(r))
|
|
|
case r < 0x800:
|
|
|
return append(b, 0xc0|byte(r>>6), 0x80|byte(r&0x3f))
|
|
|
default:
|
|
|
return append(b, 0xe0|byte(r>>12), 0x80|byte(r>>6&0x3f), 0x80|byte(r&0x3f))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// Output
|
|
|
|
|
|
func enc(v any) string {
|
|
|
var b bytes.Buffer
|
|
|
e := json.NewEncoder(&b)
|
|
|
e.SetEscapeHTML(false)
|
|
|
if err := e.Encode(v); err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
return strings.TrimSuffix(b.String(), "\n")
|
|
|
}
|
|
|
|
|
|
// ascii writes every character of the JSON s outside ASCII as an escape of
|
|
|
// JSON, a pair of surrogates above U+FFFF, so that the files hold no
|
|
|
// invisible or bidirectional character.
|
|
|
func ascii(s string) string {
|
|
|
var b strings.Builder
|
|
|
for _, r := range s {
|
|
|
switch {
|
|
|
case r < 0x80:
|
|
|
b.WriteRune(r)
|
|
|
case r <= 0xffff:
|
|
|
fmt.Fprintf(&b, `\u%04x`, r)
|
|
|
default:
|
|
|
hi, lo := utf16.EncodeRune(r)
|
|
|
fmt.Fprintf(&b, `\u%04x\u%04x`, hi, lo)
|
|
|
}
|
|
|
}
|
|
|
return b.String()
|
|
|
}
|
|
|
|
|
|
// render writes the fields of a file in this order, each list one case per
|
|
|
// line.
|
|
|
func render(fields []string, values Case) string {
|
|
|
var sb strings.Builder
|
|
|
sb.WriteString("{")
|
|
|
for i, f := range fields {
|
|
|
if i > 0 {
|
|
|
sb.WriteString(",")
|
|
|
}
|
|
|
sb.WriteString("\n " + enc(f) + ": ")
|
|
|
switch v := values[f].(type) {
|
|
|
case []Case:
|
|
|
sb.WriteString("[")
|
|
|
for j, c := range v {
|
|
|
if j > 0 {
|
|
|
sb.WriteString(",")
|
|
|
}
|
|
|
sb.WriteString("\n " + enc(c))
|
|
|
}
|
|
|
sb.WriteString("\n ]")
|
|
|
case Case:
|
|
|
sb.WriteString("{")
|
|
|
keys := make([]string, 0, len(v))
|
|
|
for k := range v {
|
|
|
keys = append(keys, k)
|
|
|
}
|
|
|
sort.Strings(keys)
|
|
|
for j, k := range keys {
|
|
|
if j > 0 {
|
|
|
sb.WriteString(",")
|
|
|
}
|
|
|
sb.WriteString("\n " + enc(k) + ": [")
|
|
|
for n, c := range v[k].([]Case) {
|
|
|
if n > 0 {
|
|
|
sb.WriteString(",")
|
|
|
}
|
|
|
sb.WriteString("\n " + enc(c))
|
|
|
}
|
|
|
sb.WriteString("\n ]")
|
|
|
}
|
|
|
sb.WriteString("\n }")
|
|
|
default:
|
|
|
sb.WriteString(enc(v))
|
|
|
}
|
|
|
}
|
|
|
sb.WriteString("\n}\n")
|
|
|
return sb.String()
|
|
|
}
|
|
|
|
|
|
func main() {
|
|
|
testdata := flag.String("testdata", "../datekeys-dart/testdata", "the synced testdata/ of datekeys-dart")
|
|
|
outDir := flag.String("out", "../datekeys-dart/test/vectors", "where the vectors go")
|
|
|
flag.Parse()
|
|
|
|
|
|
commits := commitments(*testdata)
|
|
|
encodes := encodeCases()
|
|
|
bases, cases := evaluateCases()
|
|
|
lineCases := lines()
|
|
|
holderCases := holders()
|
|
|
var ctxJSON []Case
|
|
|
for _, c := range contexts {
|
|
|
ctxJSON = append(ctxJSON, c.json())
|
|
|
}
|
|
|
|
|
|
fields := []string{"spec", "generator", "description", "contexts", "texts", "bases", "commitments", "encode", "evaluate", "lines", "holder"}
|
|
|
whole := Case{
|
|
|
"spec": specVersion,
|
|
|
"generator": "tool/security_go_vectors.go",
|
|
|
"description": "The security area of format 3 as package capsule of the Go reference gives it at the draft v0.12: commitments (PayloadCommit, ControlCommit with decode_format and format, or the text of its error, HeadDigest, SignersDigest, AuthorMessage, AuthorCode as Go's string and its bytes, SigPart, SealSubject); encode (EncodeSecurity, EncodeSecurityWith, EncodeAuthorSignature, EncodeSeal); " +
|
|
|
"evaluate: EvaluateSecurityIn of SECURITY_CBOR (hex, parts as [hex, repeat], or a base with edits) in the context of the index, EvaluateSecurity when null: the verdicts, author_key and author_label of alg 1, lines as indices into texts, alg and seal_type as read, and cms, the parts that only the reader of CMS evaluates; " +
|
|
|
"lines: Verdicts.Lines and SealedAt of verdicts built here; holder: holderText of a name, or of UTF-16 code units, and a hash. See the header of tool/security_go_vectors.go.",
|
|
|
"contexts": ctxJSON,
|
|
|
"texts": texts,
|
|
|
"bases": bases,
|
|
|
"commitments": commits,
|
|
|
"encode": encodes,
|
|
|
"evaluate": cases,
|
|
|
"lines": lineCases,
|
|
|
"holder": holderCases,
|
|
|
}
|
|
|
text := ascii(render(fields, whole))
|
|
|
if err := os.WriteFile(filepath.Join(*outDir, "security_vectors.json"), []byte(text), 0o644); err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
fmt.Fprintf(os.Stderr, "security_vectors.json: %d bytes, %d contexts, %d texts, %d evaluations, %d lines, %d names\n",
|
|
|
len(text), len(contexts), len(texts), len(cases), len(lineCases), len(holderCases))
|
|
|
|
|
|
// The part for the tests compiled to JavaScript: everything but every
|
|
|
// eighth evaluation, without the cases of 64 KiB.
|
|
|
var part []Case
|
|
|
for i, c := range cases {
|
|
|
if _, big := c["parts"]; i%8 == 0 && !big {
|
|
|
part = append(part, c)
|
|
|
}
|
|
|
}
|
|
|
small := Case{}
|
|
|
for k, v := range whole {
|
|
|
small[k] = v
|
|
|
}
|
|
|
small["evaluate"] = part
|
|
|
small["description"] = "Part of test/vectors/security_vectors.json: every eighth evaluation."
|
|
|
// No apostrophe, so that the raw string of Dart holds the JSON.
|
|
|
dart := strings.ReplaceAll(ascii(render(fields, small)), "'", `\u0027`)
|
|
|
if strings.Contains(dart, "'''") {
|
|
|
panic("a raw string of Dart cannot hold '''")
|
|
|
}
|
|
|
var b strings.Builder
|
|
|
b.WriteString("// Generated by tool/security_go_vectors.go: a part of\n")
|
|
|
b.WriteString("// test/vectors/security_vectors.json, for the tests that also run compiled\n")
|
|
|
b.WriteString("// to JavaScript, where no file can be read. Do not edit.\n\n")
|
|
|
fmt.Fprintf(&b, "/// Part of test/vectors/security_vectors.json.\nconst securityVectorsJson = r'''\n%s''';\n", dart)
|
|
|
if err := os.WriteFile(filepath.Join(*outDir, "security_vectors.g.dart"), []byte(b.String()), 0o644); err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
fmt.Fprintf(os.Stderr, "security_vectors.g.dart: %d bytes, %d evaluations\n", b.Len(), len(part))
|
|
|
}
|