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.
826 lines
51 KiB
826 lines
51 KiB
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/elliptic"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"g.activething.com/go/DateKeys/authorkey"
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
"g.activething.com/go/DateKeys/internal/cms/cmstest"
|
|
"g.activething.com/go/DateKeys/internal/testkit"
|
|
)
|
|
|
|
// 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 _, v := range []struct {
|
|
name string
|
|
gen func() (any, error)
|
|
}{
|
|
{"security_cms.json", securityCMSVectors},
|
|
{"locator.json", locatorVectors},
|
|
} {
|
|
path := filepath.Join(dir, v.name)
|
|
if _, err := os.Stat(path); err == nil {
|
|
continue
|
|
}
|
|
out, err := v.gen()
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", v.name, err)
|
|
}
|
|
if err := testkit.WriteJSON(path, out); 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)
|
|
)
|
|
|
|
// cmsVectorSpec labels security_cms.json, as every file of testdata, with
|
|
// SpecVersion. Its verdicts are those of v0.16, with the profile of the
|
|
// certificate of §29.10, the texts of §29.7 and the accuracy of §29.11.
|
|
const cmsVectorSpec = testkit.SpecVersion
|
|
|
|
func hex32(b [32]byte) string { return hex.EncodeToString(b[:]) }
|
|
|
|
// The texts of the verdicts, copied from the table of spec v0.16 §29.7: the
|
|
// lines of each case are checked against them, not against Verdicts.Lines.
|
|
const (
|
|
textF0 = "Sin firma de autor."
|
|
textF1 = "No se ha comprobado ninguna firma: trátala como no firmada."
|
|
textF2 = "La firma no corresponde a este contenido."
|
|
textF5 = "Faltan firmas o sellos que la propia cápsula exige: trátala como no firmada."
|
|
textS1 = "Lleva un sello de tiempo que esta versión no sabe comprobar: aquí no prueba nada."
|
|
textS2 = "El sello de tiempo es ilegible: no prueba nada."
|
|
textS3 = "El sello no corresponde a este contenido."
|
|
textS5 = "No acredita que se sellara antes de la fecha de apertura: "
|
|
)
|
|
|
|
// reasonTexts are the reasons of S5 in the texts of §29.7 (v0.16).
|
|
var reasonTexts = map[capsule.SealReason]string{
|
|
capsule.ReasonLate: "se selló después de esa fecha o demasiado cerca de ella",
|
|
capsule.ReasonNoAccuracyBTSP: "el sello no dice la precisión que exige su política",
|
|
capsule.ReasonNoAccuracy: "el sello no dice su precisión",
|
|
}
|
|
|
|
// resultTexts are the results of a signer in the lines of §29.7.
|
|
var resultTexts = map[string]string{
|
|
"valid": "válida", "invalid": "inválida", "not verifiable": "no verificable", "without seal": "sin sello",
|
|
"invalid seal": "con el sello inválido", "out of validity": "con el certificado fuera de validez",
|
|
}
|
|
|
|
// vsigner is a signer with the names of its certificate as §29.7 shows them:
|
|
// the holder and the issuer, or their SHA-256 when they break its rules.
|
|
type vsigner struct {
|
|
cmstest.Signer
|
|
holder, issuer string
|
|
}
|
|
|
|
// named is a signer whose holder and issuer are the commonName cn, as in
|
|
// the self-signed certificates of cmstest.NewECDSA and cmstest.NewRSA.
|
|
func named(s cmstest.Signer, cn string) vsigner { return vsigner{s, cn, cn} }
|
|
|
|
func (s vsigner) hash() [32]byte { return sha256.Sum256(s.Cert.Raw) }
|
|
|
|
// hashOfCert and hashOfIssuer are what §29.7 shows for a name that breaks its
|
|
// rules: the SHA-256 of the certificate, or of the DER of the Name of the
|
|
// issuer.
|
|
func hashOfCert(s cmstest.Signer) string { return hex32(sha256.Sum256(s.Cert.Raw)) }
|
|
func hashOfIssuer(s cmstest.Signer) string { return hex32(sha256.Sum256(s.Cert.RawIssuer)) }
|
|
|
|
// want is the result that §29.10 gives a signer: for a valid one, the
|
|
// authority of its seal, t, and whether its seal proves that it came before
|
|
// round_time, or why not (§29.7, §29.11).
|
|
type want struct {
|
|
s vsigner
|
|
result string
|
|
tsa vsigner
|
|
t time.Time
|
|
before bool
|
|
reason capsule.SealReason
|
|
}
|
|
|
|
// vcase is a case of security_cms.json with what spec v0.12 gives for it.
|
|
type vcase struct {
|
|
name string
|
|
area []byte
|
|
ctx *capsule.SecurityContext // nil: the common context
|
|
sig, seal capsule.Verdict
|
|
// signers are the required signers that have a SignerInfo, absent those
|
|
// that have none, and foreign the signers that are not required.
|
|
signers []want
|
|
absent []vsigner
|
|
foreign []want
|
|
// sealTSA and sealTime are the authority and t of a valid seal (S4, S5),
|
|
// sealReason the reason of S5, and authorKey the key of a valid signature
|
|
// of alg 1 (F4).
|
|
sealTSA vsigner
|
|
sealTime time.Time
|
|
sealReason capsule.SealReason
|
|
authorKey string
|
|
}
|
|
|
|
// cmsGen builds the cases over a common context and checks each against
|
|
// what the spec gives.
|
|
type cmsGen struct {
|
|
ctx *capsule.SecurityContext
|
|
file testkit.CMSVectorFile
|
|
errs []error
|
|
ana, luis, otro, tsa vsigner
|
|
}
|
|
|
|
func (g *cmsGen) fail(err error) {
|
|
if err != nil {
|
|
g.errs = append(g.errs, err)
|
|
}
|
|
}
|
|
|
|
func (g *cmsGen) must(b []byte, err error) []byte {
|
|
g.fail(err)
|
|
return b
|
|
}
|
|
|
|
// signersOf is SIGNERS for the required signers, canonical.
|
|
func (g *cmsGen) signersOf(required ...vsigner) []byte {
|
|
var hashes [][32]byte
|
|
for _, s := range required {
|
|
hashes = append(hashes, s.hash())
|
|
}
|
|
return g.must(capsule.EncodeSigners(hashes))
|
|
}
|
|
|
|
// messageOf is AUTHOR_MESSAGE for the SIGNERS list, in the common context.
|
|
func (g *cmsGen) messageOf(list []byte) []byte {
|
|
return capsule.AuthorMessage(g.ctx.ControlCommit, g.ctx.HeadDigest, capsule.SignersDigest(capsule.AlgCMS, list))
|
|
}
|
|
|
|
// content is the content of key 2 of a signature of alg 2 with the SIGNERS
|
|
// list and the CMS signature sig.
|
|
func (g *cmsGen) content(list, sig []byte) []byte {
|
|
return g.must(capsule.EncodeAuthorSignature(capsule.AlgCMS, list, sig))
|
|
}
|
|
|
|
// signed is the content of key 2 of a signature by the signers, with the
|
|
// options o, for the required signers.
|
|
func (g *cmsGen) signed(required []vsigner, o cmstest.Options, signers ...vsigner) []byte {
|
|
list := g.signersOf(required...)
|
|
return g.content(list, cmstest.Signature(g.messageOf(list), o, plain(signers)...))
|
|
}
|
|
|
|
func plain(ss []vsigner) []cmstest.Signer {
|
|
out := make([]cmstest.Signer, len(ss))
|
|
for i, s := range ss {
|
|
out[i] = s.Signer
|
|
}
|
|
return out
|
|
}
|
|
|
|
// area is SECURITY_CBOR with the contents of keys 2 and 3, nil when absent.
|
|
func (g *cmsGen) area(key2, key3 []byte) []byte {
|
|
return g.must(capsule.EncodeSecurityWith(key2, key3))
|
|
}
|
|
|
|
// sealedBy is the option of a CAdES-T: tsa seals each signature at when.
|
|
func sealedBy(tsa vsigner, when time.Time, o cmstest.TokenOptions) func([]byte) []byte {
|
|
return func(sig []byte) []byte { return cmstest.Token(sig, when, o, tsa.Signer) }
|
|
}
|
|
|
|
// add evaluates the case, checks it against what the spec gives, and writes
|
|
// its record.
|
|
func (g *cmsGen) add(c vcase) {
|
|
ctx := c.ctx
|
|
if ctx == nil {
|
|
ctx = g.ctx
|
|
}
|
|
v := capsule.EvaluateSecurityIn(c.area, ctx)
|
|
rec := testkit.CMSVectorCase{
|
|
Name: c.name, SecurityCBOR: hex.EncodeToString(c.area),
|
|
Context: testkit.CMSVectorContext{ControlCommit: hex32(ctx.ControlCommit), HeadDigest: hex32(ctx.HeadDigest), RoundTime: ctx.RoundTime.UTC().Format(time.RFC3339)},
|
|
Signature: string(v.Signature), Seal: string(v.Seal), Lines: v.Lines(),
|
|
}
|
|
if d := v.Detail; d != nil {
|
|
rec.Signers, rec.Foreign = cmsSignerResults(d.Signers), cmsSignerResults(d.Foreign)
|
|
if !d.SealTime.IsZero() {
|
|
rec.SealHolder, rec.SealTime = d.SealHolder, d.SealTime.UTC().Format(time.RFC3339Nano)
|
|
}
|
|
rec.SealReason = string(d.SealReason)
|
|
}
|
|
if err := c.check(v, rec); err != nil {
|
|
g.errs = append(g.errs, fmt.Errorf("%s: %w", c.name, err))
|
|
}
|
|
for _, other := range g.file.Cases {
|
|
if other.Name == c.name {
|
|
g.errs = append(g.errs, fmt.Errorf("%s: two cases of that name", c.name))
|
|
}
|
|
}
|
|
g.file.Cases = append(g.file.Cases, rec)
|
|
}
|
|
|
|
// cmsSignerResults are the results of the signers as the record writes them,
|
|
// t with its fraction when it has one.
|
|
func cmsSignerResults(lines []capsule.SignerLine) []testkit.FixtureSignerResult {
|
|
var out []testkit.FixtureSignerResult
|
|
for _, l := range lines {
|
|
r := testkit.FixtureSignerResult{Holder: l.Holder, Issuer: l.Issuer, Result: l.Result, Before: l.Before, SealReason: string(l.Reason)}
|
|
if !l.SealTime.IsZero() {
|
|
r.SealTime = l.SealTime.UTC().Format(time.RFC3339Nano)
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (w want) record() testkit.FixtureSignerResult {
|
|
r := testkit.FixtureSignerResult{Holder: w.s.holder, Issuer: w.s.issuer, Result: w.result}
|
|
if w.result == "valid" {
|
|
r.SealTime, r.Before = w.t.UTC().Format(time.RFC3339Nano), w.before
|
|
if !w.before {
|
|
r.SealReason = string(w.reason)
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
// check compares what the reader gave with what the spec gives: the
|
|
// verdicts, the result of each signer and the lines, written from the texts
|
|
// of §29.7.
|
|
func (c vcase) check(v capsule.Verdicts, rec testkit.CMSVectorCase) error {
|
|
if v.Signature != c.sig || v.Seal != c.seal {
|
|
return fmt.Errorf("verdicts %s and %s, want %s and %s", v.Signature, v.Seal, c.sig, c.seal)
|
|
}
|
|
// The required signers in the order of SIGNERS: of their hashes, in
|
|
// ascending order of bytes.
|
|
type req struct {
|
|
h [32]byte
|
|
w *want
|
|
s vsigner
|
|
}
|
|
var reqs []req
|
|
for i := range c.signers {
|
|
reqs = append(reqs, req{c.signers[i].s.hash(), &c.signers[i], c.signers[i].s})
|
|
}
|
|
for _, s := range c.absent {
|
|
reqs = append(reqs, req{s.hash(), nil, s})
|
|
}
|
|
slices.SortFunc(reqs, func(a, b req) int { return bytes.Compare(a.h[:], b.h[:]) })
|
|
var signers, foreign []testkit.FixtureSignerResult
|
|
for _, r := range reqs {
|
|
if r.w == nil {
|
|
signers = append(signers, testkit.FixtureSignerResult{Holder: hex32(r.h), Result: "absent"})
|
|
} else {
|
|
signers = append(signers, r.w.record())
|
|
}
|
|
}
|
|
for _, w := range c.foreign {
|
|
foreign = append(foreign, w.record())
|
|
}
|
|
if !reflect.DeepEqual(signers, rec.Signers) || !reflect.DeepEqual(foreign, rec.Foreign) {
|
|
return fmt.Errorf("signers %+v and foreign %+v, want %+v and %+v", rec.Signers, rec.Foreign, signers, foreign)
|
|
}
|
|
var sealHolder, sealTime string
|
|
if c.seal == capsule.VerdictSealed || c.seal == capsule.VerdictSealedLate {
|
|
sealHolder, sealTime = c.sealTSA.holder, c.sealTime.UTC().Format(time.RFC3339Nano)
|
|
}
|
|
if rec.SealHolder != sealHolder || rec.SealTime != sealTime {
|
|
return fmt.Errorf("the seal of %q at %s, want %q at %s", rec.SealHolder, rec.SealTime, sealHolder, sealTime)
|
|
}
|
|
if want := c.sealReason; c.seal != capsule.VerdictSealedLate && want != capsule.ReasonNone || rec.SealReason != string(want) {
|
|
return fmt.Errorf("the reason of the seal %q, want %q", rec.SealReason, want)
|
|
}
|
|
// The lines: the signature, the foreign signers apart, and the seal.
|
|
q := func(s string) string { return "«" + s + "»" }
|
|
at := func(t time.Time) string { return t.UTC().Format(time.RFC3339Nano) }
|
|
var lines []string
|
|
switch c.sig {
|
|
case capsule.VerdictNoSignature:
|
|
lines = append(lines, textF0)
|
|
case capsule.VerdictSignatureUnchecked:
|
|
lines = append(lines, textF1)
|
|
case capsule.VerdictSignatureInvalid:
|
|
lines = append(lines, textF2)
|
|
case capsule.VerdictSignedIncomplete:
|
|
lines = append(lines, textF5)
|
|
case capsule.VerdictSignedOther:
|
|
lines = append(lines, "Firmado con la clave "+c.authorKey+". No prueba quién la tiene.")
|
|
case capsule.VerdictSignedComplete:
|
|
var names, each []string
|
|
before := false
|
|
for _, r := range reqs {
|
|
names = append(names, q(r.s.holder))
|
|
when := "sin acreditar que fuera antes de la fecha de apertura: " + reasonTexts[r.w.reason]
|
|
if r.w.before {
|
|
when, before = "antes de la fecha de apertura", true
|
|
}
|
|
each = append(each, " "+q(r.s.holder)+" (emisor según su certificado: "+q(r.s.issuer)+"), sellado por "+q(r.w.tsa.holder)+" el "+at(r.w.t)+", "+when+".")
|
|
}
|
|
lines = append(lines, "Firmado con un certificado a nombre de "+strings.Join(names, ", ")+". DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.")
|
|
lines = append(lines, each...)
|
|
if before {
|
|
lines = append(lines, " DateKeys no comprueba quién emitió los sellos.")
|
|
}
|
|
default:
|
|
return fmt.Errorf("no lines for %s", c.sig)
|
|
}
|
|
for _, w := range c.foreign {
|
|
lines = append(lines, " Otro firmante, "+q(w.s.holder)+": "+resultTexts[w.result]+". No cuenta.")
|
|
}
|
|
switch c.seal {
|
|
case capsule.VerdictNoSeal:
|
|
case capsule.VerdictSealUnsupported:
|
|
lines = append(lines, textS1)
|
|
case capsule.VerdictSealUnreadable:
|
|
lines = append(lines, textS2)
|
|
case capsule.VerdictSealInvalid:
|
|
lines = append(lines, textS3)
|
|
case capsule.VerdictSealedLate:
|
|
lines = append(lines, textS5+reasonTexts[c.sealReason]+".")
|
|
case capsule.VerdictSealed:
|
|
lines = append(lines, "Según un sello a nombre de "+q(c.sealTSA.holder)+", existía el "+at(c.sealTime)+", antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.")
|
|
default:
|
|
return fmt.Errorf("no line for %s", c.seal)
|
|
}
|
|
if !slices.Equal(lines, rec.Lines) {
|
|
return fmt.Errorf("lines %q, want %q", rec.Lines, lines)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// securityCMSVectors makes the cases of security_cms.json: each one with the
|
|
// verdicts, the results and the lines that spec v0.12 gives, §29.7, §29.10
|
|
// and §29.11, and the list of §64 for the signature, the seal and the names.
|
|
// The generator fails when the reader gives anything else.
|
|
func securityCMSVectors() (any, error) {
|
|
g := &cmsGen{ctx: &capsule.SecurityContext{ControlCommit: sha256.Sum256([]byte("control")), HeadDigest: sha256.Sum256([]byte("head")), RoundTime: vecRound}}
|
|
g.file = testkit.CMSVectorFile{
|
|
Spec: cmsVectorSpec,
|
|
Description: "SECURITY_CBOR with an author signature of alg 2 or a time seal of seal_type 2, the context of its capsule, " +
|
|
"and the verdicts, the result of each signer and the lines of spec v0.16 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.",
|
|
}
|
|
g.signatureCases()
|
|
g.signersCases()
|
|
g.formCases()
|
|
g.algorithmCases()
|
|
g.nameCases()
|
|
g.sealCases()
|
|
if err := errors.Join(g.errs...); err != nil {
|
|
return nil, err
|
|
}
|
|
return g.file, nil
|
|
}
|
|
|
|
// people makes the signers of the cases once. The certificates of
|
|
// cmstest.NewECDSA and NewRSA are self-signed, so the issuer of each is its
|
|
// holder.
|
|
func (g *cmsGen) people() (ana, luis, otro, tsa vsigner) {
|
|
if g.ana.Key == nil {
|
|
g.ana = named(cmstest.NewECDSA("Ana López", elliptic.P256(), certFrom, certTo), "Ana López")
|
|
g.luis = named(cmstest.NewRSA("Luis Gómez", 2048, certFrom, certTo), "Luis Gómez")
|
|
g.otro = named(cmstest.NewECDSA("Otro", elliptic.P384(), certFrom, certTo), "Otro")
|
|
g.tsa = named(cmstest.NewECDSA("Autoridad de Sellado de prueba", elliptic.P256(), certFrom, certTo), "Autoridad de Sellado de prueba")
|
|
}
|
|
return g.ana, g.luis, g.otro, g.tsa
|
|
}
|
|
|
|
// valid is the result of a required signer that verifies, sealed by tsa at
|
|
// vecSigned with an accuracy of a second: before round_time.
|
|
func valid(s, tsa vsigner) want {
|
|
return want{s: s, result: "valid", tsa: tsa, t: vecSigned, before: true}
|
|
}
|
|
|
|
func result(s vsigner, r string) want { return want{s: s, result: r} }
|
|
|
|
// signatureCases are the verdicts of alg 2 (spec §29.10, "Verificación"):
|
|
// F6 when every required signer is valid and sealed, F5 when one is absent,
|
|
// not verifiable, without a seal, with an invalid seal or out of validity,
|
|
// or when key 3 exists, and F2 when one is invalid, before F5.
|
|
func (g *cmsGen) signatureCases() {
|
|
ana, luis, otro, tsa := g.people()
|
|
both := []vsigner{ana, luis}
|
|
sealed := cmstest.Options{Token: sealedBy(tsa, vecSigned, cmstest.TokenOptions{Accuracy: time.Second})}
|
|
only := func(s vsigner) []vsigner { return []vsigner{s} }
|
|
|
|
g.add(vcase{name: "alg 2: two signers, each sealed before the round time: F6", area: g.area(g.signed(both, sealed, ana, luis), nil),
|
|
sig: capsule.VerdictSignedComplete, seal: capsule.VerdictNoSeal, signers: []want{valid(ana, tsa), valid(luis, tsa)}})
|
|
late := cmstest.Options{Token: sealedBy(tsa, vecRound.Add(time.Hour), cmstest.TokenOptions{Accuracy: time.Second})}
|
|
g.add(vcase{name: "alg 2: sealed after the round time: F6, not before the opening date", area: g.area(g.signed(only(ana), late, ana), nil),
|
|
sig: capsule.VerdictSignedComplete, seal: capsule.VerdictNoSeal, signers: []want{{s: ana, result: "valid", tsa: tsa, t: vecRound.Add(time.Hour), reason: capsule.ReasonLate}}})
|
|
// t + accuracy equal to round_time is not before it (§29.7).
|
|
edge := cmstest.Options{Token: sealedBy(tsa, vecRound.Add(-time.Second), cmstest.TokenOptions{Accuracy: time.Second})}
|
|
g.add(vcase{name: "alg 2: t plus the accuracy of the seal equals the round time: F6, not before the opening date", area: g.area(g.signed(only(ana), edge, ana), nil),
|
|
sig: capsule.VerdictSignedComplete, seal: capsule.VerdictNoSeal, signers: []want{{s: ana, result: "valid", tsa: tsa, t: vecRound.Add(-time.Second), reason: capsule.ReasonLate}}})
|
|
// Spec v0.16, §29.7: a seal without accuracy does not prove that it came
|
|
// before the opening date, and its line gives the reason.
|
|
bare := cmstest.Options{Token: sealedBy(tsa, vecSigned, cmstest.TokenOptions{})}
|
|
g.add(vcase{name: "alg 2: a seal without accuracy: F6, not proven before the opening date", area: g.area(g.signed(only(ana), bare, ana), nil),
|
|
sig: capsule.VerdictSignedComplete, seal: capsule.VerdictNoSeal, signers: []want{{s: ana, result: "valid", tsa: tsa, t: vecSigned, reason: capsule.ReasonNoAccuracy}}})
|
|
btsp := cmstest.Options{Token: sealedBy(tsa, vecSigned, cmstest.TokenOptions{Policy: cmstest.BTSPPolicy})}
|
|
g.add(vcase{name: "alg 2: a seal of the BTSP policy without accuracy: F6, not proven before the opening date, by its policy", area: g.area(g.signed(only(ana), btsp, ana), nil),
|
|
sig: capsule.VerdictSignedComplete, seal: capsule.VerdictNoSeal, signers: []want{{s: ana, result: "valid", tsa: tsa, t: vecSigned, reason: capsule.ReasonNoAccuracyBTSP}}})
|
|
g.add(vcase{name: "alg 2: a signer who is not required shows apart and does not count: F6", area: g.area(g.signed(only(ana), sealed, ana, otro), nil),
|
|
sig: capsule.VerdictSignedComplete, seal: capsule.VerdictNoSeal, signers: []want{valid(ana, tsa)}, foreign: []want{valid(otro, tsa)}})
|
|
g.add(vcase{name: "alg 2: a required signer is absent: F5", area: g.area(g.signed(both, sealed, ana), nil),
|
|
sig: capsule.VerdictSignedIncomplete, seal: capsule.VerdictNoSeal, signers: []want{valid(ana, tsa)}, absent: []vsigner{luis}})
|
|
{
|
|
// §64: the author withdrawn and the others intact; a signature
|
|
// withdrawn and SIGNERS changed to hide it, which changes
|
|
// AUTHOR_MESSAGE, so the one who stays does not verify.
|
|
list := g.signersOf(both...)
|
|
sig := cmstest.Signature(g.messageOf(list), sealed, ana.Signer, luis.Signer)
|
|
g.add(vcase{name: "alg 2: the author withdrawn and the co-signer intact: F5", area: g.area(g.content(list, cmstest.Withdraw(sig, ana.Signer)), nil),
|
|
sig: capsule.VerdictSignedIncomplete, seal: capsule.VerdictNoSeal, signers: []want{valid(luis, tsa)}, absent: []vsigner{ana}})
|
|
g.add(vcase{name: "alg 2: a signature withdrawn and SIGNERS changed to hide it: F2", area: g.area(g.content(g.signersOf(ana), cmstest.Withdraw(sig, luis.Signer)), nil),
|
|
sig: capsule.VerdictSignatureInvalid, seal: capsule.VerdictNoSeal, signers: []want{result(ana, "invalid")}})
|
|
g.add(vcase{name: "alg 2: the CAdES-T of a signer withdrawn: F5, without seal", area: g.area(g.content(list, cmstest.WithoutTimeStamp(sig, luis.Signer)), nil),
|
|
sig: capsule.VerdictSignedIncomplete, seal: capsule.VerdictNoSeal, signers: []want{valid(ana, tsa), result(luis, "without seal")}})
|
|
}
|
|
g.add(vcase{name: "alg 2: no seal: F5", area: g.area(g.signed(only(ana), cmstest.Options{}, ana), nil),
|
|
sig: capsule.VerdictSignedIncomplete, seal: capsule.VerdictNoSeal, signers: []want{result(ana, "without seal")}})
|
|
// Step 6: a seal that verifies, at a time when the certificate of the
|
|
// signer is no longer valid, and the authority still is.
|
|
expired := named(cmstest.NewECDSA("Ana caducada", elliptic.P256(), certFrom, time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)), "Ana caducada")
|
|
g.add(vcase{name: "alg 2: the certificate of the signer out of validity, its authority valid: F5, out of validity", area: g.area(g.signed(only(expired), sealed, expired), nil),
|
|
sig: capsule.VerdictSignedIncomplete, seal: capsule.VerdictNoSeal, signers: []want{result(expired, "out of validity")}})
|
|
// The validity is inclusive (RFC 5280 4.1.2.5): sealed at the last second.
|
|
lastDay := named(cmstest.NewECDSA("Eva Martín", elliptic.P256(), certFrom, vecSigned), "Eva Martín")
|
|
g.add(vcase{name: "alg 2: sealed at the last second of the validity of the certificate: F6", area: g.area(g.signed(only(lastDay), sealed, lastDay), nil),
|
|
sig: capsule.VerdictSignedComplete, seal: capsule.VerdictNoSeal, signers: []want{valid(lastDay, tsa)}})
|
|
// Step 5: a token of the profile of §29.11 that gives S3, S2 or S1 is an
|
|
// invalid seal.
|
|
for _, tc := range []struct {
|
|
name string
|
|
o cmstest.Options
|
|
}{
|
|
{"alg 2: a seal whose authority was not valid at its time: F5, invalid seal", cmstest.Options{Token: sealedBy(tsa, certFrom.AddDate(-1, 0, 0), cmstest.TokenOptions{})}},
|
|
{"alg 2: a seal over another signature value: F5, invalid seal", cmstest.Options{Token: func([]byte) []byte {
|
|
return cmstest.Token([]byte("other"), vecSigned, cmstest.TokenOptions{}, tsa.Signer)
|
|
}}},
|
|
{"alg 2: a seal of a TSTInfo of version 2: F5, invalid seal", cmstest.Options{Token: sealedBy(tsa, vecSigned, cmstest.TokenOptions{Version: 2})}},
|
|
{"alg 2: a seal by an authority with a key of 1024 bits: F5, invalid seal", cmstest.Options{Token: sealedBy(named(cmstest.NewRSA("TSA de 1024 bits", 1024, certFrom, certTo), "TSA de 1024 bits"), vecSigned, cmstest.TokenOptions{})}},
|
|
} {
|
|
g.add(vcase{name: tc.name, area: g.area(g.signed(only(ana), tc.o, ana), nil),
|
|
sig: capsule.VerdictSignedIncomplete, seal: capsule.VerdictNoSeal, signers: []want{result(ana, "invalid seal")}})
|
|
}
|
|
// Within alg 2 the imprint takes any hash of the table (§29.11 step 2).
|
|
g.add(vcase{name: "alg 2: a seal with an imprint of SHA-384: F6", area: g.area(g.signed(only(ana), cmstest.Options{Token: sealedBy(tsa, vecSigned, cmstest.TokenOptions{Hash: crypto.SHA384, Accuracy: time.Second})}, ana), nil),
|
|
sig: capsule.VerdictSignedComplete, seal: capsule.VerdictNoSeal, signers: []want{valid(ana, tsa)}})
|
|
// Rule 1: the token is inside the ContentInfo, which is DER in all of it.
|
|
g.add(vcase{name: "alg 2: a seal in BER makes the signature not DER: F1", area: g.area(g.signed(only(ana), cmstest.Options{Token: sealedBy(tsa, vecSigned, cmstest.TokenOptions{CMS: cmstest.Options{BER: true}})}, ana), nil),
|
|
sig: capsule.VerdictSignatureUnchecked, seal: capsule.VerdictNoSeal})
|
|
// Key 3 beside alg 2: F5, and the seal is evaluated apart (§29.3, §29.7).
|
|
key2 := g.signed(only(ana), sealed, ana)
|
|
g.add(vcase{name: "alg 2: a key 3 of seal_type 4294967295 beside it: F5 and S1", area: g.area(key2, g.must(capsule.EncodeSeal(capsule.SealTypeTest, []byte{1}))),
|
|
sig: capsule.VerdictSignedIncomplete, seal: capsule.VerdictSealUnsupported, signers: []want{valid(ana, tsa)}})
|
|
subject := capsule.SealSubject(g.ctx.ControlCommit, g.ctx.HeadDigest, capsule.SigPart(key2))
|
|
g.add(vcase{name: "alg 2: a valid seal of seal_type 2 in key 3 beside it: F5 and S4", area: g.area(key2, g.must(capsule.EncodeSeal(capsule.SealTypeRFC3161, cmstest.Token(subject[:], vecSigned, cmstest.TokenOptions{Accuracy: time.Second}, tsa.Signer)))),
|
|
sig: capsule.VerdictSignedIncomplete, seal: capsule.VerdictSealed, signers: []want{valid(ana, tsa)}, sealTSA: tsa, sealTime: vecSigned})
|
|
other := *g.ctx
|
|
other.HeadDigest[5] ^= 9
|
|
g.add(vcase{name: "alg 2: in the context of another head: F2", area: g.area(g.signed(only(ana), sealed, ana), nil), ctx: &other,
|
|
sig: capsule.VerdictSignatureInvalid, seal: capsule.VerdictNoSeal, signers: []want{result(ana, "invalid")}})
|
|
g.add(vcase{name: "alg 2: a message-digest of another message: F2", area: g.area(g.signed(only(ana), cmstest.Options{Token: sealed.Token, Message: []byte("another message")}, ana), nil),
|
|
sig: capsule.VerdictSignatureInvalid, seal: capsule.VerdictNoSeal, signers: []want{result(ana, "invalid")}})
|
|
// F2 before F5: one invalid and one absent.
|
|
g.add(vcase{name: "alg 2: one signer invalid and another absent: F2", area: g.area(g.signed(both, cmstest.Options{Token: sealed.Token, Message: []byte("another message")}, ana), nil),
|
|
sig: capsule.VerdictSignatureInvalid, seal: capsule.VerdictNoSeal, signers: []want{result(ana, "invalid")}, absent: []vsigner{luis}})
|
|
g.add(vcase{name: "alg 2: not a CMS: F1", area: g.area(g.content(g.signersOf(ana), []byte("not DER")), nil),
|
|
sig: capsule.VerdictSignatureUnchecked, seal: capsule.VerdictNoSeal})
|
|
}
|
|
|
|
// signersCases are SIGNERS that break its profile (spec §29.10, "Firmantes
|
|
// exigidos"), each beside a CMS signature that is valid for the
|
|
// AUTHOR_MESSAGE of those very SIGNERS: F1 comes from the rule, and a reader
|
|
// that skipped it would give F5 or F6.
|
|
func (g *cmsGen) signersCases() {
|
|
ana, luis, _, tsa := g.people()
|
|
sealed := cmstest.Options{Token: sealedBy(tsa, vecSigned, cmstest.TokenOptions{Accuracy: time.Second})}
|
|
bstr := func(h []byte) []byte { return append([]byte{0x58, byte(len(h))}, h...) }
|
|
array := func(n int, items ...[]byte) []byte { return append([]byte{0x80 | byte(n)}, bytes.Join(items, nil)...) }
|
|
a, l := ana.hash(), luis.hash()
|
|
if bytes.Compare(a[:], l[:]) > 0 {
|
|
a, l = l, a
|
|
}
|
|
cosigned := func(name string, list []byte, signers ...vsigner) {
|
|
sig := cmstest.Signature(g.messageOf(list), sealed, plain(signers)...)
|
|
g.add(vcase{name: name, area: g.area(g.content(list, sig), nil), sig: capsule.VerdictSignatureUnchecked, seal: capsule.VerdictNoSeal})
|
|
}
|
|
cosigned("alg 2: SIGNERS out of order, beside a valid CMS: F1", array(2, bstr(l[:]), bstr(a[:])), ana, luis)
|
|
cosigned("alg 2: SIGNERS empty, beside a valid CMS: F1", array(0), ana)
|
|
ah := ana.hash()
|
|
cosigned("alg 2: SIGNERS with an element of 31 bytes, beside a valid CMS: F1", array(1, bstr(ah[:31])), ana)
|
|
cosigned("alg 2: SIGNERS with a certificate twice, beside a valid CMS: F1", array(2, bstr(a[:]), bstr(a[:])), ana, luis)
|
|
var many []vsigner
|
|
for i := range 17 {
|
|
name := fmt.Sprintf("Firmante %02d", i+1)
|
|
many = append(many, named(cmstest.NewECDSA(name, elliptic.P256(), certFrom, certTo), name))
|
|
}
|
|
// 16, the most: F6. 17, beside a valid CMS of the 17: F1.
|
|
var wants []want
|
|
for _, s := range many[:16] {
|
|
wants = append(wants, valid(s, tsa))
|
|
}
|
|
g.add(vcase{name: "alg 2: SIGNERS of 16 entries, the most, each signer sealed: F6", area: g.area(g.signed(many[:16], sealed, many[:16]...), nil),
|
|
sig: capsule.VerdictSignedComplete, seal: capsule.VerdictNoSeal, signers: wants})
|
|
hashes := make([][]byte, 17)
|
|
for i, s := range many {
|
|
h := s.hash()
|
|
hashes[i] = h[:]
|
|
}
|
|
slices.SortFunc(hashes, bytes.Compare)
|
|
var items [][]byte
|
|
for _, h := range hashes {
|
|
items = append(items, bstr(h))
|
|
}
|
|
cosigned("alg 2: SIGNERS of 17 entries, beside a valid CMS of the 17: F1", append([]byte{0x91}, bytes.Join(items, nil)...), many...)
|
|
}
|
|
|
|
// formCases break the form of the CMS signature (spec §29.10, rules 1 to 4
|
|
// and the rules after them): F1.
|
|
func (g *cmsGen) formCases() {
|
|
ana, luis, _, tsa := g.people()
|
|
tok := sealedBy(tsa, vecSigned, cmstest.TokenOptions{Accuracy: time.Second})
|
|
only := []vsigner{ana}
|
|
unchecked := func(name string, required []vsigner, o cmstest.Options, signers ...vsigner) {
|
|
o.Token = tok
|
|
g.add(vcase{name: name, area: g.area(g.signed(required, o, signers...), nil), sig: capsule.VerdictSignatureUnchecked, seal: capsule.VerdictNoSeal})
|
|
}
|
|
unchecked("alg 2: the signature in BER: F1", only, cmstest.Options{BER: true}, ana)
|
|
unchecked("alg 2: signerInfos out of order: F1", []vsigner{ana, luis}, cmstest.Options{Unsorted: true}, ana, luis)
|
|
unchecked("alg 2: two SignerInfo of one certificate: F1", only, cmstest.Options{}, ana, ana)
|
|
unchecked("alg 2: the same SignerInfo twice: F1", only, cmstest.Options{SignerInfoTwice: true}, ana)
|
|
unchecked("alg 2: a SignerInfo of version 3 with issuerAndSerialNumber: F1", only, cmstest.Options{Version: 3}, ana)
|
|
unchecked("alg 2: a SignerInfo of version 1 with subjectKeyIdentifier: F1", only, cmstest.Options{Version: 1, SKI: true}, ana)
|
|
unchecked("alg 2: two content-type attributes: F1", only, cmstest.Options{ContentType2: true}, ana)
|
|
unchecked("alg 2: a second content-type with an empty set of values: F1", only, cmstest.Options{ExtraAttrs: [][]byte{cmstest.Seq(cmstest.OID(cmstest.OIDContentType), cmstest.Set(0x31))}}, ana)
|
|
unchecked("alg 2: an ESSCertIDv2 of SHA-1: F1", only, cmstest.Options{ESSHashAlg: cmstest.HashAlg(crypto.SHA1)}, ana)
|
|
unchecked("alg 2: an ESSCertIDv2 with the hash of another certificate: F1", only, cmstest.Options{ESSCert: luis.Cert.Raw}, ana)
|
|
unchecked("alg 2: only a signing-certificate, without the v2: F1", only, cmstest.Options{NoSigCertV2: true, SigCertV1: true}, ana)
|
|
unchecked("alg 2: two signature-time-stamp attributes: F1", only, cmstest.Options{TimeStamps2: true}, ana)
|
|
unchecked("alg 2: a CRL in crls: F1", only, cmstest.Options{CRLs: [][]byte{cmstest.Seq(cmstest.Int(1))}}, ana)
|
|
unchecked("alg 2: no certificate of the signer: F1", only, cmstest.Options{OmitCert: true}, ana)
|
|
// A certificate that breaks the profile names no signer: rule 3.
|
|
v1 := vsigner{Signer: cmstest.NewCert(cmstest.CertSpec{CN: "Ana v1", NoVersion: true}, cmstest.ECKey(elliptic.P256()))}
|
|
unchecked("alg 2: the certificate of the signer of version 1: F1", []vsigner{v1}, cmstest.Options{}, v1)
|
|
frac := vsigner{Signer: cmstest.NewCert(cmstest.CertSpec{CN: "Ana", NotAfter: cmstest.GeneralizedTime("20391231235959.5Z")}, cmstest.ECKey(elliptic.P256()))}
|
|
unchecked("alg 2: the certificate of the signer valid until a fraction of a second: F1", []vsigner{frac}, cmstest.Options{}, frac)
|
|
twice := vsigner{Signer: cmstest.NewCert(cmstest.CertSpec{CN: "Ana", Extensions: [][]byte{cmstest.ExtSKI([]byte{1}), cmstest.ExtKeyUsage(), cmstest.ExtKeyUsage()}}, cmstest.ECKey(elliptic.P256()))}
|
|
unchecked("alg 2: the certificate of the signer with an extension twice: F1", []vsigner{twice}, cmstest.Options{}, twice)
|
|
}
|
|
|
|
// algorithmCases are the table of algorithms and keys (spec §29.10,
|
|
// "Algoritmos" and step 2), and what decides nothing.
|
|
func (g *cmsGen) algorithmCases() {
|
|
ana, luis, otro, tsa := g.people()
|
|
tok := sealedBy(tsa, vecSigned, cmstest.TokenOptions{Accuracy: time.Second})
|
|
complete := func(name string, s vsigner, o cmstest.Options) {
|
|
o.Token = tok
|
|
g.add(vcase{name: name, area: g.area(g.signed([]vsigner{s}, o, s), nil), sig: capsule.VerdictSignedComplete, seal: capsule.VerdictNoSeal, signers: []want{valid(s, tsa)}})
|
|
}
|
|
incomplete := func(name string, s vsigner, o cmstest.Options, r string) {
|
|
o.Token = tok
|
|
g.add(vcase{name: name, area: g.area(g.signed([]vsigner{s}, o, s), nil), sig: capsule.VerdictSignedIncomplete, seal: capsule.VerdictNoSeal, signers: []want{result(s, r)}})
|
|
}
|
|
complete("alg 2: ECDSA P-256 with SHA-384: F6", ana, cmstest.Options{Hash: crypto.SHA384})
|
|
complete("alg 2: ECDSA P-256 with SHA-512: F6", ana, cmstest.Options{Hash: crypto.SHA512})
|
|
complete("alg 2: ECDSA P-384 with SHA-384: F6", otro, cmstest.Options{Hash: crypto.SHA384})
|
|
p521 := named(cmstest.NewECDSA("Raúl Sanz", elliptic.P521(), certFrom, certTo), "Raúl Sanz")
|
|
complete("alg 2: ECDSA P-521 with SHA-512: F6", p521, cmstest.Options{Hash: crypto.SHA512})
|
|
complete("alg 2: RSA of 2048 bits, sha256WithRSAEncryption: F6", luis, cmstest.Options{SigAlg: cmstest.AlgID(cmstest.OIDSHA256RSA, cmstest.Null())})
|
|
complete("alg 2: RSA of 3072 bits: F6", named(cmstest.NewRSA("Sara Gil", 3072, certFrom, certTo), "Sara Gil"), cmstest.Options{})
|
|
complete("alg 2: RSA of 4096 bits with SHA-512: F6", named(cmstest.NewRSA("Pablo Ruiz", 4096, certFrom, certTo), "Pablo Ruiz"), cmstest.Options{Hash: crypto.SHA512})
|
|
complete("alg 2: RSASSA-PSS: F6", luis, cmstest.Options{PSS: true})
|
|
complete("alg 2: the sid by subjectKeyIdentifier: F6", ana, cmstest.Options{SKI: true})
|
|
complete("alg 2: a signing-certificate beside the v2: F6", ana, cmstest.Options{SigCertV1: true})
|
|
complete("alg 2: an ESSCertIDv2 with SHA-256 written: F6", ana, cmstest.Options{ESSHashAlg: cmstest.HashAlg(crypto.SHA256)})
|
|
complete("alg 2: an unknown attribute with an arc of 2^31: F6", ana, cmstest.Options{ExtraAttrs: [][]byte{cmstest.BigArcAttr()}})
|
|
complete("alg 2: the certificate of the signer twice in certificates: F6", ana, cmstest.Options{ExtraCerts: [][]byte{ana.Cert.Raw}})
|
|
v1 := cmstest.NewCert(cmstest.CertSpec{CN: "Intermedia de versión 1", NoVersion: true}, cmstest.ECKey(elliptic.P256()))
|
|
complete("alg 2: a certificate of version 1 that names no signer: F6", ana, cmstest.Options{ExtraCerts: [][]byte{v1.Cert.Raw}})
|
|
complete("alg 2: an attribute certificate in certificates: F6", ana, cmstest.Options{ExtraCerts: [][]byte{cmstest.TLV(0xa1, cmstest.Seq(cmstest.Int(1)))}})
|
|
complete("alg 2: an OCSP response in crls: F6", ana, cmstest.Options{OCSP: cmstest.Seq(cmstest.Int(0))})
|
|
garbage := cmstest.NewCert(cmstest.CertSpec{CN: "Ana López", Signature: cmstest.BitString([]byte("not a signature"))}, cmstest.ECKey(elliptic.P256()))
|
|
complete("alg 2: a certificate whose own signature is not one: F6", vsigner{garbage, "Ana López", "Ana López"}, cmstest.Options{})
|
|
numeric := cmstest.NewCert(cmstest.CertSpec{Subject: cmstest.Name(cmstest.ATV(cmstest.OIDCommonName, cmstest.UTF8("Ivan Petrov")),
|
|
cmstest.ATV([]int{1, 2, 643, 3, 131, 1, 1}, cmstest.Numeric("123456789012")))}, cmstest.ECKey(elliptic.P256()))
|
|
complete("alg 2: a NumericString in the subject, which is DER: F6", vsigner{numeric, "Ivan Petrov", "Ivan Petrov"}, cmstest.Options{})
|
|
|
|
// Step 2: outside the table, not verifiable.
|
|
incomplete("alg 2: RSASSA-PSS with trailerField written: F5, not verifiable", luis, cmstest.Options{PSS: true, PSSTrailer: true}, "not verifiable")
|
|
incomplete("alg 2: a digest outside the table, SHA-1: F5, not verifiable", ana, cmstest.Options{Hash: crypto.SHA1}, "not verifiable")
|
|
incomplete("alg 2: RSA of 1024 bits: F5, not verifiable", named(cmstest.NewRSA("Clave corta", 1024, certFrom, certTo), "Clave corta"), cmstest.Options{}, "not verifiable")
|
|
rsaKey := cmstest.RSAKey(2048)
|
|
even := cmstest.NewCert(cmstest.CertSpec{CN: "Módulo par", SPKI: cmstest.SPKIRSA(new(big.Int).Add(rsaKey.N, big.NewInt(1)), big.NewInt(int64(rsaKey.E)))}, rsaKey)
|
|
incomplete("alg 2: RSA with an even modulus: F5, not verifiable", vsigner{even, "Módulo par", "Módulo par"}, cmstest.Options{}, "not verifiable")
|
|
ecKey := cmstest.ECKey(elliptic.P256())
|
|
compressed := cmstest.NewCert(cmstest.CertSpec{CN: "Punto comprimido", SPKI: cmstest.SPKICompressed(&ecKey.PublicKey)}, ecKey)
|
|
incomplete("alg 2: an EC key compressed: F5, not verifiable", vsigner{compressed, "Punto comprimido", "Punto comprimido"}, cmstest.Options{}, "not verifiable")
|
|
brainpool := cmstest.NewCert(cmstest.CertSpec{CN: "Curva brainpool", SPKI: cmstest.SPKIEC(cmstest.OID(cmstest.OIDBrainpoolP256), cmstest.Uncompressed(&ecKey.PublicKey))}, ecKey)
|
|
incomplete("alg 2: a key on brainpoolP256r1: F5, not verifiable", vsigner{brainpool, "Curva brainpool", "Curva brainpool"}, cmstest.Options{}, "not verifiable")
|
|
// Step 3: a key of another scheme than its algorithm is invalid.
|
|
g.add(vcase{name: "alg 2: an RSA key with an ECDSA algorithm: F2", area: g.area(g.signed([]vsigner{luis}, cmstest.Options{Token: tok, SigAlg: cmstest.AlgID(cmstest.OIDECDSA256)}, luis), nil),
|
|
sig: capsule.VerdictSignatureInvalid, seal: capsule.VerdictNoSeal, signers: []want{result(luis, "invalid")}})
|
|
}
|
|
|
|
// nameCases are the names of a certificate as spec §29.7 shows them: a
|
|
// holder from givenName and surname, or from commonName, with the rules of
|
|
// the declared author, at most 64 code points and no two spaces in a row, and
|
|
// the text of §29.10; otherwise the SHA-256 of the certificate. The issuer:
|
|
// its commonName, or its organizationName without one, with the same rules;
|
|
// otherwise the SHA-256 of its Name.
|
|
func (g *cmsGen) nameCases() {
|
|
_, _, _, tsa := g.people()
|
|
tok := sealedBy(tsa, vecSigned, cmstest.TokenOptions{Accuracy: time.Second})
|
|
ca := cmstest.Name(cmstest.ATV(cmstest.OIDCommonName, cmstest.UTF8("CA de prueba")))
|
|
cn := func(v []byte) []byte { return cmstest.ATV(cmstest.OIDCommonName, v) }
|
|
utf8 := cmstest.UTF8
|
|
holder := func(name string, subject []byte, shownAs string) {
|
|
s := cmstest.NewCert(cmstest.CertSpec{Subject: subject, Issuer: ca}, cmstest.ECKey(elliptic.P256()))
|
|
if shownAs == "" {
|
|
shownAs = hashOfCert(s)
|
|
}
|
|
vs := vsigner{s, shownAs, "CA de prueba"}
|
|
g.add(vcase{name: name, area: g.area(g.signed([]vsigner{vs}, cmstest.Options{Token: tok}, vs), nil),
|
|
sig: capsule.VerdictSignedComplete, seal: capsule.VerdictNoSeal, signers: []want{valid(vs, tsa)}})
|
|
}
|
|
sixtyFour := strings.Repeat("ñ", 64)
|
|
holder("names: a commonName of 64 code points, shown", cmstest.Name(cn(utf8(sixtyFour))), sixtyFour)
|
|
holder("names: a commonName of 65 code points, its SHA-256", cmstest.Name(cn(utf8(sixtyFour+"a"))), "")
|
|
holder("names: two spaces in a row, its SHA-256", cmstest.Name(cn(utf8("Ana López"))), "")
|
|
holder("names: an escape, its SHA-256", cmstest.Name(cn(utf8("Ana\x1b[2JLópez"))), "")
|
|
holder("names: U+202E, its SHA-256", cmstest.Name(cn(utf8("Ana \xe2\x80\xaezepóL"))), "")
|
|
holder("names: a byte order mark, its SHA-256", cmstest.Name(cn(utf8("\xef\xbb\xbfAna López"))), "")
|
|
holder("names: a line feed, its SHA-256", cmstest.Name(cn(utf8("Ana\nLópez"))), "")
|
|
holder("names: givenName, surname and a commonName with the NIF, the name without the NIF", cmstest.Name(
|
|
cmstest.ATV(cmstest.OIDCountry, cmstest.Printable("ES")), cmstest.ATV(cmstest.OIDSerialNumber, cmstest.Printable("IDCES-12345678Z")),
|
|
cmstest.ATV(cmstest.OIDSurname, utf8("ESPAÑOL ESPAÑOL")), cmstest.ATV(cmstest.OIDGivenName, utf8("JUAN")),
|
|
cn(utf8("ESPAÑOL ESPAÑOL JUAN - 12345678Z"))), "JUAN ESPAÑOL ESPAÑOL")
|
|
holder("names: a commonName in a VisibleString, no text: its SHA-256", cmstest.Name(cn(cmstest.Visible("Ana Lopez"))), "")
|
|
holder("names: a PrintableString, shown", cmstest.Name(cn(cmstest.Printable("Ana Lopez"))), "Ana Lopez")
|
|
holder("names: a PrintableString with an underscore, no text: its SHA-256", cmstest.Name(cn(cmstest.Printable("Ana_Lopez"))), "")
|
|
holder("names: a PrintableString with an at sign, no text: its SHA-256", cmstest.Name(cn(cmstest.Printable("ana@example.com"))), "")
|
|
holder("names: a BMPString, shown", cmstest.Name(cn(cmstest.BMPText("Ana López"))), "Ana López")
|
|
holder("names: a BMPString of odd length, no text: its SHA-256", cmstest.Name(cn(cmstest.BMP(append(cmstest.BMPText("Ana")[2:], 0)))), "")
|
|
holder("names: a BMPString with a surrogate, no text: its SHA-256", cmstest.Name(cn(cmstest.BMPText("Ana \xf0\x9f\x98\x80"))), "")
|
|
holder("names: a TeletexString in ASCII, shown", cmstest.Name(cn(cmstest.Teletex("Ana Lopez"))), "Ana Lopez")
|
|
holder("names: a TeletexString with a byte of 0xE9, no text: its SHA-256", cmstest.Name(cn(cmstest.Teletex("Ana L\xe9a"))), "")
|
|
holder("names: an IA5String, shown", cmstest.Name(cn(cmstest.IA5("ana.lopez@example.com"))), "ana.lopez@example.com")
|
|
holder("names: a UTF8String that is not UTF-8, no text: its SHA-256", cmstest.Name(cn(utf8("Ana L\xf3pez"))), "")
|
|
holder("names: two commonNames, no text: its SHA-256", cmstest.Name(cn(utf8("Ana López")), cn(utf8("Luis Gómez"))), "")
|
|
|
|
issuer := func(name string, issuerName []byte, shownAs string) {
|
|
s := cmstest.NewCert(cmstest.CertSpec{CN: "Ana López", Issuer: issuerName}, cmstest.ECKey(elliptic.P256()))
|
|
if shownAs == "" {
|
|
shownAs = hashOfIssuer(s)
|
|
}
|
|
vs := vsigner{s, "Ana López", shownAs}
|
|
g.add(vcase{name: name, area: g.area(g.signed([]vsigner{vs}, cmstest.Options{Token: tok}, vs), nil),
|
|
sig: capsule.VerdictSignedComplete, seal: capsule.VerdictNoSeal, signers: []want{valid(vs, tsa)}})
|
|
}
|
|
issuer("names: an issuer without a commonName, its organizationName", cmstest.Name(cmstest.ATV(cmstest.OIDCountry, cmstest.Printable("ES")),
|
|
cmstest.ATV(cmstest.OIDOrganization, utf8("Banco de Pruebas S.A."))), "Banco de Pruebas S.A.")
|
|
issuer("names: an issuer without text, the SHA-256 of its name", cmstest.Name(cmstest.ATV(cmstest.OIDCountry, cmstest.Printable("ES")),
|
|
cmstest.ATV(cmstest.OIDOrgUnit, utf8("Unidad de pruebas"))), "")
|
|
issuer("names: an issuer whose commonName has an escape, the SHA-256 of its name and not its organizationName", cmstest.Name(
|
|
cmstest.ATV(cmstest.OIDOrganization, utf8("Banco de Pruebas S.A.")), cn(utf8("CA\x1b[2J de prueba"))), "")
|
|
}
|
|
|
|
// sealCases are the seals of seal_type 2 (spec §29.11), over an alg 1
|
|
// signature, which gives F4, unless a case says otherwise.
|
|
func (g *cmsGen) sealCases() {
|
|
_, _, _, tsa := g.people()
|
|
key, err := authorkey.NewFromSeed(bytes.Repeat([]byte{7}, 32))
|
|
if err != nil {
|
|
g.fail(err)
|
|
return
|
|
}
|
|
pub, err := authorkey.PublicString(key.Public())
|
|
g.fail(err)
|
|
msg := capsule.AuthorMessage(g.ctx.ControlCommit, g.ctx.HeadDigest, capsule.SignersDigest(capsule.AlgEd25519, nil))
|
|
sig := g.must(capsule.EncodeAuthorSignature(capsule.AlgEd25519, key.Public(), key.Sign(msg)))
|
|
subject := capsule.SealSubject(g.ctx.ControlCommit, g.ctx.HeadDigest, capsule.SigPart(sig))
|
|
tok := func(when time.Time, o cmstest.TokenOptions, s vsigner) []byte {
|
|
return cmstest.Token(subject[:], when, o, s.Signer)
|
|
}
|
|
sealArea := func(token []byte) []byte {
|
|
return g.area(sig, g.must(capsule.EncodeSeal(capsule.SealTypeRFC3161, token)))
|
|
}
|
|
seal := func(name string, token []byte, verdict capsule.Verdict) {
|
|
g.add(vcase{name: name, area: sealArea(token), sig: capsule.VerdictSignedOther, seal: verdict, authorKey: pub})
|
|
}
|
|
sealedAt := func(name string, token []byte, s vsigner, t time.Time, verdict capsule.Verdict) {
|
|
g.add(vcase{name: name, area: sealArea(token), sig: capsule.VerdictSignedOther, seal: verdict, authorKey: pub, sealTSA: s, sealTime: t})
|
|
}
|
|
late := func(name string, token []byte, s vsigner, t time.Time, reason capsule.SealReason) {
|
|
g.add(vcase{name: name, area: sealArea(token), sig: capsule.VerdictSignedOther, seal: capsule.VerdictSealedLate, authorKey: pub, sealTSA: s, sealTime: t, sealReason: reason})
|
|
}
|
|
// The tokens of the cases about something else carry an accuracy of a
|
|
// second: without it, a valid seal proves nothing before the round time
|
|
// (spec v0.16, §29.11).
|
|
second := cmstest.TokenOptions{Accuracy: time.Second}
|
|
sealedAt("seal: before the round time: S4", tok(vecSigned, second, tsa), tsa, vecSigned, capsule.VerdictSealed)
|
|
late("seal: after the round time, without accuracy: S5, sealed after", tok(vecRound.Add(time.Minute), cmstest.TokenOptions{}, tsa), tsa, vecRound.Add(time.Minute), capsule.ReasonLate)
|
|
early := vecRound.Add(-time.Second)
|
|
late("seal: t plus the accuracy past the round time: S5", tok(early, cmstest.TokenOptions{Accuracy: 2 * time.Second}, tsa), tsa, early, capsule.ReasonLate)
|
|
late("seal: t plus the accuracy equal to the round time: S5", tok(early, cmstest.TokenOptions{Accuracy: time.Second}, tsa), tsa, early, capsule.ReasonLate)
|
|
late("seal: without accuracy, years before the round time: S5, it does not say its precision", tok(vecSigned, cmstest.TokenOptions{}, tsa), tsa, vecSigned, capsule.ReasonNoAccuracy)
|
|
late("seal: of the BTSP policy of ETSI, without accuracy: S5, it does not say the precision its policy requires", tok(vecSigned, cmstest.TokenOptions{Policy: cmstest.BTSPPolicy}, tsa), tsa, vecSigned, capsule.ReasonNoAccuracyBTSP)
|
|
late("seal: of the BTSP policy, without accuracy, after the round time: S5, sealed after", tok(vecRound, cmstest.TokenOptions{Policy: cmstest.BTSPPolicy}, tsa), tsa, vecRound, capsule.ReasonLate)
|
|
sealedAt("seal: of the BTSP policy, with accuracy: S4", tok(vecSigned, cmstest.TokenOptions{Policy: cmstest.BTSPPolicy, Accuracy: time.Second}, tsa), tsa, vecSigned, capsule.VerdictSealed)
|
|
sealedAt("seal: an accuracy of 0 seconds, a microsecond before the round time: S4", tok(vecRound.Add(-time.Microsecond), cmstest.TokenOptions{AccuracyRaw: cmstest.Seq(cmstest.Int(0))}, tsa), tsa, vecRound.Add(-time.Microsecond), capsule.VerdictSealed)
|
|
sealedAt("seal: an empty accuracy, a precision of 0: S4", tok(vecSigned, cmstest.TokenOptions{AccuracyRaw: cmstest.Seq()}, tsa), tsa, vecSigned, capsule.VerdictSealed)
|
|
sealedAt("seal: t plus an accuracy of 999 ms and 999 µs, a microsecond before the round time: S4",
|
|
tok(early, cmstest.TokenOptions{Accuracy: 999*time.Millisecond + 999*time.Microsecond}, tsa), tsa, early, capsule.VerdictSealed)
|
|
sealedAt("seal: an accuracy of seconds, millis and micros: S4", tok(vecSigned, cmstest.TokenOptions{Accuracy: time.Second + 5*time.Millisecond + 7*time.Microsecond}, tsa), tsa, vecSigned, capsule.VerdictSealed)
|
|
fraction := vecSigned.Add(250 * time.Millisecond)
|
|
sealedAt("seal: a genTime with a fraction of a second: S4, t with its fraction", tok(fraction, second, tsa), tsa, fraction, capsule.VerdictSealed)
|
|
sealedAt("seal: the certificate of the authority twice: S4", tok(vecSigned, cmstest.TokenOptions{TSATwice: true, Accuracy: time.Second}, tsa), tsa, vecSigned, capsule.VerdictSealed)
|
|
sealedAt("seal: a CRL in the token decides nothing: S4", tok(vecSigned, cmstest.TokenOptions{CRL: cmstest.Seq(cmstest.Seq(cmstest.Int(1)), cmstest.Seq(cmstest.OID(cmstest.OIDECDSA256)), cmstest.BitString([]byte{0})), Accuracy: time.Second}, tsa), tsa, vecSigned, capsule.VerdictSealed)
|
|
sealedAt("seal: signing-certificate-v2 in the token: S4", tok(vecSigned, cmstest.TokenOptions{SigCertV2: true, Accuracy: time.Second}, tsa), tsa, vecSigned, capsule.VerdictSealed)
|
|
tsaName := cmstest.TLV(0xa0, cmstest.TLV(0xa4, cmstest.Name(cmstest.ATV(cmstest.OIDCommonName, cmstest.UTF8("Autoridad de Sellado de prueba")))))
|
|
exts := cmstest.TLV(0xa1, cmstest.Extension([]int{1, 2, 3, 4}, false, cmstest.Null()))
|
|
sealedAt("seal: ordering TRUE, a nonce, a tsa and extensions: S4", tok(vecSigned, cmstest.TokenOptions{After: [][]byte{cmstest.Bool(true), cmstest.Int(99), tsaName, exts}, Accuracy: time.Second}, tsa), tsa, vecSigned, capsule.VerdictSealed)
|
|
rsaTSA := named(cmstest.NewRSA("Autoridad RSA de prueba", 2048, certFrom, certTo), "Autoridad RSA de prueba")
|
|
sealedAt("seal: an authority of RSA with RSASSA-PSS and SHA-512: S4", tok(vecSigned, cmstest.TokenOptions{CMS: cmstest.Options{PSS: true, Hash: crypto.SHA512}, Accuracy: time.Second}, rsaTSA), rsaTSA, vecSigned, capsule.VerdictSealed)
|
|
// The case of §76, change 1: a name that lines up a text of its own.
|
|
spaced := cmstest.NewECDSA("TSA"+strings.Repeat(" ", 50)+"Firmado con la clave que guardaste como Banco", elliptic.P256(), certFrom, certTo)
|
|
vspaced := vsigner{spaced, hashOfCert(spaced), ""}
|
|
sealedAt("seal: an authority named with 50 spaces and the text of F3: S4, by its SHA-256", tok(vecSigned, second, vspaced), vspaced, vecSigned, capsule.VerdictSealed)
|
|
|
|
// S3: it reads, and does not verify (§29.11, step 3).
|
|
seal("seal: over another subject: S3", cmstest.Token([]byte("other"), vecSigned, cmstest.TokenOptions{}, tsa.Signer), capsule.VerdictSealInvalid)
|
|
seal("seal: a messageImprint of 33 bytes: S3", tok(vecSigned, cmstest.TokenOptions{Imprint: append(sha256Of(subject[:]), 0)}, tsa), capsule.VerdictSealInvalid)
|
|
seal("seal: the authority had expired at its time: S3", tok(certTo.AddDate(1, 0, 0), cmstest.TokenOptions{}, tsa), capsule.VerdictSealInvalid)
|
|
seal("seal: the authority was not yet valid at its time: S3", tok(certFrom.AddDate(-1, 0, 0), cmstest.TokenOptions{}, tsa), capsule.VerdictSealInvalid)
|
|
seal("seal: the signature of the authority does not verify: S3", tok(vecSigned, cmstest.TokenOptions{CMS: cmstest.Options{CorruptSignature: true}}, tsa), capsule.VerdictSealInvalid)
|
|
seal("seal: the message-digest of the token is not that of its TSTInfo: S3", tok(vecSigned, cmstest.TokenOptions{CMS: cmstest.Options{Message: []byte("another TSTInfo")}}, tsa), capsule.VerdictSealInvalid)
|
|
|
|
// S2: the form (§29.11, step 1).
|
|
seal("seal: not DER: S2", []byte("not DER"), capsule.VerdictSealUnreadable)
|
|
seal("seal: a token in BER: S2", tok(vecSigned, cmstest.TokenOptions{CMS: cmstest.Options{BER: true}}, tsa), capsule.VerdictSealUnreadable)
|
|
seal("seal: a token without its message-digest: S2", tok(vecSigned, cmstest.TokenOptions{NoMessageDigest: true}, tsa), capsule.VerdictSealUnreadable)
|
|
info := cmstest.TSTInfo(subject[:], vecSigned, cmstest.TokenOptions{})
|
|
other := named(cmstest.NewECDSA("Otra autoridad", elliptic.P256(), certFrom, certTo), "Otra autoridad")
|
|
seal("seal: a token of two SignerInfo: S2", cmstest.Merge(cmstest.TokenRaw(info, tsa.Signer), cmstest.TokenRaw(info, other.Signer)), capsule.VerdictSealUnreadable)
|
|
seal("seal: a TSTInfo of version 2: S2", tok(vecSigned, cmstest.TokenOptions{Version: 2}, tsa), capsule.VerdictSealUnreadable)
|
|
for _, tc := range []struct {
|
|
name string
|
|
raw []byte
|
|
}{
|
|
{"seal: a negative accuracy: S2", cmstest.Seq(cmstest.Int(-1))},
|
|
{"seal: an accuracy of seconds in an INTEGER that is not minimal: S2", cmstest.Seq(cmstest.IntBytes([]byte{0, 5}))},
|
|
{"seal: an accuracy of millis in an INTEGER that is not minimal: S2", cmstest.Seq(cmstest.TLV(0x80, []byte{0, 5}))},
|
|
{"seal: an accuracy of 0 millis: S2", cmstest.Seq(cmstest.TLV(0x80, []byte{0}))},
|
|
{"seal: an accuracy of 1000 millis: S2", cmstest.Seq(cmstest.TLV(0x80, []byte{0x03, 0xe8}))},
|
|
{"seal: an accuracy of 1000 micros: S2", cmstest.Seq(cmstest.TLV(0x81, []byte{0x03, 0xe8}))},
|
|
{"seal: an accuracy of 2^31 seconds: S2", cmstest.Seq(cmstest.Int(1 << 31))},
|
|
} {
|
|
seal(tc.name, tok(vecSigned, cmstest.TokenOptions{AccuracyRaw: tc.raw}, tsa), capsule.VerdictSealUnreadable)
|
|
}
|
|
seal("seal: a genTime without Z: S2", tok(vecSigned, cmstest.TokenOptions{GenTimeRaw: cmstest.GeneralizedTime("20260930120000")}, tsa), capsule.VerdictSealUnreadable)
|
|
seal("seal: a genTime with a trailing zero in its fraction: S2", tok(vecSigned, cmstest.TokenOptions{GenTimeRaw: cmstest.GeneralizedTime("20260930120000.50Z")}, tsa), capsule.VerdictSealUnreadable)
|
|
seal("seal: ordering FALSE written: S2", tok(vecSigned, cmstest.TokenOptions{OrderingFalse: true}, tsa), capsule.VerdictSealUnreadable)
|
|
seal("seal: a field after the last: S2", tok(vecSigned, cmstest.TokenOptions{After: [][]byte{exts, cmstest.Int(7)}}, tsa), capsule.VerdictSealUnreadable)
|
|
|
|
// S1: the algorithms (§29.11, step 2), after the form.
|
|
seal("seal: SHA-384 in the imprint: S1", tok(vecSigned, cmstest.TokenOptions{Hash: crypto.SHA384}, tsa), capsule.VerdictSealUnsupported)
|
|
seal("seal: a token signed with SHA-1: S1", tok(vecSigned, cmstest.TokenOptions{CMS: cmstest.Options{Hash: crypto.SHA1}}, tsa), capsule.VerdictSealUnsupported)
|
|
ecKey := cmstest.ECKey(elliptic.P256())
|
|
compressed := vsigner{cmstest.NewCert(cmstest.CertSpec{CN: "TSA comprimida", SPKI: cmstest.SPKICompressed(&ecKey.PublicKey)}, ecKey), "TSA comprimida", "TSA comprimida"}
|
|
seal("seal: an authority with a compressed key: S1", tok(vecSigned, cmstest.TokenOptions{}, compressed), capsule.VerdictSealUnsupported)
|
|
seal("seal: an authority with a key of 1024 bits: S1", tok(vecSigned, cmstest.TokenOptions{}, named(cmstest.NewRSA("TSA corta", 1024, certFrom, certTo), "TSA corta")), capsule.VerdictSealUnsupported)
|
|
|
|
// The seal stands apart from the signature: over no signature, and over
|
|
// a signature of an alg that the reader does not implement, whose bytes it
|
|
// seals all the same (§29.11, SIG_PART).
|
|
noSig := capsule.SealSubject(g.ctx.ControlCommit, g.ctx.HeadDigest, capsule.SigPart(nil))
|
|
g.add(vcase{name: "seal: over a capsule without a signature: F0 and S4", area: g.area(nil, g.must(capsule.EncodeSeal(capsule.SealTypeRFC3161, cmstest.Token(noSig[:], vecSigned, second, tsa.Signer)))),
|
|
sig: capsule.VerdictNoSignature, seal: capsule.VerdictSealed, sealTSA: tsa, sealTime: vecSigned})
|
|
unknown := g.must(capsule.EncodeAuthorSignature(capsule.AlgTest, []byte{1}, []byte{1}))
|
|
beside := capsule.SealSubject(g.ctx.ControlCommit, g.ctx.HeadDigest, capsule.SigPart(unknown))
|
|
g.add(vcase{name: "seal: beside a signature of alg 4294967295, which it seals all the same: F1 and S4", area: g.area(unknown, g.must(capsule.EncodeSeal(capsule.SealTypeRFC3161, cmstest.Token(beside[:], vecSigned, second, tsa.Signer)))),
|
|
sig: capsule.VerdictSignatureUnchecked, seal: capsule.VerdictSealed, sealTSA: tsa, sealTime: vecSigned})
|
|
}
|
|
|
|
func sha256Of(b []byte) []byte {
|
|
h := sha256.Sum256(b)
|
|
return h[:]
|
|
}
|