|
|
//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.
|
|
|
//
|
|
|
// For stage 5c it also writes test/vectors/securitycms_vectors.json:
|
|
|
// EvaluateSecurityIn on areas whose signature of alg 2, a CMS signature with
|
|
|
// certificates, or seal of seal_type 2, an RFC 3161 token, this program
|
|
|
// makes, as internal/cms/cmstest makes them for the tests of the reference,
|
|
|
// with the verdicts, the lines, the detail of every signer and of a valid
|
|
|
// seal, and SealedAt:
|
|
|
//
|
|
|
// - one required signer in each of its kinds (valid, sealed before or
|
|
|
// after the round time, invalid, not verifiable, without seal, with each
|
|
|
// kind of invalid seal, absent), beside foreign signers of each result,
|
|
|
// without a round time, at the time of the seals, in the context of
|
|
|
// another head and with a key 3; and signers that are out of validity,
|
|
|
// not verifiable or of RSA and P-384 by themselves;
|
|
|
// - three required signers, each absent or in one of its kinds, with
|
|
|
// foreign signers and a key 3, drawn from a seed of their own;
|
|
|
// - the validity of the certificate of a signer at t, the time of its
|
|
|
// seal, both ends included, at the nanosecond, from a UTCTime to a
|
|
|
// GeneralizedTime, with a genTime of ten digits of fraction; and that of
|
|
|
// the authority at genTime;
|
|
|
// - t plus the accuracy against the round time, one nanosecond before, at
|
|
|
// it and after it, and without a round time, for a signer and for a seal
|
|
|
// of key 3 beside no signature and beside one of alg 1, with each form
|
|
|
// of the accuracy, Go's zero time and the last second of 9999;
|
|
|
// - a seal of key 3 of each verdict beside a signature of each verdict;
|
|
|
// - mutations, one edit each, of the SignedData of a signature of alg 2,
|
|
|
// of its SIGNERS and of the token of a seal of key 3, drawn from the seed
|
|
|
// and swept bit by bit, the area written again with the encoders of
|
|
|
// capsule.
|
|
|
//
|
|
|
// Package internal/cms/cmstest cannot be imported from outside the tree of
|
|
|
// datekeys-go: the part of it that these cases need is restated here. Their
|
|
|
// keys come from labels, ECDSA signs with the nonce of RFC 6979 and RSA with
|
|
|
// PKCS #1 v1.5, so the output does not depend on crypto/rand. The DER that
|
|
|
// the cases share (certificates, tokens and SignerInfo) is written once, as
|
|
|
// chunks. securitycms_vectors.g.dart holds a part of the cases and the
|
|
|
// fixtures format3_signed_cms and format3_sealed of testdata, for the tests
|
|
|
// compiled to JavaScript.
|
|
|
//
|
|
|
// Binary values are lower-case hexadecimal. The seeds are 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"
|
|
|
"crypto/ecdsa"
|
|
|
"crypto/ed25519"
|
|
|
"crypto/elliptic"
|
|
|
"crypto/rsa"
|
|
|
"crypto/sha1"
|
|
|
"crypto/sha256"
|
|
|
"crypto/sha512"
|
|
|
"crypto/x509"
|
|
|
"encoding/asn1"
|
|
|
"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 { return randBytesOf(rng, n) }
|
|
|
|
|
|
func randBytesOf(r *rand.Rand, n int) []byte {
|
|
|
b := make([]byte, n)
|
|
|
for i := range b {
|
|
|
b[i] = byte(r.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())
|
|
|
// The signers of alg 2 and the authority of a valid seal, and the
|
|
|
// earliest seal: no case of security_vectors.json has them.
|
|
|
if v.Detail != nil {
|
|
|
c["detail"] = detailJSON(v.Detail)
|
|
|
}
|
|
|
if t, ok := v.SealedAt(); ok {
|
|
|
c["sealed_at"] = t.UTC().Format(time.RFC3339Nano)
|
|
|
}
|
|
|
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 { return randomEditOf(rng, b) }
|
|
|
|
|
|
// randomEditOf is randomEdit with the generator r.
|
|
|
func randomEditOf(r *rand.Rand, b []byte) []any {
|
|
|
switch r.IntN(6) {
|
|
|
case 0:
|
|
|
at := r.IntN(len(b))
|
|
|
return []any{at, 1, hx([]byte{b[at] ^ 1<<r.IntN(8)})}
|
|
|
case 1:
|
|
|
at := r.IntN(len(b))
|
|
|
return []any{at, 1, hx([]byte{byte(r.IntN(256))})}
|
|
|
case 2:
|
|
|
return []any{r.IntN(len(b) + 1), 0, hx(randBytesOf(r, 1+r.IntN(4)))}
|
|
|
case 3:
|
|
|
at := r.IntN(len(b))
|
|
|
return []any{at, min(1+r.IntN(4), len(b)-at), ""}
|
|
|
case 4:
|
|
|
at := r.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[r.IntN(len(heads))], len(b)-1)
|
|
|
return []any{at, 1, hx([]byte{pickByteOf(r)})}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// pickByteOf draws a byte among those of the heads of CBOR that change a
|
|
|
// type or a length, and any other.
|
|
|
func pickByteOf(r *rand.Rand) 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 r.IntN(4) == 0 {
|
|
|
return byte(r.IntN(256))
|
|
|
}
|
|
|
return special[r.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
|
|
|
}
|
|
|
|
|
|
// detailJSON is d as lineCase writes it.
|
|
|
func detailJSON(d *capsule.Detail) Case {
|
|
|
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)
|
|
|
}
|
|
|
return dj
|
|
|
}
|
|
|
|
|
|
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))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// Signatures of alg 2 and seals of seal_type 2
|
|
|
//
|
|
|
// securitycms_vectors.json, for stage 5c: EvaluateSecurityIn on areas whose
|
|
|
// CMS signature of alg 2 or RFC 3161 seal of seal_type 2 this program makes,
|
|
|
// as internal/cms/cmstest of the reference makes them for its tests, which a
|
|
|
// program outside the tree of datekeys-go cannot import: what follows
|
|
|
// restates the part of it that these cases need. The keys come from labels;
|
|
|
// ECDSA signs with the nonce of RFC 6979 and RSA with PKCS #1 v1.5, so every
|
|
|
// run writes the same bytes. A certificate is signed by its own key with the
|
|
|
// name of another issuer: DateKeys does not check who issued it (§29.10).
|
|
|
|
|
|
// cmsRng draws the choices of these cases: a seed of their own, so that the
|
|
|
// sections above stay as they were.
|
|
|
var cmsRng = rand.New(rand.NewPCG(0x5c0a052026, 0xc35))
|
|
|
|
|
|
func derTLV(tag byte, content ...[]byte) []byte {
|
|
|
c := bytes.Join(content, nil)
|
|
|
out := []byte{tag}
|
|
|
switch n := len(c); {
|
|
|
case n < 0x80:
|
|
|
out = append(out, byte(n))
|
|
|
case n < 0x100:
|
|
|
out = append(out, 0x81, byte(n))
|
|
|
case n < 0x10000:
|
|
|
out = append(out, 0x82, byte(n>>8), byte(n))
|
|
|
default:
|
|
|
out = append(out, 0x83, byte(n>>16), byte(n>>8), byte(n))
|
|
|
}
|
|
|
return append(out, c...)
|
|
|
}
|
|
|
|
|
|
func derSeq(content ...[]byte) []byte { return derTLV(0x30, content...) }
|
|
|
|
|
|
// derSet is a SET OF with the identifier octet tag, in DER order.
|
|
|
func derSet(tag byte, elems ...[]byte) []byte {
|
|
|
e := slices.Clone(elems)
|
|
|
slices.SortFunc(e, bytes.Compare)
|
|
|
return derTLV(tag, e...)
|
|
|
}
|
|
|
|
|
|
func derOID(oid asn1.ObjectIdentifier) []byte { return must(asn1.Marshal(oid)) }
|
|
|
func derOctets(b []byte) []byte { return derTLV(0x04, b) }
|
|
|
func derInt(n int64) []byte { return must(asn1.Marshal(n)) }
|
|
|
func derNull() []byte { return []byte{0x05, 0x00} }
|
|
|
func derBits(b []byte) []byte { return derTLV(0x03, append([]byte{0}, b...)) }
|
|
|
func derUTF8(s string) []byte { return derTLV(0x0c, []byte(s)) }
|
|
|
|
|
|
func derAlg(oid asn1.ObjectIdentifier, params ...[]byte) []byte {
|
|
|
return derSeq(append([][]byte{derOID(oid)}, params...)...)
|
|
|
}
|
|
|
|
|
|
// derGenTime is the GeneralizedTime of t in UTC as DER writes it: a fraction
|
|
|
// only when there is one, without trailing zeros.
|
|
|
func derGenTime(t time.Time) []byte {
|
|
|
t = t.UTC()
|
|
|
s := t.Format("20060102150405")
|
|
|
if ns := t.Nanosecond(); ns != 0 {
|
|
|
s += "." + strings.TrimRight(fmt.Sprintf("%09d", ns), "0")
|
|
|
}
|
|
|
return derTLV(0x18, []byte(s+"Z"))
|
|
|
}
|
|
|
|
|
|
// derCertTime is a time of validity as RFC 5280 writes it: a UTCTime from
|
|
|
// 1950 to 2049, a GeneralizedTime otherwise, without a fraction.
|
|
|
func derCertTime(t time.Time) []byte {
|
|
|
t = t.UTC()
|
|
|
if t.Nanosecond() != 0 {
|
|
|
panic("a time of validity with a fraction")
|
|
|
}
|
|
|
if t.Year() >= 1950 && t.Year() < 2050 {
|
|
|
return derTLV(0x17, []byte(t.Format("060102150405")+"Z"))
|
|
|
}
|
|
|
return derGenTime(t)
|
|
|
}
|
|
|
|
|
|
// The object identifiers, as in cmstest.
|
|
|
var (
|
|
|
oidData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1}
|
|
|
oidSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}
|
|
|
oidContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3}
|
|
|
oidMessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4}
|
|
|
oidSigCertV1 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 12}
|
|
|
oidSigCertV2 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 47}
|
|
|
oidTimeStamp = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 14}
|
|
|
oidTSTInfo = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 1, 4}
|
|
|
oidSHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26}
|
|
|
oidSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}
|
|
|
oidSHA384 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2}
|
|
|
oidSHA512 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3}
|
|
|
oidRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}
|
|
|
oidSHA256RSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11}
|
|
|
oidECDSASHA1 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 1}
|
|
|
oidECDSA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2}
|
|
|
oidECDSA384 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 3}
|
|
|
oidECDSA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4}
|
|
|
oidECPublicKey = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
|
|
|
oidP256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7}
|
|
|
oidCommonName = asn1.ObjectIdentifier{2, 5, 4, 3}
|
|
|
oidSurname = asn1.ObjectIdentifier{2, 5, 4, 4}
|
|
|
oidOrgName = asn1.ObjectIdentifier{2, 5, 4, 10}
|
|
|
oidGivenName = asn1.ObjectIdentifier{2, 5, 4, 42}
|
|
|
oidSKI = asn1.ObjectIdentifier{2, 5, 29, 14}
|
|
|
oidKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15}
|
|
|
oidTSAPolicy = asn1.ObjectIdentifier{1, 2, 3, 4}
|
|
|
)
|
|
|
|
|
|
func hashOID(h crypto.Hash) asn1.ObjectIdentifier {
|
|
|
switch h {
|
|
|
case crypto.SHA1:
|
|
|
return oidSHA1
|
|
|
case crypto.SHA384:
|
|
|
return oidSHA384
|
|
|
case crypto.SHA512:
|
|
|
return oidSHA512
|
|
|
}
|
|
|
return oidSHA256
|
|
|
}
|
|
|
|
|
|
func ecdsaOID(h crypto.Hash) asn1.ObjectIdentifier {
|
|
|
switch h {
|
|
|
case crypto.SHA1:
|
|
|
return oidECDSASHA1
|
|
|
case crypto.SHA384:
|
|
|
return oidECDSA384
|
|
|
case crypto.SHA512:
|
|
|
return oidECDSA512
|
|
|
}
|
|
|
return oidECDSA256
|
|
|
}
|
|
|
|
|
|
func hashSum(h crypto.Hash, b []byte) []byte {
|
|
|
w := h.New()
|
|
|
w.Write(b)
|
|
|
return w.Sum(nil)
|
|
|
}
|
|
|
|
|
|
// ecKeyOf is the ECDSA key of label on curve: a scalar from its SHA-512.
|
|
|
func ecKeyOf(curve elliptic.Curve, label string) *ecdsa.PrivateKey {
|
|
|
n := curve.Params().N
|
|
|
h := sha512.Sum512([]byte("datekeys-dart: an ECDSA key: " + label))
|
|
|
d := new(big.Int).SetBytes(h[:])
|
|
|
d.Mod(d, new(big.Int).Sub(n, big.NewInt(1))).Add(d, big.NewInt(1))
|
|
|
return must(ecdsa.ParseRawPrivateKey(curve, d.FillBytes(make([]byte, (n.BitLen()+7)/8))))
|
|
|
}
|
|
|
|
|
|
// rsaKeyOf is the RSA key of label of bits bits, with the exponent 65537:
|
|
|
// its primes are drawn from a generator seeded by the label, each with its
|
|
|
// two top bits set, so that the modulus has exactly bits bits.
|
|
|
func rsaKeyOf(bits int, label string) *rsa.PrivateKey {
|
|
|
h := sha256.Sum256([]byte("datekeys-dart: an RSA key: " + label))
|
|
|
r := rand.New(rand.NewPCG(binary.BigEndian.Uint64(h[:8]), binary.BigEndian.Uint64(h[8:16])))
|
|
|
prime := func() *big.Int {
|
|
|
for {
|
|
|
b := make([]byte, bits/16)
|
|
|
for i := range b {
|
|
|
b[i] = byte(r.IntN(256))
|
|
|
}
|
|
|
b[0] |= 0xc0
|
|
|
b[len(b)-1] |= 1
|
|
|
if p := new(big.Int).SetBytes(b); p.ProbablyPrime(20) {
|
|
|
return p
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
e := big.NewInt(65537)
|
|
|
for {
|
|
|
p, q := prime(), prime()
|
|
|
phi := new(big.Int).Mul(new(big.Int).Sub(p, big.NewInt(1)), new(big.Int).Sub(q, big.NewInt(1)))
|
|
|
d := new(big.Int).ModInverse(e, phi)
|
|
|
if p.Cmp(q) == 0 || d == nil {
|
|
|
continue
|
|
|
}
|
|
|
k := &rsa.PrivateKey{PublicKey: rsa.PublicKey{N: new(big.Int).Mul(p, q), E: 65537}, D: d, Primes: []*big.Int{p, q}}
|
|
|
k.Precompute()
|
|
|
if k.Validate() != nil || k.N.BitLen() != bits {
|
|
|
panic("an RSA key that does not validate")
|
|
|
}
|
|
|
return k
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// cmsSign signs digest, the hash h of what is signed, with key: ECDSA with
|
|
|
// the nonce of RFC 6979, RSA with PKCS #1 v1.5. It returns the
|
|
|
// signatureAlgorithm of the key and the signature value.
|
|
|
func cmsSign(key crypto.Signer, h crypto.Hash, digest []byte) (alg, sig []byte) {
|
|
|
switch k := key.(type) {
|
|
|
case *ecdsa.PrivateKey:
|
|
|
return derAlg(ecdsaOID(h)), must(k.Sign(nil, digest, h))
|
|
|
case *rsa.PrivateKey:
|
|
|
return derAlg(oidRSA, derNull()), must(rsa.SignPKCS1v15(nil, k, h, digest))
|
|
|
}
|
|
|
panic("a key of another type")
|
|
|
}
|
|
|
|
|
|
// cmsCert is a certificate with its key, and what a sid names of it.
|
|
|
type cmsCert struct {
|
|
|
key crypto.Signer
|
|
|
raw, issuer, serial, ski []byte
|
|
|
}
|
|
|
|
|
|
// certSpec describes a certificate as cmstest.CertSpec does, for what these
|
|
|
// cases change.
|
|
|
type certSpec struct {
|
|
|
cn string // the commonName of the subject, when subject is nil
|
|
|
subject, issuer []byte // the Names; the issuer is caName by default
|
|
|
from, to time.Time // 2020-01-01 to 2040-01-01 by default
|
|
|
spki []byte // that of the key by default
|
|
|
}
|
|
|
|
|
|
func cmsATV(oid asn1.ObjectIdentifier, value []byte) []byte {
|
|
|
return derSeq(derOID(oid), value)
|
|
|
}
|
|
|
|
|
|
// cmsName is a Name with one RelativeDistinguishedName for each attribute.
|
|
|
func cmsName(atvs ...[]byte) []byte {
|
|
|
rdns := make([][]byte, len(atvs))
|
|
|
for i, a := range atvs {
|
|
|
rdns[i] = derTLV(0x31, a)
|
|
|
}
|
|
|
return derSeq(rdns...)
|
|
|
}
|
|
|
|
|
|
var caName = cmsName(cmsATV(oidOrgName, derUTF8("DateKeys test")), cmsATV(oidCommonName, derUTF8("CA de prueba")))
|
|
|
|
|
|
func newCMSCert(label string, s certSpec, key crypto.Signer) cmsCert {
|
|
|
from, to := s.from, s.to
|
|
|
if from.IsZero() {
|
|
|
from = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
}
|
|
|
if to.IsZero() {
|
|
|
to = time.Date(2040, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
}
|
|
|
h := sha256.Sum256([]byte("datekeys-dart: a serial: " + label))
|
|
|
serial := derTLV(0x02, append([]byte{0x01}, h[:8]...))
|
|
|
sigAlg := derAlg(oidECDSA256)
|
|
|
if _, ok := key.(*rsa.PrivateKey); ok {
|
|
|
sigAlg = derAlg(oidSHA256RSA, derNull())
|
|
|
}
|
|
|
subject, issuer, spki := s.subject, s.issuer, s.spki
|
|
|
if subject == nil {
|
|
|
subject = cmsName(cmsATV(oidCommonName, derUTF8(s.cn)))
|
|
|
}
|
|
|
if issuer == nil {
|
|
|
issuer = caName
|
|
|
}
|
|
|
if spki == nil {
|
|
|
spki = must(x509.MarshalPKIXPublicKey(key.Public()))
|
|
|
}
|
|
|
k := sha256.Sum256(spki)
|
|
|
ski := k[:20]
|
|
|
exts := derSeq(
|
|
|
derSeq(derOID(oidSKI), derOctets(derOctets(ski))),
|
|
|
derSeq(derOID(oidKeyUsage), []byte{0x01, 0x01, 0xff}, derOctets([]byte{0x03, 0x02, 0x07, 0x80})),
|
|
|
)
|
|
|
tbs := derSeq(derTLV(0xa0, derInt(2)), serial, sigAlg, issuer, derSeq(derCertTime(from), derCertTime(to)), subject, spki, derTLV(0xa3, exts))
|
|
|
_, sig := cmsSign(key, crypto.SHA256, hashSum(crypto.SHA256, tbs))
|
|
|
c := cmsCert{key: key, raw: derSeq(tbs, sigAlg, derBits(sig)), issuer: issuer, serial: serial, ski: ski}
|
|
|
addChunk(c.raw)
|
|
|
return c
|
|
|
}
|
|
|
|
|
|
// compressedSPKI is the SubjectPublicKeyInfo of the P-256 key k with its
|
|
|
// point compressed, which the table of spec §29.10 does not have.
|
|
|
func compressedSPKI(k *ecdsa.PrivateKey) []byte {
|
|
|
u := must(k.PublicKey.Bytes())
|
|
|
n := (len(u) - 1) / 2
|
|
|
point := append([]byte{2 | u[len(u)-1]&1}, u[1:1+n]...)
|
|
|
return derSeq(derAlg(oidECPublicKey, derOID(oidP256)), derBits(point))
|
|
|
}
|
|
|
|
|
|
func cmsAttr(oid asn1.ObjectIdentifier, values ...[]byte) []byte {
|
|
|
return derSeq(derOID(oid), derSet(0x31, values...))
|
|
|
}
|
|
|
|
|
|
// signOpts changes what signerInfo writes, as cmstest.Options does.
|
|
|
type signOpts struct {
|
|
|
hash crypto.Hash // of the signature, SHA-256 by default
|
|
|
message []byte // what the message-digest covers, when not the message
|
|
|
corrupt bool // a bit of the value flipped after signing
|
|
|
sigAlg []byte // the signatureAlgorithm written, when not that of the key
|
|
|
ski bool // the sid by subjectKeyIdentifier
|
|
|
token func(sig []byte) []byte // the signature-time-stamp of the value; none when nil
|
|
|
}
|
|
|
|
|
|
// signerInfo is the SignerInfo of c over msg, as AutoFirma writes it, with
|
|
|
// content-type, message-digest and signing-certificate-v2 as signed
|
|
|
// attributes and the token of its value as an unsigned one.
|
|
|
func signerInfo(msg []byte, c cmsCert, o signOpts) []byte {
|
|
|
h := o.hash
|
|
|
if h == 0 {
|
|
|
h = crypto.SHA256
|
|
|
}
|
|
|
sid, version := derSeq(c.issuer, c.serial), int64(1)
|
|
|
if o.ski {
|
|
|
sid, version = derTLV(0x80, c.ski), 3
|
|
|
}
|
|
|
md := msg
|
|
|
if o.message != nil {
|
|
|
md = o.message
|
|
|
}
|
|
|
certHash := sha256.Sum256(c.raw)
|
|
|
signed := derSet(0xa0,
|
|
|
cmsAttr(oidContentType, derOID(oidData)),
|
|
|
cmsAttr(oidMessageDigest, derOctets(hashSum(h, md))),
|
|
|
cmsAttr(oidSigCertV2, derSeq(derSeq(derSeq(derOctets(certHash[:]))))),
|
|
|
)
|
|
|
alg, sig := cmsSign(c.key, h, hashSum(h, append([]byte{0x31}, signed[1:]...)))
|
|
|
if o.sigAlg != nil {
|
|
|
alg = o.sigAlg
|
|
|
}
|
|
|
if o.corrupt {
|
|
|
sig[len(sig)/2] ^= 1
|
|
|
}
|
|
|
f := [][]byte{derInt(version), sid, derAlg(hashOID(h)), signed, alg, derOctets(sig)}
|
|
|
if o.token != nil {
|
|
|
f = append(f, derSet(0xa1, cmsAttr(oidTimeStamp, o.token(sig))))
|
|
|
}
|
|
|
info := derSeq(f...)
|
|
|
addChunk(info)
|
|
|
return info
|
|
|
}
|
|
|
|
|
|
// cmsSigned is a member of a signature: a certificate and its SignerInfo.
|
|
|
type cmsSigned struct {
|
|
|
cert cmsCert
|
|
|
info []byte
|
|
|
}
|
|
|
|
|
|
// signedData is the detached signature of the members: a ContentInfo of a
|
|
|
// SignedData with each certificate and each SignerInfo, the SETs in DER
|
|
|
// order, and the digest algorithms of the SignerInfo.
|
|
|
func signedData(members ...cmsSigned) []byte {
|
|
|
var algs, certs, infos [][]byte
|
|
|
for _, m := range members {
|
|
|
alg := der2(m.info, 2)
|
|
|
if !slices.ContainsFunc(algs, func(a []byte) bool { return bytes.Equal(a, alg) }) {
|
|
|
algs = append(algs, alg)
|
|
|
}
|
|
|
if !slices.ContainsFunc(certs, func(c []byte) bool { return bytes.Equal(c, m.cert.raw) }) {
|
|
|
certs = append(certs, m.cert.raw)
|
|
|
}
|
|
|
infos = append(infos, m.info)
|
|
|
}
|
|
|
fields := [][]byte{derInt(1), derSet(0x31, algs...), derSeq(derOID(oidData))}
|
|
|
if len(certs) > 0 {
|
|
|
fields = append(fields, derSet(0xa0, certs...))
|
|
|
}
|
|
|
fields = append(fields, derSet(0x31, infos...))
|
|
|
return derSeq(derOID(oidSignedData), derTLV(0xa0, derSeq(fields...)))
|
|
|
}
|
|
|
|
|
|
// der2 is the i-th element of the SEQUENCE b, read with encoding/asn1.
|
|
|
func der2(b []byte, i int) []byte {
|
|
|
var seq asn1.RawValue
|
|
|
if rest, err := asn1.Unmarshal(b, &seq); err != nil || len(rest) > 0 {
|
|
|
panic("not one element")
|
|
|
}
|
|
|
in := seq.Bytes
|
|
|
for n := 0; ; n++ {
|
|
|
var e asn1.RawValue
|
|
|
rest, err := asn1.Unmarshal(in, &e)
|
|
|
if err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
if n == i {
|
|
|
return e.FullBytes
|
|
|
}
|
|
|
in = rest
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// tokOpts changes what cmsToken writes, as cmstest.TokenOptions does.
|
|
|
type tokOpts struct {
|
|
|
hash crypto.Hash // of the messageImprint, SHA-256 by default
|
|
|
sigHash crypto.Hash // of the signature of the authority, SHA-256 by default
|
|
|
accuracy []byte // the Accuracy element; none when nil
|
|
|
version int64 // of the TSTInfo, 1 by default
|
|
|
genTime []byte // the GeneralizedTime as written; that of t when nil
|
|
|
twice bool // the SignerInfo of the authority twice
|
|
|
}
|
|
|
|
|
|
// accuracyOf is the Accuracy element of d: its seconds, millis and micros,
|
|
|
// each only when it is not zero.
|
|
|
func accuracyOf(d time.Duration) []byte {
|
|
|
var f [][]byte
|
|
|
if s := d / time.Second; s != 0 {
|
|
|
f = append(f, derInt(int64(s)))
|
|
|
}
|
|
|
if ms := d % time.Second / time.Millisecond; ms != 0 {
|
|
|
f = append(f, derTLV(0x80, derInt(int64(ms))[2:]))
|
|
|
}
|
|
|
if us := d % time.Millisecond / time.Microsecond; us != 0 {
|
|
|
f = append(f, derTLV(0x81, derInt(int64(us))[2:]))
|
|
|
}
|
|
|
return derSeq(f...)
|
|
|
}
|
|
|
|
|
|
// cmsToken is the RFC 3161 token that tsa issues over subject at t: a
|
|
|
// TSTInfo of version 1 with the policy 1.2.3.4 and the serial number 42,
|
|
|
// signed with content-type, message-digest and signing-certificate.
|
|
|
func cmsToken(subject []byte, t time.Time, o tokOpts, tsa cmsCert) []byte {
|
|
|
h, sh, version := o.hash, o.sigHash, o.version
|
|
|
if h == 0 {
|
|
|
h = crypto.SHA256
|
|
|
}
|
|
|
if sh == 0 {
|
|
|
sh = crypto.SHA256
|
|
|
}
|
|
|
if version == 0 {
|
|
|
version = 1
|
|
|
}
|
|
|
gt := o.genTime
|
|
|
if gt == nil {
|
|
|
gt = derGenTime(t)
|
|
|
}
|
|
|
fields := [][]byte{derInt(version), derOID(oidTSAPolicy), derSeq(derAlg(hashOID(h)), derOctets(hashSum(h, subject))), derInt(42), gt}
|
|
|
if o.accuracy != nil {
|
|
|
fields = append(fields, o.accuracy)
|
|
|
}
|
|
|
info := derSeq(fields...)
|
|
|
certHash := sha1.Sum(tsa.raw)
|
|
|
signed := derSet(0xa0,
|
|
|
cmsAttr(oidContentType, derOID(oidTSTInfo)),
|
|
|
cmsAttr(oidMessageDigest, derOctets(hashSum(sh, info))),
|
|
|
cmsAttr(oidSigCertV1, derSeq(derSeq(derSeq(derOctets(certHash[:]))))),
|
|
|
)
|
|
|
alg, sig := cmsSign(tsa.key, sh, hashSum(sh, append([]byte{0x31}, signed[1:]...)))
|
|
|
si := derSeq(derInt(1), derSeq(tsa.issuer, tsa.serial), derAlg(hashOID(sh)), signed, alg, derOctets(sig))
|
|
|
infos := [][]byte{si}
|
|
|
if o.twice {
|
|
|
infos = append(infos, si)
|
|
|
}
|
|
|
sd := derSeq(derInt(1), derSet(0x31, derAlg(hashOID(sh))), derSeq(derOID(oidTSTInfo), derTLV(0xa0, derOctets(info))),
|
|
|
derSet(0xa0, tsa.raw), derSet(0x31, infos...))
|
|
|
tok := derSeq(derOID(oidSignedData), derTLV(0xa0, sd))
|
|
|
addChunk(tok)
|
|
|
return tok
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// The chunks: pieces of DER that the cases share, each written once
|
|
|
|
|
|
// cmsChunks are the certificates, the tokens and the SignerInfo that this
|
|
|
// program makes, in the order of making, each once; by their bytes, and by
|
|
|
// their first chunkHead bytes.
|
|
|
var cmsChunks [][]byte
|
|
|
var cmsChunkIndex = map[string]int{}
|
|
|
var cmsChunkByHead = map[string][]int{}
|
|
|
|
|
|
const chunkHead = 8
|
|
|
|
|
|
func addChunk(b []byte) {
|
|
|
if _, ok := cmsChunkIndex[string(b)]; ok {
|
|
|
return
|
|
|
}
|
|
|
if len(b) < 64 {
|
|
|
panic("a chunk too small to be worth it")
|
|
|
}
|
|
|
cmsChunkIndex[string(b)] = len(cmsChunks)
|
|
|
cmsChunkByHead[string(b[:chunkHead])] = append(cmsChunkByHead[string(b[:chunkHead])], len(cmsChunks))
|
|
|
cmsChunks = append(cmsChunks, slices.Clone(b))
|
|
|
}
|
|
|
|
|
|
// piecesOf writes b as a list of pieces: the hexadecimal of some bytes, or
|
|
|
// the index of a chunk below limit, the longest that starts there. The
|
|
|
// bytes are those pieces put together.
|
|
|
func piecesOf(b []byte, limit int) []any {
|
|
|
out := []any{}
|
|
|
var lit []byte
|
|
|
for i := 0; i < len(b); {
|
|
|
best := -1
|
|
|
if len(b)-i >= chunkHead {
|
|
|
for _, j := range cmsChunkByHead[string(b[i:i+chunkHead])] {
|
|
|
if j < limit && (best < 0 || len(cmsChunks[j]) > len(cmsChunks[best])) && bytes.HasPrefix(b[i:], cmsChunks[j]) {
|
|
|
best = j
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
if best < 0 {
|
|
|
lit = append(lit, b[i])
|
|
|
i++
|
|
|
continue
|
|
|
}
|
|
|
if len(lit) > 0 {
|
|
|
out = append(out, hx(lit))
|
|
|
lit = nil
|
|
|
}
|
|
|
out = append(out, best)
|
|
|
i += len(cmsChunks[best])
|
|
|
}
|
|
|
if len(lit) > 0 {
|
|
|
out = append(out, hx(lit))
|
|
|
}
|
|
|
return out
|
|
|
}
|
|
|
|
|
|
// joinPieces puts pieces together again.
|
|
|
func joinPieces(pieces []any) []byte {
|
|
|
var out []byte
|
|
|
for _, p := range pieces {
|
|
|
switch v := p.(type) {
|
|
|
case string:
|
|
|
out = append(out, must(hex.DecodeString(v))...)
|
|
|
case int:
|
|
|
out = append(out, cmsChunks[v]...)
|
|
|
}
|
|
|
}
|
|
|
return out
|
|
|
}
|
|
|
|
|
|
// writeCMS writes securitycms_vectors.json and, for the tests compiled to
|
|
|
// JavaScript, securitycms_vectors.g.dart: a part of its cases, each built
|
|
|
// area as its hexadecimal, and the fixtures format3_signed_cms and
|
|
|
// format3_sealed of testdata with what their records say.
|
|
|
func writeCMS(outDir, testdata string) {
|
|
|
var ctxJSON, chunks, bases []Case
|
|
|
for _, c := range contexts {
|
|
|
ctxJSON = append(ctxJSON, c.json())
|
|
|
}
|
|
|
// Only the chunks that some case or base uses, by itself or inside
|
|
|
// another chunk, in the order they were made.
|
|
|
chunkPieces := make([][]any, len(cmsChunks))
|
|
|
for j, c := range cmsChunks {
|
|
|
chunkPieces[j] = piecesOf(c, j)
|
|
|
}
|
|
|
var basePieces [][]any
|
|
|
for _, b := range cmsBases {
|
|
|
basePieces = append(basePieces, piecesOf(b, len(cmsChunks)))
|
|
|
}
|
|
|
used := make([]bool, len(cmsChunks))
|
|
|
var use func(pieces []any)
|
|
|
use = func(pieces []any) {
|
|
|
for _, p := range pieces {
|
|
|
if j, ok := p.(int); ok && !used[j] {
|
|
|
used[j] = true
|
|
|
use(chunkPieces[j])
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
for _, c := range cmsCases {
|
|
|
if p, ok := c["pieces"].([]any); ok {
|
|
|
use(p)
|
|
|
}
|
|
|
}
|
|
|
for _, p := range basePieces {
|
|
|
use(p)
|
|
|
}
|
|
|
renumber := map[int]int{}
|
|
|
for j := range cmsChunks {
|
|
|
if used[j] {
|
|
|
renumber[j] = len(renumber)
|
|
|
}
|
|
|
}
|
|
|
re := func(pieces []any) []any {
|
|
|
out := []any{}
|
|
|
for _, p := range pieces {
|
|
|
if j, ok := p.(int); ok {
|
|
|
p = renumber[j]
|
|
|
}
|
|
|
out = append(out, p)
|
|
|
}
|
|
|
return out
|
|
|
}
|
|
|
for j := range cmsChunks {
|
|
|
if used[j] {
|
|
|
chunks = append(chunks, Case{"pieces": re(chunkPieces[j])})
|
|
|
}
|
|
|
}
|
|
|
for i, b := range cmsBases {
|
|
|
sum := sha256.Sum256(b)
|
|
|
bases = append(bases, Case{"pieces": re(basePieces[i]), "sha256": hx(sum[:8])})
|
|
|
}
|
|
|
var cases []Case
|
|
|
for _, c := range cmsCases {
|
|
|
if p, ok := c["pieces"].([]any); ok {
|
|
|
d := Case{}
|
|
|
for k, v := range c {
|
|
|
d[k] = v
|
|
|
}
|
|
|
d["pieces"] = re(p)
|
|
|
c = d
|
|
|
}
|
|
|
cases = append(cases, c)
|
|
|
}
|
|
|
fields := []string{"spec", "generator", "description", "contexts", "texts", "chunks", "bases", "cases"}
|
|
|
whole := Case{
|
|
|
"spec": specVersion,
|
|
|
"generator": "tool/security_go_vectors.go",
|
|
|
"description": "Signatures of alg 2 and seals of seal_type 2 that this program makes, with keys of labels and the deterministic signatures of Go, as package capsule of the Go reference at the draft v0.12 evaluates them with EvaluateSecurityIn in the context of the index: " +
|
|
|
"the verdicts, the lines as indices into texts, alg and seal_type as read, cms, the detail of the signers and of a valid seal as the lines of security_vectors.json write it, sealed_at, and author_key of alg 1. " +
|
|
|
"An area is pieces (the hexadecimal of bytes, or the index of a chunk, each chunk itself pieces of the chunks before it), or a base edited in its target (value, the SignedData of key 2; signers, its SIGNERS; token, that of key 3) and written again with the encoders of capsule; sha256 is the first 8 bytes of the SHA-256 of the area. See the header of tool/security_go_vectors.go.",
|
|
|
"contexts": ctxJSON,
|
|
|
"texts": texts,
|
|
|
"chunks": chunks,
|
|
|
"bases": bases,
|
|
|
"cases": cases,
|
|
|
}
|
|
|
text := ascii(render(fields, whole))
|
|
|
if err := os.WriteFile(filepath.Join(outDir, "securitycms_vectors.json"), []byte(text), 0o644); err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
fmt.Fprintf(os.Stderr, "securitycms_vectors.json: %d bytes, %d contexts, %d texts, %d chunks, %d cases\n",
|
|
|
len(text), len(contexts), len(texts), len(chunks), len(cases))
|
|
|
|
|
|
// The part: of the built cases whose area is of at most 2400 bytes, the
|
|
|
// first of each group with each pair of verdicts and one of every eight,
|
|
|
// each as its hexadecimal; and one mutation of every twenty, with the
|
|
|
// bases whole.
|
|
|
var part []Case
|
|
|
built, mutations := 0, 0
|
|
|
pairs := map[string]bool{}
|
|
|
for _, c := range cmsCases {
|
|
|
if c["group"] == "mutations" {
|
|
|
if mutations%20 == 0 {
|
|
|
part = append(part, c)
|
|
|
}
|
|
|
mutations++
|
|
|
continue
|
|
|
}
|
|
|
b := joinPieces(c["pieces"].([]any))
|
|
|
if len(b) > 2400 {
|
|
|
continue
|
|
|
}
|
|
|
pair := fmt.Sprint(c["group"], " ", c["signature"], " ", c["seal"])
|
|
|
if built%8 == 0 || !pairs[pair] {
|
|
|
pairs[pair] = true
|
|
|
p := Case{}
|
|
|
for k, v := range c {
|
|
|
p[k] = v
|
|
|
}
|
|
|
delete(p, "pieces")
|
|
|
p["hex"] = hx(b)
|
|
|
part = append(part, p)
|
|
|
}
|
|
|
built++
|
|
|
}
|
|
|
var baseHex []string
|
|
|
for _, b := range cmsBases {
|
|
|
baseHex = append(baseHex, hx(b))
|
|
|
}
|
|
|
small := Case{"spec": specVersion, "generator": "tool/security_go_vectors.go",
|
|
|
"description": "Part of test/vectors/securitycms_vectors.json: its contexts and texts, its bases whole, and some of its cases, each built area as its hexadecimal.",
|
|
|
"contexts": ctxJSON, "texts": texts, "bases": baseHex, "cases": part}
|
|
|
partFields := []string{"spec", "generator", "description", "contexts", "texts", "bases", "cases"}
|
|
|
|
|
|
// The fixtures of testdata whose security area holds a signature of
|
|
|
// alg 2 or a seal of seal_type 2, as their records give them.
|
|
|
var fixtures []Case
|
|
|
for _, name := range []string{"format3_signed_cms", "format3_sealed"} {
|
|
|
raw := must(os.ReadFile(filepath.Join(testdata, "fixtures", name+".json")))
|
|
|
var r map[string]any
|
|
|
if err := json.Unmarshal(raw, &r); err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
dkc := must(os.ReadFile(filepath.Join(testdata, "fixtures", r["file"].(string))))
|
|
|
f := Case{"name": name, "dkc": hx(dkc), "release": r["release"], "unlock_at": r["unlock_at"], "verdicts": r["verdicts"]}
|
|
|
if sig, ok := r["signature"].(map[string]any); ok && sig["signer_results"] != nil {
|
|
|
f["signer_results"] = sig["signer_results"]
|
|
|
}
|
|
|
if seal, ok := r["seal"].(map[string]any); ok {
|
|
|
f["seal"] = Case{"holder": seal["holder"], "time": seal["time"]}
|
|
|
}
|
|
|
fixtures = append(fixtures, f)
|
|
|
}
|
|
|
fixtureText := ascii(render([]string{"fixtures"}, Case{"fixtures": fixtures}))
|
|
|
|
|
|
// No apostrophe, so that the raw strings of Dart hold the JSON.
|
|
|
dart := strings.ReplaceAll(ascii(render(partFields, small)), "'", `\u0027`)
|
|
|
fixtureText = strings.ReplaceAll(fixtureText, "'", `\u0027`)
|
|
|
if strings.Contains(dart, "'''") || strings.Contains(fixtureText, "'''") {
|
|
|
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/securitycms_vectors.json and two fixtures of testdata/, for\n")
|
|
|
b.WriteString("// the tests that also run compiled to JavaScript, where no file can be read.\n")
|
|
|
b.WriteString("// Do not edit.\n\n")
|
|
|
fmt.Fprintf(&b, "/// Part of test/vectors/securitycms_vectors.json.\nconst securityCmsVectorsJson = r'''\n%s''';\n\n", dart)
|
|
|
fmt.Fprintf(&b, "/// The fixtures format3_signed_cms and format3_sealed of testdata/: the\n/// .dkc in hexadecimal, and what their records say of their opening.\nconst securityCmsFixturesJson = r'''\n%s''';\n", fixtureText)
|
|
|
if err := os.WriteFile(filepath.Join(outDir, "securitycms_vectors.g.dart"), []byte(b.String()), 0o644); err != nil {
|
|
|
panic(err)
|
|
|
}
|
|
|
fmt.Fprintf(os.Stderr, "securitycms_vectors.g.dart: %d bytes, %d cases\n", b.Len(), len(part))
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// The cases
|
|
|
|
|
|
// cmsCases are the cases of securitycms_vectors.json, and cmsBases the areas
|
|
|
// that its mutations edit.
|
|
|
var cmsCases []Case
|
|
|
var cmsBases [][]byte
|
|
|
|
|
|
// addCMS evaluates the area b in the context ctx and records it, as pieces.
|
|
|
func addCMS(group, name string, b []byte, ctx int) {
|
|
|
sum := sha256.Sum256(b)
|
|
|
c := Case{"group": group, "name": name, "pieces": piecesOf(b, len(cmsChunks)), "sha256": hx(sum[:8])}
|
|
|
cmsCases = append(cmsCases, evaluate(c, b, ctx))
|
|
|
}
|
|
|
|
|
|
// cmsArea is the area of a signature of alg 2 with signers and value, and of
|
|
|
// the seal of key 3 when not nil.
|
|
|
func cmsArea(signers, value, seal []byte) []byte {
|
|
|
return must(capsule.EncodeSecurityWith(must(capsule.EncodeAuthorSignature(capsule.AlgCMS, signers, value)), seal))
|
|
|
}
|
|
|
|
|
|
func sealArea(signature, token []byte) []byte {
|
|
|
return must(capsule.EncodeSecurityWith(signature, must(capsule.EncodeSeal(capsule.SealTypeRFC3161, token))))
|
|
|
}
|
|
|
|
|
|
// signersOf is SIGNERS of the certificates.
|
|
|
func signersOf(certs ...cmsCert) []byte {
|
|
|
var hashes [][32]byte
|
|
|
for _, c := range certs {
|
|
|
hashes = append(hashes, sha256.Sum256(c.raw))
|
|
|
}
|
|
|
return must(capsule.EncodeSigners(hashes))
|
|
|
}
|
|
|
|
|
|
// signersOfHashes is SIGNERS of the hashes, as EncodeSigners writes it.
|
|
|
func signersOfHashes(hashes [][32]byte) []byte { return must(capsule.EncodeSigners(hashes)) }
|
|
|
|
|
|
func cmsMessage(ctx int, signers []byte) []byte {
|
|
|
c := contexts[ctx]
|
|
|
return capsule.AuthorMessage(c.cc, c.hd, capsule.SignersDigest(capsule.AlgCMS, signers))
|
|
|
}
|
|
|
|
|
|
// A kind of a member of a signature: who signs, and how.
|
|
|
type kind struct {
|
|
|
name string
|
|
|
cert cmsCert
|
|
|
o signOpts
|
|
|
}
|
|
|
|
|
|
func cmsSecurityCases() {
|
|
|
utc := func(y, mo, d, h, mi, s, ns int) time.Time {
|
|
|
return time.Date(y, time.Month(mo), d, h, mi, s, ns, time.UTC)
|
|
|
}
|
|
|
t0 := utc(2026, 9, 30, 12, 0, 0, 0)
|
|
|
round := utc(2030, 1, 1, 0, 0, 0, 0)
|
|
|
cc, hd := digest("datekeys-dart: the control of a capsule with certificates"), digest("datekeys-dart: its head")
|
|
|
c0 := addContext(&context{name: "a capsule", cc: cc, hd: hd, round: round})
|
|
|
cZero := addContext(&context{name: "the same, without a round time", cc: cc, hd: hd})
|
|
|
cEarly := addContext(&context{name: "the same, opening when the seals were made", cc: cc, hd: hd, round: t0})
|
|
|
cHead := addContext(&context{name: "another head", cc: cc, hd: digest("datekeys-dart: another head"), round: round})
|
|
|
edgeContext := func(name string, r time.Time) int {
|
|
|
return addContext(&context{name: name, cc: cc, hd: hd, round: r})
|
|
|
}
|
|
|
|
|
|
p256, p384 := elliptic.P256(), elliptic.P384()
|
|
|
ana := newCMSCert("ana", certSpec{cn: "Ana L\u00f3pez"}, ecKeyOf(p256, "ana"))
|
|
|
luis := newCMSCert("luis", certSpec{cn: "Luis G\u00f3mez", issuer: cmsName(cmsATV(oidOrgName, derUTF8("Banco de Pruebas S.A.")))}, rsaKeyOf(2048, "luis"))
|
|
|
bea := newCMSCert("bea", certSpec{subject: cmsName(cmsATV(oidCommonName, derUTF8("RUIZ GIL BEATRIZ - 12345678Z")),
|
|
|
cmsATV(oidGivenName, derUTF8("BEATRIZ")), cmsATV(oidSurname, derUTF8("RUIZ GIL")))}, ecKeyOf(p384, "bea"))
|
|
|
otro := newCMSCert("otro", certSpec{cn: "Otro firmante"}, ecKeyOf(p256, "otro"))
|
|
|
caducada := newCMSCert("caducada", certSpec{cn: "Firmante caducada", to: utc(2026, 8, 30, 12, 0, 0, 0)}, ecKeyOf(p256, "caducada"))
|
|
|
futura := newCMSCert("futura", certSpec{cn: "Firmante futura", from: utc(2027, 1, 1, 0, 0, 0, 0)}, ecKeyOf(p256, "futura"))
|
|
|
compKey := ecKeyOf(p256, "comprimida")
|
|
|
comprimida := newCMSCert("comprimida", certSpec{cn: "Clave comprimida", spki: compressedSPKI(compKey)}, compKey)
|
|
|
tsa := newCMSCert("tsa", certSpec{cn: "Autoridad de Sellado de prueba"}, ecKeyOf(p256, "tsa"))
|
|
|
tsaRSA := newCMSCert("tsa rsa", certSpec{cn: "TSA RSA de prueba"}, rsaKeyOf(2048, "tsa rsa"))
|
|
|
tsa1024 := newCMSCert("tsa 1024", certSpec{cn: "TSA de 1024 bits"}, rsaKeyOf(1024, "tsa 1024"))
|
|
|
tsaComp := newCMSCert("tsa comprimida", certSpec{cn: "TSA comprimida", spki: compressedSPKI(ecKeyOf(p256, "tsa comprimida"))}, ecKeyOf(p256, "tsa comprimida"))
|
|
|
always := certSpec{from: utc(0, 1, 1, 0, 0, 0, 0), to: utc(9999, 12, 31, 23, 59, 59, 0)}
|
|
|
tsaAlways := newCMSCert("tsa always", certSpec{cn: "TSA de siempre", from: always.from, to: always.to}, ecKeyOf(p256, "tsa always"))
|
|
|
anaAlways := newCMSCert("ana always", certSpec{cn: "Ana de siempre", from: always.from, to: always.to}, ecKeyOf(p256, "ana always"))
|
|
|
second := accuracyOf(time.Second)
|
|
|
|
|
|
// sealedBy is the signature-time-stamp of tsa at t with the accuracy acc.
|
|
|
sealedBy := func(tsa cmsCert, t time.Time, o tokOpts) func([]byte) []byte {
|
|
|
return func(sig []byte) []byte { return cmsToken(sig, t, o, tsa) }
|
|
|
}
|
|
|
sealed := sealedBy(tsa, t0, tokOpts{accuracy: second})
|
|
|
|
|
|
// A: one required signer, Ana, in each of her kinds, beside each kind
|
|
|
// of a foreign signer, in a capsule, and each kind alone without a round
|
|
|
// time, at the time of the seals and with a key 3 beside it.
|
|
|
signersA := signersOf(ana)
|
|
|
mA := cmsMessage(c0, signersA)
|
|
|
anaKinds := []kind{
|
|
|
{"valid, sealed before the round time", ana, signOpts{token: sealed}},
|
|
|
{"valid, sealed after the round time", ana, signOpts{token: sealedBy(tsa, round.Add(time.Hour), tokOpts{})}},
|
|
|
{"valid, sealed by an authority of RSA at a fraction of a second", ana, signOpts{token: sealedBy(tsaRSA, t0.Add(250*time.Millisecond), tokOpts{})}},
|
|
|
{"valid with SHA-384, sealed with an imprint of SHA-512", ana, signOpts{hash: crypto.SHA384, token: sealedBy(tsa, t0, tokOpts{hash: crypto.SHA512, accuracy: second})}},
|
|
|
{"valid, named by its subjectKeyIdentifier", ana, signOpts{ski: true, token: sealed}},
|
|
|
{"invalid: a bit of its signature flipped", ana, signOpts{corrupt: true, token: sealed}},
|
|
|
{"invalid: a message-digest of another message", ana, signOpts{message: []byte("another message"), token: sealed}},
|
|
|
{"not verifiable: SHA-1", ana, signOpts{hash: crypto.SHA1, token: sealed}},
|
|
|
{"without seal", ana, signOpts{}},
|
|
|
{"invalid seal: over other bytes", ana, signOpts{token: func([]byte) []byte { return cmsToken([]byte("other bytes"), t0, tokOpts{}, tsa) }}},
|
|
|
{"invalid seal: the authority expired at its time", ana, signOpts{token: sealedBy(tsa, utc(2040, 1, 1, 0, 0, 0, 1), tokOpts{})}},
|
|
|
{"invalid seal: a TSTInfo of version 2", ana, signOpts{token: sealedBy(tsa, t0, tokOpts{version: 2})}},
|
|
|
{"invalid seal: a token of two SignerInfo", ana, signOpts{token: sealedBy(tsa, t0, tokOpts{twice: true})}},
|
|
|
{"invalid seal: an authority with a key of 1024 bits", ana, signOpts{token: sealedBy(tsa1024, t0, tokOpts{})}},
|
|
|
{"invalid seal: an authority with a compressed key", ana, signOpts{token: sealedBy(tsaComp, t0, tokOpts{})}},
|
|
|
{"invalid seal: a token signed with SHA-1", ana, signOpts{token: sealedBy(tsa, t0, tokOpts{sigHash: crypto.SHA1})}},
|
|
|
}
|
|
|
foreignKinds := []kind{
|
|
|
{"valid", otro, signOpts{token: sealed}},
|
|
|
{"valid, of RSA", luis, signOpts{token: sealedBy(tsaRSA, t0, tokOpts{accuracy: accuracyOf(999*time.Millisecond + 999*time.Microsecond)})}},
|
|
|
{"invalid", otro, signOpts{corrupt: true, token: sealed}},
|
|
|
{"not verifiable", comprimida, signOpts{token: sealed}},
|
|
|
{"without seal", otro, signOpts{}},
|
|
|
{"with an invalid seal", otro, signOpts{token: sealedBy(tsa1024, t0, tokOpts{})}},
|
|
|
{"out of validity", caducada, signOpts{token: sealed}},
|
|
|
{"not yet valid", futura, signOpts{token: sealed}},
|
|
|
}
|
|
|
type member struct {
|
|
|
name string
|
|
|
m *cmsSigned
|
|
|
}
|
|
|
memberOf := func(msg []byte, k kind) member {
|
|
|
return member{k.name, &cmsSigned{k.cert, signerInfo(msg, k.cert, k.o)}}
|
|
|
}
|
|
|
required := []member{{"absent", nil}}
|
|
|
for _, k := range anaKinds {
|
|
|
required = append(required, memberOf(mA, k))
|
|
|
}
|
|
|
foreign := []member{{"none", nil}}
|
|
|
for _, k := range foreignKinds {
|
|
|
foreign = append(foreign, memberOf(mA, k))
|
|
|
}
|
|
|
// Each required kind alone, in each context and with a key 3, and beside
|
|
|
// two foreign kinds, which go round, so that each foreign kind meets
|
|
|
// several required ones.
|
|
|
testSeal := must(capsule.EncodeSeal(capsule.SealTypeTest, []byte{1}))
|
|
|
for i, r := range required {
|
|
|
for j, f := range foreign {
|
|
|
if j != 0 && j != 1+(2*i)%(len(foreign)-1) && j != 1+(2*i+1)%(len(foreign)-1) {
|
|
|
continue
|
|
|
}
|
|
|
var ms []cmsSigned
|
|
|
for _, m := range []*cmsSigned{r.m, f.m} {
|
|
|
if m != nil {
|
|
|
ms = append(ms, *m)
|
|
|
}
|
|
|
}
|
|
|
if len(ms) == 0 {
|
|
|
continue
|
|
|
}
|
|
|
area := cmsArea(signersA, signedData(ms...), nil)
|
|
|
name := fmt.Sprintf("Ana %s, a foreign signer %s", r.name, f.name)
|
|
|
addCMS("one signer", name, area, c0)
|
|
|
if f.m == nil {
|
|
|
addCMS("one signer", name+", without a round time", area, cZero)
|
|
|
addCMS("one signer", name+", opening at the time of the seals", area, cEarly)
|
|
|
if i%4 == 1 {
|
|
|
addCMS("one signer", name+", in the context of another head", area, cHead)
|
|
|
}
|
|
|
addCMS("one signer", name+", with a key 3", cmsArea(signersA, signedData(ms...), testSeal), c0)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// The signers that are naturally out of validity or not verifiable,
|
|
|
// required.
|
|
|
for _, k := range []kind{
|
|
|
{"a signer whose certificate expired before her seal", caducada, signOpts{token: sealed}},
|
|
|
{"the same, sealed before it expired", caducada, signOpts{token: sealedBy(tsa, utc(2026, 8, 30, 11, 59, 59, 0), tokOpts{})}},
|
|
|
{"a signer whose certificate was not yet valid at her seal", futura, signOpts{token: sealed}},
|
|
|
{"the same, sealed once it was valid", futura, signOpts{token: sealedBy(tsa, utc(2027, 1, 1, 0, 0, 0, 0), tokOpts{})}},
|
|
|
{"a signer with a compressed key", comprimida, signOpts{token: sealed}},
|
|
|
{"an RSA signer, sealed by an authority of RSA", luis, signOpts{token: sealedBy(tsaRSA, t0, tokOpts{accuracy: second})}},
|
|
|
{"an RSA signer with an ECDSA algorithm", luis, signOpts{sigAlg: derAlg(oidECDSA256), token: sealed}},
|
|
|
{"an RSA signer with SHA-512", luis, signOpts{hash: crypto.SHA512, token: sealed}},
|
|
|
{"a signer of P-384 named by givenName and surname", bea, signOpts{hash: crypto.SHA384, token: sealed}},
|
|
|
} {
|
|
|
signers := signersOf(k.cert)
|
|
|
m := memberOf(cmsMessage(c0, signers), k)
|
|
|
addCMS("one signer", k.name, cmsArea(signers, signedData(*m.m), nil), c0)
|
|
|
}
|
|
|
|
|
|
// Two foreign signers, which show in the order of the encoding, beside a
|
|
|
// required one; without seals, so that the area stays small.
|
|
|
two := []cmsSigned{*required[9].m}
|
|
|
for _, c := range []cmsCert{otro, caducada} {
|
|
|
two = append(two, *memberOf(mA, kind{"", c, signOpts{}}).m)
|
|
|
}
|
|
|
addCMS("order", "Ana without seal, and two foreign signers without seal", cmsArea(signersA, signedData(two...), nil), c0)
|
|
|
|
|
|
// SIGNERS of 16 entries, the most, and of 17, with Ana among them, beside
|
|
|
// her signature over the AUTHOR_MESSAGE of those very SIGNERS, or of
|
|
|
// SIGNERS with her alone: 17 entries are F1, whatever the signature.
|
|
|
for _, n := range []int{16, 17} {
|
|
|
hashes := [][32]byte{sha256.Sum256(ana.raw)}
|
|
|
for i := 1; i < n; i++ {
|
|
|
hashes = append(hashes, digest(fmt.Sprintf("datekeys-dart: a certificate that signs nothing, %d", i)))
|
|
|
}
|
|
|
var e bytes.Buffer
|
|
|
e.Write(head(4, uint64(n)))
|
|
|
slices.SortFunc(hashes, func(a, b [32]byte) int { return bytes.Compare(a[:], b[:]) })
|
|
|
for _, h := range hashes {
|
|
|
e.Write(bstr(h[:]))
|
|
|
}
|
|
|
signers := e.Bytes()
|
|
|
if n == 16 && !bytes.Equal(signers, signersOfHashes(hashes)) {
|
|
|
panic("SIGNERS is not what EncodeSigners writes")
|
|
|
}
|
|
|
own := memberOf(cmsMessage(c0, signers), kind{"", ana, signOpts{token: sealed}})
|
|
|
addCMS("SIGNERS", fmt.Sprintf("SIGNERS of %d entries, Ana valid among them", n), cmsArea(signers, signedData(*own.m), nil), c0)
|
|
|
addCMS("SIGNERS", fmt.Sprintf("SIGNERS of %d entries, beside the signature of Ana for SIGNERS with her alone", n), cmsArea(signers, signedData(*required[1].m), nil), c0)
|
|
|
}
|
|
|
|
|
|
// B: three required signers, Ana, Luis and Beatriz, each absent or in
|
|
|
// one of her kinds, beside foreign signers, drawn from the seed.
|
|
|
signersB := signersOf(ana, luis, bea)
|
|
|
mB := cmsMessage(c0, signersB)
|
|
|
kindsB := [][]member{{{"absent", nil}}, {{"absent", nil}}, {{"absent", nil}}}
|
|
|
for i, c := range []cmsCert{ana, luis, bea} {
|
|
|
h := crypto.SHA256
|
|
|
if c.key == bea.key {
|
|
|
h = crypto.SHA384
|
|
|
}
|
|
|
for _, k := range []kind{
|
|
|
{"valid", c, signOpts{hash: h, token: sealed}},
|
|
|
{"valid, sealed after the round time", c, signOpts{hash: h, token: sealedBy(tsa, round, tokOpts{})}},
|
|
|
{"valid, sealed by an authority of RSA", c, signOpts{hash: h, token: sealedBy(tsaRSA, t0.Add(time.Nanosecond), tokOpts{})}},
|
|
|
{"invalid", c, signOpts{hash: h, corrupt: true, token: sealed}},
|
|
|
{"without seal", c, signOpts{hash: h}},
|
|
|
{"with an invalid seal", c, signOpts{hash: h, token: sealedBy(tsa, utc(2041, 1, 1, 0, 0, 0, 0), tokOpts{})}},
|
|
|
} {
|
|
|
kindsB[i] = append(kindsB[i], memberOf(mB, k))
|
|
|
}
|
|
|
}
|
|
|
foreignB := []member{}
|
|
|
for _, k := range []kind{
|
|
|
{"Otro, valid", otro, signOpts{token: sealed}},
|
|
|
{"Otro, invalid", otro, signOpts{corrupt: true}},
|
|
|
{"the expired one, sealed", caducada, signOpts{token: sealed}},
|
|
|
{"the compressed one, sealed", comprimida, signOpts{token: sealed}},
|
|
|
} {
|
|
|
foreignB = append(foreignB, memberOf(mB, k))
|
|
|
}
|
|
|
allValid := []cmsSigned{*kindsB[0][1].m, *kindsB[1][1].m, *kindsB[2][1].m}
|
|
|
for _, ctx := range []int{c0, cZero, cEarly, cHead} {
|
|
|
addCMS("three signers", fmt.Sprintf("all three valid, in the context %q", contexts[ctx].name), cmsArea(signersB, signedData(allValid...), nil), ctx)
|
|
|
}
|
|
|
addCMS("three signers", "all three valid, with two foreign signers", cmsArea(signersB, signedData(append(slices.Clone(allValid), *foreignB[0].m, *foreignB[2].m)...), nil), c0)
|
|
|
for range 40 {
|
|
|
var names []string
|
|
|
var ms []cmsSigned
|
|
|
for i, who := range []string{"Ana", "Luis", "Beatriz"} {
|
|
|
// Mostly valid, so that the other kinds stand out.
|
|
|
j := 1
|
|
|
if cmsRng.IntN(3) == 0 {
|
|
|
j = cmsRng.IntN(len(kindsB[i]))
|
|
|
}
|
|
|
m := kindsB[i][j]
|
|
|
names = append(names, who+" "+m.name)
|
|
|
if m.m != nil {
|
|
|
ms = append(ms, *m.m)
|
|
|
}
|
|
|
}
|
|
|
for _, f := range foreignB {
|
|
|
if cmsRng.IntN(5) == 0 {
|
|
|
names = append(names, "a foreign signer, "+f.name)
|
|
|
ms = append(ms, *f.m)
|
|
|
}
|
|
|
}
|
|
|
var seal []byte
|
|
|
if cmsRng.IntN(6) == 0 {
|
|
|
names = append(names, "a key 3")
|
|
|
seal = testSeal
|
|
|
}
|
|
|
if len(ms) == 0 {
|
|
|
continue
|
|
|
}
|
|
|
ctx := []int{c0, c0, c0, cZero, cEarly, cHead}[cmsRng.IntN(6)]
|
|
|
addCMS("three signers", strings.Join(names, "; "), cmsArea(signersB, signedData(ms...), seal), ctx)
|
|
|
}
|
|
|
|
|
|
// C: the validity of a certificate at t, the time of the seal, both
|
|
|
// ends included, at the nanosecond; and of the authority, at genTime.
|
|
|
y, x := utc(2026, 1, 1, 0, 0, 0, 0), utc(2027, 1, 1, 0, 0, 0, 0)
|
|
|
edge := newCMSCert("edge", certSpec{cn: "Firmante al l\u00edmite", from: y, to: x}, ecKeyOf(p256, "edge"))
|
|
|
signersC := signersOf(edge)
|
|
|
mC := cmsMessage(c0, signersC)
|
|
|
for _, at := range []struct {
|
|
|
name string
|
|
|
t time.Time
|
|
|
raw []byte
|
|
|
}{
|
|
|
{"a nanosecond before notBefore", y.Add(-time.Nanosecond), nil},
|
|
|
{"at notBefore", y, nil},
|
|
|
{"a nanosecond after notBefore", y.Add(time.Nanosecond), nil},
|
|
|
{"half a second before notAfter", x.Add(-500 * time.Millisecond), nil},
|
|
|
{"at notAfter", x, nil},
|
|
|
{"a nanosecond after notAfter", x.Add(time.Nanosecond), nil},
|
|
|
{"a second after notAfter", x.Add(time.Second), nil},
|
|
|
{"at notAfter, with ten digits of fraction that Go cuts to zero", x, derTLV(0x18, []byte("20270101000000.0000000001Z"))},
|
|
|
{"just before notAfter, with ten digits of fraction", x, derTLV(0x18, []byte("20261231235959.9999999999Z"))},
|
|
|
} {
|
|
|
k := kind{at.name, edge, signOpts{token: sealedBy(tsaAlways, at.t, tokOpts{genTime: at.raw})}}
|
|
|
m := memberOf(mC, k)
|
|
|
addCMS("validity", "a signer sealed "+at.name, cmsArea(signersC, signedData(*m.m), nil), c0)
|
|
|
}
|
|
|
edge50 := newCMSCert("edge 2050", certSpec{cn: "Firmante de 2050", from: utc(2049, 12, 31, 23, 59, 59, 0), to: utc(2050, 1, 1, 0, 0, 0, 0)}, ecKeyOf(p256, "edge 2050"))
|
|
|
signers50 := signersOf(edge50)
|
|
|
m50 := cmsMessage(c0, signers50)
|
|
|
for _, at := range []time.Time{utc(2049, 12, 31, 23, 59, 58, 500000000), utc(2049, 12, 31, 23, 59, 59, 0), utc(2050, 1, 1, 0, 0, 0, 0), utc(2050, 1, 1, 0, 0, 0, 1)} {
|
|
|
k := kind{"", edge50, signOpts{token: sealedBy(tsaAlways, at, tokOpts{})}}
|
|
|
m := memberOf(m50, k)
|
|
|
addCMS("validity", "a signer valid from a UTCTime to a GeneralizedTime, sealed at "+at.Format(time.RFC3339Nano), cmsArea(signers50, signedData(*m.m), nil), c0)
|
|
|
}
|
|
|
tsaEdge := newCMSCert("tsa edge", certSpec{cn: "Autoridad al l\u00edmite", from: y, to: x}, ecKeyOf(p256, "tsa edge"))
|
|
|
subject0 := capsule.SealSubject(cc, hd, capsule.SigPart(nil))
|
|
|
for _, at := range []struct {
|
|
|
name string
|
|
|
t time.Time
|
|
|
}{
|
|
|
{"a nanosecond before the authority was valid", y.Add(-time.Nanosecond)},
|
|
|
{"when the authority became valid", y},
|
|
|
{"when the authority expired", x},
|
|
|
{"a nanosecond after the authority expired", x.Add(time.Nanosecond)},
|
|
|
} {
|
|
|
m := memberOf(mA, kind{at.name, ana, signOpts{token: sealedBy(tsaEdge, at.t, tokOpts{})}})
|
|
|
addCMS("validity", "Ana sealed "+at.name, cmsArea(signersA, signedData(*m.m), nil), c0)
|
|
|
addCMS("validity", "a seal of key 3 "+at.name, sealArea(nil, cmsToken(subject0[:], at.t, tokOpts{}, tsaEdge)), c0)
|
|
|
}
|
|
|
|
|
|
// D: t plus the accuracy against the round time, at the nanosecond, for
|
|
|
// a signer and for a seal of key 3 beside no signature and beside one of
|
|
|
// alg 1; and without a round time.
|
|
|
k0 := keyOf("datekeys-dart: author 0")
|
|
|
alg1 := must(capsule.EncodeAuthorSignature(capsule.AlgEd25519, k0.Public(), k0.Sign(contexts[c0].message())))
|
|
|
subject1 := capsule.SealSubject(cc, hd, capsule.SigPart(alg1))
|
|
|
for _, v := range []struct {
|
|
|
name string
|
|
|
t time.Time
|
|
|
accuracy time.Duration
|
|
|
tsa cmsCert
|
|
|
signer cmsCert
|
|
|
}{
|
|
|
{"no accuracy", t0, 0, tsa, ana},
|
|
|
{"an accuracy of a second", t0, time.Second, tsa, ana},
|
|
|
{"an accuracy of 999 ms and 999 \u00b5s", t0, 999*time.Millisecond + 999*time.Microsecond, tsa, ana},
|
|
|
{"t with nine digits of fraction and an accuracy of a microsecond", t0.Add(123456789), time.Microsecond, tsaRSA, ana},
|
|
|
{"an accuracy of 2^31 - 1 seconds", t0, (1<<31 - 1) * time.Second, tsa, ana},
|
|
|
{"an accuracy of a second, a millisecond and a microsecond", t0, time.Second + time.Millisecond + time.Microsecond, tsa, ana},
|
|
|
{"t at 0001-01-01T00:00:00Z, the zero time of Go, and an accuracy of a second", utc(1, 1, 1, 0, 0, 0, 0), time.Second, tsaAlways, anaAlways},
|
|
|
{"t at the last second of 9999 and an accuracy of 2^31 - 1 seconds", utc(9999, 12, 31, 23, 59, 59, 0), (1<<31 - 1) * time.Second, tsaAlways, anaAlways},
|
|
|
{"t at the last nanosecond of 9999, after the authority expired", utc(9999, 12, 31, 23, 59, 59, 999999999), (1<<31 - 1) * time.Second, tsaAlways, anaAlways},
|
|
|
} {
|
|
|
o := tokOpts{}
|
|
|
if v.accuracy != 0 {
|
|
|
o.accuracy = accuracyOf(v.accuracy)
|
|
|
}
|
|
|
signers := signersOf(v.signer)
|
|
|
m := memberOf(cmsMessage(c0, signers), kind{v.name, v.signer, signOpts{token: sealedBy(v.tsa, v.t, o)}})
|
|
|
areas := []struct {
|
|
|
name string
|
|
|
b []byte
|
|
|
}{
|
|
|
{"a signer", cmsArea(signers, signedData(*m.m), nil)},
|
|
|
{"a seal of key 3", sealArea(nil, cmsToken(subject0[:], v.t, o, v.tsa))},
|
|
|
{"a seal of key 3 beside a signature of alg 1", sealArea(alg1, cmsToken(subject1[:], v.t, o, v.tsa))},
|
|
|
}
|
|
|
end := v.t.Add(v.accuracy)
|
|
|
var ctxs []int
|
|
|
for _, d := range []time.Duration{-1, 0, 1} {
|
|
|
r := end.Add(d)
|
|
|
if r.Year() > 9999 || r.IsZero() {
|
|
|
continue
|
|
|
}
|
|
|
ctxs = append(ctxs, edgeContext(fmt.Sprintf("a round time of t plus the accuracy %+d ns, %s", d, v.name), r))
|
|
|
}
|
|
|
if v.t.Year() == 9999 {
|
|
|
ctxs = append(ctxs, edgeContext("a round time at the last second of 9999, "+v.name, utc(9999, 12, 31, 23, 59, 59, 0)))
|
|
|
}
|
|
|
ctxs = append(ctxs, cZero)
|
|
|
for _, a := range areas {
|
|
|
for _, ctx := range ctxs {
|
|
|
addCMS("round time", fmt.Sprintf("%s with %s, in the context %q", a.name, v.name, contexts[ctx].name), a.b, ctx)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// E: a seal of key 3 of each kind beside a signature of each kind: it
|
|
|
// seals SIG_PART of the exact content of key 2, whatever its verdict.
|
|
|
other := capsule.AuthorMessage(cc, digest("datekeys-dart: not this head"), capsule.SignersDigest(capsule.AlgEd25519, nil))
|
|
|
signatures := []struct {
|
|
|
name string
|
|
|
content []byte
|
|
|
}{
|
|
|
{"no signature", nil},
|
|
|
{"a signature of alg 1", alg1},
|
|
|
{"a signature of alg 1 over another message", must(capsule.EncodeAuthorSignature(capsule.AlgEd25519, k0.Public(), k0.Sign(other)))},
|
|
|
{"a valid signature of alg 2", must(capsule.EncodeAuthorSignature(capsule.AlgCMS, signersA, signedData(*required[1].m)))},
|
|
|
{"a signature of alg 2 with its signer invalid", must(capsule.EncodeAuthorSignature(capsule.AlgCMS, signersA, signedData(*required[6].m)))},
|
|
|
{"a signature of alg 4294967295", must(capsule.EncodeAuthorSignature(capsule.AlgTest, k0.Public(), k0.Sign(other)))},
|
|
|
{"a signature of alg 2 with SIGNERS empty", must(capsule.EncodeAuthorSignature(capsule.AlgCMS, []byte{0x80}, signedData(*required[1].m)))},
|
|
|
}
|
|
|
for _, s := range signatures {
|
|
|
subject := capsule.SealSubject(cc, hd, capsule.SigPart(s.content))
|
|
|
for _, t := range []struct {
|
|
|
name string
|
|
|
token []byte
|
|
|
}{
|
|
|
{"before the round time", cmsToken(subject[:], t0, tokOpts{accuracy: second}, tsa)},
|
|
|
{"at the round time", cmsToken(subject[:], round, tokOpts{}, tsa)},
|
|
|
{"by an authority of RSA", cmsToken(subject[:], t0, tokOpts{hash: crypto.SHA256, sigHash: crypto.SHA512}, tsaRSA)},
|
|
|
{"over another subject", cmsToken(subject0[:], t0, tokOpts{}, tsa)},
|
|
|
{"with an imprint of SHA-384", cmsToken(subject[:], t0, tokOpts{hash: crypto.SHA384}, tsa)},
|
|
|
{"signed with SHA-1", cmsToken(subject[:], t0, tokOpts{sigHash: crypto.SHA1}, tsa)},
|
|
|
{"by an authority with a key of 1024 bits", cmsToken(subject[:], t0, tokOpts{}, tsa1024)},
|
|
|
{"of a TSTInfo of version 2", cmsToken(subject[:], t0, tokOpts{version: 2}, tsa)},
|
|
|
} {
|
|
|
addCMS("seal", fmt.Sprintf("a seal %s beside %s", t.name, s.name), sealArea(s.content, t.token), c0)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// F: mutations of three areas, one edit each, of the SignedData of a
|
|
|
// signature of alg 2, of its SIGNERS or of the token of a seal of key 3,
|
|
|
// drawn from the seed and swept bit by bit; the area is written again
|
|
|
// with the encoders of capsule.
|
|
|
base0 := cmsArea(signersB, signedData(append(slices.Clone(allValid), *foreignB[0].m)...), nil)
|
|
|
base1 := sealArea(alg1, cmsToken(subject1[:], t0, tokOpts{accuracy: accuracyOf(time.Second + 500*time.Millisecond)}, tsa))
|
|
|
base2 := sealArea(nil, cmsToken(subject0[:], t0.Add(time.Millisecond), tokOpts{sigHash: crypto.SHA384}, tsaRSA))
|
|
|
cmsBases = [][]byte{base0, base1, base2}
|
|
|
mutate := func(base int, target string, e []any, ctx int) {
|
|
|
b := cmsBases[base]
|
|
|
content, value, _ := capsule.SecurityKey2(b)
|
|
|
var seal []byte
|
|
|
if _, token, err := capsule.SecurityKey3(b); err == nil {
|
|
|
seal = must(capsule.EncodeSeal(capsule.SealTypeRFC3161, token))
|
|
|
value = token
|
|
|
}
|
|
|
var signers []byte
|
|
|
if content != nil {
|
|
|
_, signers, _, _ = capsule.DecodeAuthorSignature(content)
|
|
|
}
|
|
|
edit := func(t []byte) []byte {
|
|
|
at, n := e[0].(int), e[1].(int)
|
|
|
return append([]byte{}, slices.Concat(t[:at], must(hex.DecodeString(e[2].(string))), t[at+n:])...)
|
|
|
}
|
|
|
switch target {
|
|
|
case "value":
|
|
|
content = must(capsule.EncodeAuthorSignature(capsule.AlgCMS, signers, edit(value)))
|
|
|
case "signers":
|
|
|
_, _, sigValue, _ := capsule.DecodeAuthorSignature(content)
|
|
|
content = must(capsule.EncodeAuthorSignature(capsule.AlgCMS, edit(signers), sigValue))
|
|
|
case "token":
|
|
|
seal = must(capsule.EncodeSeal(capsule.SealTypeRFC3161, edit(value)))
|
|
|
}
|
|
|
area := must(capsule.EncodeSecurityWith(content, seal))
|
|
|
sum := sha256.Sum256(area)
|
|
|
c := Case{"group": "mutations", "base": base, "target": target, "edits": [][]any{e}, "sha256": hx(sum[:8])}
|
|
|
cmsCases = append(cmsCases, evaluate(c, area, ctx))
|
|
|
}
|
|
|
targetOf := func(base int, target string) []byte {
|
|
|
b := cmsBases[base]
|
|
|
switch target {
|
|
|
case "value":
|
|
|
_, v, _ := capsule.SecurityKey2(b)
|
|
|
return v
|
|
|
case "signers":
|
|
|
c, _, _ := capsule.SecurityKey2(b)
|
|
|
_, s, _, _ := capsule.DecodeAuthorSignature(c)
|
|
|
return s
|
|
|
}
|
|
|
_, t, _ := capsule.SecurityKey3(b)
|
|
|
return t
|
|
|
}
|
|
|
for _, m := range []struct {
|
|
|
base int
|
|
|
target string
|
|
|
random int
|
|
|
sweep int
|
|
|
}{
|
|
|
{0, "value", 140, 64},
|
|
|
{0, "signers", 16, 0},
|
|
|
{1, "token", 90, 32},
|
|
|
{2, "token", 60, 24},
|
|
|
} {
|
|
|
t := targetOf(m.base, m.target)
|
|
|
for range m.random {
|
|
|
mutate(m.base, m.target, randomEditOf(cmsRng, t), []int{c0, c0, cZero}[cmsRng.IntN(3)])
|
|
|
}
|
|
|
for i := range m.sweep {
|
|
|
at := i * len(t) / m.sweep
|
|
|
mutate(m.base, m.target, []any{at, 1, hx([]byte{t[at] ^ 1<<(i%8)})}, c0)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// 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))
|
|
|
|
|
|
// The signatures of alg 2 and the seals of seal_type 2, with contexts
|
|
|
// and texts of their own.
|
|
|
contexts, texts, textIndex = nil, nil, map[string]int{}
|
|
|
cmsSecurityCases()
|
|
|
writeCMS(*outDir, *testdata)
|
|
|
}
|