You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/internal/testkit/genfixtures/cmsvectors.go

261 lines
13 KiB

package main
import (
"bytes"
"crypto"
"crypto/elliptic"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"g.activething.com/go/DateKeys/authorkey"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/datekey"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/cms/cmstest"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/locator"
"g.activething.com/go/DateKeys/profile"
)
// frozenVectors writes testdata/vectors/security_cms.json and locator.json
// when they are missing: their bytes hold the randomness of certificates, of
// age and of tlock, so they are made once and kept, as the fixtures are.
// Delete a file to make it again.
func frozenVectors(dir string) error {
for name, gen := range map[string]func() (any, error){
"security_cms.json": securityCMSVectors,
"locator.json": locatorVectors,
} {
path := filepath.Join(dir, name)
if _, err := os.Stat(path); err == nil {
continue
}
v, err := gen()
if err != nil {
return fmt.Errorf("%s: %w", name, err)
}
if err := testkit.WriteJSON(path, v); err != nil {
return err
}
}
return nil
}
var (
vecRound = time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)
vecSigned = time.Date(2026, 9, 30, 12, 0, 0, 0, time.UTC)
)
func hex32(b [32]byte) string { return hex.EncodeToString(b[:]) }
// cmsArea builds SECURITY_CBOR with an alg 2 signature by the signers over
// the context c, sealed by tsa at when; required lists who must sign.
func cmsArea(c *capsule.SecurityContext, required, signers []cmstest.Signer, tsa cmstest.Signer, when time.Time, seal []byte) ([]byte, error) {
var hashes [][32]byte
for _, s := range required {
hashes = append(hashes, sum256(s.Cert.Raw))
}
list, err := capsule.EncodeSigners(hashes)
if err != nil {
return nil, err
}
msg := capsule.AuthorMessage(c.ControlCommit, c.HeadDigest, capsule.SignersDigest(capsule.AlgCMS, list))
opts := cmstest.Options{}
if tsa.Key != nil {
opts.Token = func(sig []byte) []byte {
return cmstest.Token(sig, when, cmstest.TokenOptions{Accuracy: time.Second}, tsa)
}
}
content, err := capsule.EncodeAuthorSignature(capsule.AlgCMS, list, cmstest.Signature(msg, opts, signers...))
if err != nil {
return nil, err
}
return capsule.EncodeSecurityWith(content, seal)
}
func securityCMSVectors() (any, error) {
ana := cmstest.NewECDSA("Ana López", elliptic.P256(), certFrom, certTo)
luis := cmstest.NewRSA("Luis Gómez", 2048, certFrom, certTo)
otro := cmstest.NewECDSA("Otro", elliptic.P384(), certFrom, certTo)
tsa := cmstest.NewECDSA("Autoridad de Sellado de prueba", elliptic.P256(), certFrom, certTo)
ctx := func() *capsule.SecurityContext {
return &capsule.SecurityContext{ControlCommit: sha256.Sum256([]byte("control")), HeadDigest: sha256.Sum256([]byte("head")), RoundTime: vecRound}
}
key, err := authorkey.NewFromSeed(bytes.Repeat([]byte{7}, 32))
if err != nil {
return nil, err
}
f := testkit.CMSVectorFile{
Spec: testkit.SpecVersion,
Description: "SECURITY_CBOR with an author signature of alg 2 or a time seal of seal_type 2, its context and the verdicts of spec v0.11 29.7, 29.10 and 29.11. " +
"Certificates and tokens are made once with test keys and the file is frozen. See testdata/README.md.",
}
var errs []error
add := func(name string, area []byte, c *capsule.SecurityContext, wantSig, wantSeal capsule.Verdict) {
var v capsule.Verdicts
vc := testkit.CMSVectorCase{Name: name, SecurityCBOR: hex.EncodeToString(area)}
if c == nil {
v, vc.NoContext = capsule.EvaluateSecurity(area), true
c = ctx()
} else {
v = capsule.EvaluateSecurityIn(area, c)
}
vc.Context = testkit.CMSVectorContext{ControlCommit: hex32(c.ControlCommit), HeadDigest: hex32(c.HeadDigest), RoundTime: c.RoundTime.UTC().Format(time.RFC3339)}
vc.Signature, vc.Seal = string(v.Signature), string(v.Seal)
if v.Signature != wantSig || v.Seal != wantSeal {
errs = append(errs, fmt.Errorf("%s: verdicts %s and %s, want %s and %s", name, v.Signature, v.Seal, wantSig, wantSeal))
}
if d := v.Detail; d != nil {
vc.Signers, vc.Foreign = signerResults(d.Signers), signerResults(d.Foreign)
if !d.SealTime.IsZero() {
vc.SealHolder, vc.SealTime = d.SealHolder, d.SealTime.UTC().Format(time.RFC3339)
}
}
f.Cases = append(f.Cases, vc)
}
must := func(b []byte, err error) []byte {
if err != nil {
errs = append(errs, err)
}
return b
}
both := []cmstest.Signer{ana, luis}
c := ctx()
add("alg 2: two signers, each sealed before the round time", must(cmsArea(c, both, both, tsa, vecSigned, nil)), c, capsule.VerdictSignedComplete, capsule.VerdictNoSeal)
add("alg 2: a seal after the round time proves nothing before it", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, vecRound.Add(time.Hour), nil)), c, capsule.VerdictSignedComplete, capsule.VerdictNoSeal)
add("alg 2: a signer who is not required shows apart", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana, otro}, tsa, vecSigned, nil)), c, capsule.VerdictSignedComplete, capsule.VerdictNoSeal)
add("alg 2: a required signer is absent", must(cmsArea(c, both, []cmstest.Signer{ana}, tsa, vecSigned, nil)), c, capsule.VerdictSignedIncomplete, capsule.VerdictNoSeal)
add("alg 2: no seal", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, cmstest.Signer{}, vecSigned, nil)), c, capsule.VerdictSignedIncomplete, capsule.VerdictNoSeal)
add("alg 2: a seal from before the certificate was valid", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, certFrom.AddDate(-1, 0, 0), nil)), c, capsule.VerdictSignedIncomplete, capsule.VerdictNoSeal)
add("alg 2: a key 3 beside it", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, vecSigned, must(capsule.EncodeSeal(1, []byte{1})))), c, capsule.VerdictSignedIncomplete, capsule.VerdictSealUnsupported)
other := ctx()
other.HeadDigest[5] ^= 9
add("alg 2: another head", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, vecSigned, nil)), other, capsule.VerdictSignatureInvalid, capsule.VerdictNoSeal)
add("alg 2: without the context of a capsule", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, vecSigned, nil)), nil, capsule.VerdictSignatureUnchecked, capsule.VerdictNoSeal)
one, two := sum256([]byte("a")), sum256([]byte("b"))
if bytes.Compare(one[:], two[:]) > 0 {
one, two = two, one
}
unsorted := append(append([]byte{0x82, 0x58, 0x20}, two[:]...), append([]byte{0x58, 0x20}, one[:]...)...)
for name, signers := range map[string][]byte{"alg 2: SIGNERS out of order": unsorted, "alg 2: an empty SIGNERS": {0x80}} {
content, _ := capsule.EncodeAuthorSignature(capsule.AlgCMS, signers, []byte{0x30, 0x00})
add(name, must(capsule.EncodeSecurityWith(content, nil)), c, capsule.VerdictSignatureUnchecked, capsule.VerdictNoSeal)
}
{
list, _ := capsule.EncodeSigners([][32]byte{sum256(ana.Cert.Raw)})
content, _ := capsule.EncodeAuthorSignature(capsule.AlgCMS, list, []byte("not DER"))
add("alg 2: not a CMS", must(capsule.EncodeSecurityWith(content, nil)), c, capsule.VerdictSignatureUnchecked, capsule.VerdictNoSeal)
}
// Seals of seal_type 2 over an alg 1 signature.
msg := capsule.AuthorMessage(c.ControlCommit, c.HeadDigest, capsule.SignersDigest(capsule.AlgEd25519, nil))
sig := must(capsule.EncodeAuthorSignature(capsule.AlgEd25519, key.Public(), key.Sign(msg)))
subject := capsule.SealSubject(c.ControlCommit, c.HeadDigest, capsule.SigPart(sig))
sealed := func(token []byte) []byte {
return must(capsule.EncodeSecurityWith(sig, must(capsule.EncodeSeal(capsule.SealTypeRFC3161, token))))
}
tok := func(subject []byte, when time.Time, o cmstest.TokenOptions, s cmstest.Signer) []byte {
return cmstest.Token(subject, when, o, s)
}
okSig := capsule.VerdictSignedOther
add("seal: before the round time", sealed(tok(subject[:], vecSigned, cmstest.TokenOptions{}, tsa)), c, okSig, capsule.VerdictSealed)
add("seal: after the round time", sealed(tok(subject[:], vecRound.Add(time.Minute), cmstest.TokenOptions{}, tsa)), c, okSig, capsule.VerdictSealedLate)
add("seal: the accuracy reaches the round time", sealed(tok(subject[:], vecRound.Add(-time.Second), cmstest.TokenOptions{Accuracy: 2 * time.Second}, tsa)), c, okSig, capsule.VerdictSealedLate)
add("seal: over another subject", sealed(tok([]byte("other"), vecSigned, cmstest.TokenOptions{}, tsa)), c, okSig, capsule.VerdictSealInvalid)
add("seal: the authority had expired at its time", sealed(tok(subject[:], certTo.AddDate(1, 0, 0), cmstest.TokenOptions{}, tsa)), c, okSig, capsule.VerdictSealInvalid)
add("seal: a TSTInfo of version 2", sealed(tok(subject[:], vecSigned, cmstest.TokenOptions{Version: 2}, tsa)), c, okSig, capsule.VerdictSealUnreadable)
add("seal: not DER", sealed([]byte("not DER")), c, okSig, capsule.VerdictSealUnreadable)
add("seal: SHA-384 in the imprint", sealed(tok(subject[:], vecSigned, cmstest.TokenOptions{Hash: crypto.SHA384}, tsa)), c, okSig, capsule.VerdictSealUnsupported)
add("seal: without a context, as a reader of v0.10", sealed(tok(subject[:], vecSigned, cmstest.TokenOptions{}, tsa)), nil, capsule.VerdictSignatureUnchecked, capsule.VerdictSealUnsupported)
noSig := capsule.SealSubject(c.ControlCommit, c.HeadDigest, capsule.SigPart(nil))
add("seal: over a capsule without a signature", must(capsule.EncodeSecurityWith(nil, must(capsule.EncodeSeal(capsule.SealTypeRFC3161, tok(noSig[:], vecSigned, cmstest.TokenOptions{}, tsa))))),
c, capsule.VerdictNoSignature, capsule.VerdictSealed)
if err := errors.Join(errs...); err != nil {
return nil, err
}
return f, nil
}
// locatorVectors makes the vector of the extension datekeys.capsule: a .dkc of
// patterned bytes in an envelope, hidden in a host, its locator sealed with
// tlock for round 1000, and the data of the extension with a note.
func locatorVectors() (any, error) {
p := profile.Quicknet()
dkc := patterned("locator dkc", 5000)
loc, rest, err := locator.NewEnvelope(dkc)
if err != nil {
return nil, err
}
host := patterned("host file", 3000)
file, offset := locator.Hide(host, rest)
const cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
loc.Addresses = []locator.Address{{URI: "https://ejemplo.org/foto.jpg", Offset: offset}, {URI: "ipfs://" + cid}}
plain, err := loc.Marshal()
if err != nil {
return nil, err
}
const round = 1000
sealed, err := locator.Seal(p, round, loc)
if err != nil {
return nil, err
}
dk, err := datekey.Resolve(p, mustRoundTime(p, round))
if err != nil {
return nil, err
}
const note = "Cartas del viaje a Lisboa"
x, err := (&locator.Info{Note: note, DateKey: dk, Sealed: sealed}).Extension()
if err != nil {
return nil, err
}
if x.ID != extension.CapsuleID {
return nil, errors.New("not datekeys.capsule")
}
v := testkit.LocatorVectorFile{
Spec: testkit.SpecVersion,
Description: "The extension datekeys.capsule of a .dkk and what it points to (spec v0.11, 44.1): an envelope of age with its header apart from its rest, " +
"the rest hidden in a host file, the locator sealed with tlock for round 1000, and the data of the extension. Frozen. See testdata/README.md.",
Round: round, DateKey: dk.Compact(), Note: note, DKC: hex.EncodeToString(dkc), Rest: hex.EncodeToString(rest), Header: hex.EncodeToString(loc.EnvelopeHeader),
Host: hex.EncodeToString(file), HostOffset: offset, Plaintext: hex.EncodeToString(plain), Sealed: hex.EncodeToString(sealed), Extension: hex.EncodeToString(x.Data),
EnvelopeKey: hex.EncodeToString(loc.EnvelopeKey[:]), RestDigest: hex32(loc.RestDigest), RestSize: loc.RestSize, CapsuleDigest: hex32(loc.CapsuleDigest),
}
for _, a := range loc.Addresses {
v.Addresses = append(v.Addresses, testkit.LocatorVectorAddress{URI: a.URI, Offset: a.Offset, Host: a.Host()})
}
for _, base := range []int{100, 3000, 4000, 4060, 4066, 4067, 4068, 4069, 4070, 4071, 4072, 4090, 4094, 4095, 4096, 4097, 4100, 8160, 8190, 8192, 8193, 12000} {
v.PaddingCases = append(v.PaddingCases, testkit.LocatorPaddingCase{Base: base, Total: locator.PlaintextLength(base)})
}
for _, c := range []struct {
uri string
ok bool
}{
{"https://ejemplo.org/a.bin", true}, {"https://ejemplo.org:8443/x?y=1", true}, {"ipfs://" + cid, true}, {"ipfs://" + cid + "/ruta", true},
{"", false}, {"http://ejemplo.org/a", false}, {"file:///etc/passwd", false}, {"ftp://x/y", false}, {"https://user:pass@ejemplo.org/", false},
{"https://", false}, {"ipfs://notacid", false}, {"https://ejemplo.org/ñ", false}, {"https://ejemplo.org/a b", false},
{"https://%D0%B0pple.com/x", false}, {"https://ejemplo.org%E2%80%AEtxt.exe/", false}, {"https://127.0.0.1/", false}, {"https://[::1]/", false},
{"https://0x7f000001/", false}, {"https://a.com:99999999/", false}, {"https://a.com:0/", false}, {"https://xn--pple-43d.com/", true},
} {
if (locator.CheckURI(c.uri) == nil) != c.ok {
return nil, fmt.Errorf("the address %q: accepted %v, want %v", c.uri, !c.ok, c.ok)
}
v.URICases = append(v.URICases, testkit.LocatorURICase{URI: c.uri, OK: c.ok})
}
return v, nil
}
func mustRoundTime(p *profile.Profile, round uint64) time.Time {
t, err := datekey.RoundTime(p, round)
if err != nil {
panic(err)
}
return t
}

Powered by TurnKey Linux.