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/cms/cert_test.go

250 lines
18 KiB

Tests of the CMS reader: certificates field by field, every check, fuzzing The builder of the tests, cmstest: - NewCert writes a certificate from the DER of its tbsCertificate, field by field: names of any string type with any bytes (UTF8String, PrintableString with an underscore or an at sign, IA5String, TeletexString, BMPString of odd length or with a surrogate, VisibleString, NumericString), an attribute twice or none, no version, times with a fraction, an extension twice, a compressed EC key, an even modulus, and any signature. A Signer made so serves Signature and Token. - Options for the version of a SignerInfo, the hashAlgorithm and the certHash of an ESSCertIDv2, a signatureAlgorithm other than the one of the key, a certificate twice, two content-type attributes, an attribute with an arc of 2^31, a SignerInfo twice, BER, signerInfos out of order, two signature-time-stamp attributes, and edits of the SignedData and of each SignerInfo. - Token options for any accuracy, a genTime of free text, ordering FALSE, a field after the last, an imprint of any length, no message-digest, a CRL in crls and the certificate of the authority twice. - Edits of the DER after signing: Edit, Retag, Withdraw (a SignerInfo removed), WithoutTimeStamp (a CAdES-T removed), Merge (a co-signature) and Indefinite. The tests of internal/cms and internal/der fail for each check of cms.go, cert.go, verify.go and der.go. A mutation run, which replaces each leaf of each condition by false and by true, one at a time, kills every mutant that is not equivalent to the code it mutates. FuzzParseSignature, FuzzParseToken, FuzzParseCert and FuzzDERCheck, seeded with security_cms.json and with what cmstest builds: no panic, what Check accepts Split reads, and the parsers fail only with ErrForm or ErrAlgorithm. capsule: the case of a seal outside the validity of the certificate gave an invalid seal; it now tests a certificate that expired before a valid seal (out of validity) apart from an authority that was not valid at its time (invalid seal). SIGNERS out of order, empty, too long, with 31 bytes or with a hash twice are F1 beside a CMS signature that is valid for the AUTHOR_MESSAGE of those SIGNERS, and the names of certificates show as spec v0.12 §29.7 says. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
package cms_test
import (
"bytes"
"crypto/sha256"
"encoding/asn1"
"testing"
"time"
"g.activething.com/go/DateKeys/internal/cms"
"g.activething.com/go/DateKeys/internal/cms/cmstest"
)
// The profile of the certificate (spec §29.10): what a certificate made field
// by field gives, and each rule that a certificate can break.
func TestCertProfile(t *testing.T) {
spec := cmstest.CertSpec{CN: "Ana López", From: time.Date(2021, 2, 3, 4, 5, 6, 0, time.UTC), To: time.Date(2051, 2, 3, 4, 5, 6, 0, time.UTC), Serial: cmstest.Int(0x1234)}
s := cmstest.NewCert(spec, ecKey)
c, err := cms.ParseCert(s.Cert.Raw)
if err != nil {
t.Fatal(err)
}
if c.Hash != sha256.Sum256(s.Cert.Raw) || !bytes.Equal(c.Raw, s.Cert.Raw) || !bytes.Equal(c.Serial, []byte{0x12, 0x34}) ||
!bytes.Equal(c.RawIssuer, s.Cert.RawIssuer) || !bytes.Equal(c.RawSubject, s.Cert.RawIssuer) || !bytes.Equal(c.SKI, s.Cert.SubjectKeyId) ||
!c.NotBefore.Equal(spec.From) || !c.NotAfter.Equal(spec.To) || !bytes.Equal(c.SPKI, cmstest.SPKI(ecKey.Public())) {
t.Errorf("the fields: %+v", c)
}
// The validity is inclusive (RFC 5280 4.1.2.5).
for at, want := range map[time.Time]bool{
spec.From: true, spec.From.Add(-time.Nanosecond): false, spec.To: true, spec.To.Add(time.Nanosecond): false, now: true,
} {
if c.ValidAt(at) != want {
t.Errorf("valid at %v: %v", at, !want)
}
}
raw := s.Cert.Raw
tbs := func(i int) []int { return []int{0, i} }
time1 := cmstest.UTCTime("200101000000Z")
for name, b := range map[string][]byte{
"a SET": cmstest.Edit(raw, cmstest.Retag(0x31)),
"a fourth element": cmstest.Edit(raw, cmstest.Append(cmstest.Null())),
"tbsCertificate as a SET": cmstest.Edit(raw, cmstest.Retag(0x31), 0),
"signatureAlgorithm as a SET": cmstest.Edit(raw, cmstest.Retag(0x31), 1),
"the signature as an OCTET STRING": cmstest.Edit(raw, cmstest.Retag(0x04), 2),
"no version: a certificate of v1": cert(cmstest.CertSpec{NoVersion: true}),
"version 1 with extensions": cert(cmstest.CertSpec{NoVersion: true, After: [][]byte{cmstest.Null()}}),
"the version written as 1": cert(cmstest.CertSpec{Version: cmstest.TLV(0xa0, cmstest.Int(0))}),
"the version written as 2": cert(cmstest.CertSpec{Version: cmstest.TLV(0xa0, cmstest.Int(1))}),
"the version as [1]": cert(cmstest.CertSpec{Version: cmstest.TLV(0xa1, cmstest.Int(2))}),
"the version of two INTEGERs": cert(cmstest.CertSpec{Version: cmstest.TLV(0xa0, cmstest.Int(2), cmstest.Int(2))}),
"a tbsCertificate without its SPKI": cmstest.Edit(raw, func(b []byte) []byte { return cmstest.Seq(cmstest.Children(b)[:6]...) }, 0),
"the serial as an OCTET STRING": cert(cmstest.CertSpec{Serial: cmstest.Octets([]byte{1})}),
"the signature of tbs as a SET": cmstest.Edit(raw, cmstest.Retag(0x31), tbs(2)...),
"the issuer as a SET": cmstest.Edit(raw, cmstest.Retag(0x31), tbs(3)...),
"the validity as a SET": cmstest.Edit(raw, cmstest.Retag(0x31), tbs(4)...),
"the subject as a SET": cmstest.Edit(raw, cmstest.Retag(0x31), tbs(5)...),
"the SPKI as a SET": cmstest.Edit(raw, cmstest.Retag(0x31), tbs(6)...),
"a validity of three times": cmstest.Edit(raw, cmstest.Append(time1), tbs(4)...),
"a validity of one time": cmstest.Edit(raw, cmstest.Replace(cmstest.Seq(time1)), tbs(4)...),
"notBefore with a fraction": cert(cmstest.CertSpec{NotBefore: cmstest.GeneralizedTime("20200101000000.5Z")}),
"notAfter with a fraction": cert(cmstest.CertSpec{NotAfter: cmstest.GeneralizedTime("20391231235959.5Z")}),
"notBefore an INTEGER": cert(cmstest.CertSpec{NotBefore: cmstest.Int(1)}),
"notAfter that is not a time": cert(cmstest.CertSpec{NotAfter: cmstest.UTCTime("not a time!!Z")}),
"subjectUniqueID before issuerUniqueID": cert(cmstest.CertSpec{UniqueIDs: [][]byte{cmstest.TLV(0x82, []byte{0, 2}), cmstest.TLV(0x81, []byte{0, 1})}}),
"the extensions as [4]": cert(cmstest.CertSpec{NoExtensions: true, After: [][]byte{cmstest.TLV(0xa4, cmstest.Seq(cmstest.ExtKeyUsage()))}}),
"an element after the extensions": cert(cmstest.CertSpec{After: [][]byte{cmstest.Null()}}),
"[3] of two SEQUENCEs": cert(cmstest.CertSpec{NoExtensions: true, After: [][]byte{cmstest.TLV(0xa3, cmstest.Seq(cmstest.ExtKeyUsage()), cmstest.Seq(cmstest.ExtKeyUsage()))}}),
"[3] holding a SET": cert(cmstest.CertSpec{NoExtensions: true, After: [][]byte{cmstest.TLV(0xa3, cmstest.Set(0x31, cmstest.ExtKeyUsage()))}}),
"an empty [3]": cert(cmstest.CertSpec{NoExtensions: true, After: [][]byte{cmstest.TLV(0xa3)}}),
"no Extension": cert(cmstest.CertSpec{Extensions: [][]byte{}}),
"an Extension as a SET": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.TLV(0x31, cmstest.OID(oidX), cmstest.Octets(nil))}}),
"an Extension that is an INTEGER": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.Int(1)}}),
"an Extension of only its type": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.Seq(cmstest.OID(oidX))}}),
"an empty Extension": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.Seq()}}),
"an Extension of four elements": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.Seq(cmstest.OID(oidX), cmstest.Bool(true), cmstest.Octets(nil), cmstest.Octets(nil))}}),
"an Extension whose type is an INTEGER": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.Seq(cmstest.Int(1), cmstest.Octets(nil))}}),
"an Extension whose value is not OCTETS": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.Seq(cmstest.OID(oidX), cmstest.BitString(nil))}}),
"an Extension critical by an INTEGER": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.Seq(cmstest.OID(oidX), cmstest.Int(1), cmstest.Octets(nil))}}),
"an extension twice": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.ExtKeyUsage(), cmstest.ExtKeyUsage()}}),
"subjectKeyIdentifier twice": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.ExtSKI([]byte{1}), cmstest.ExtSKI([]byte{2})}}),
"a subjectKeyIdentifier not in DER": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.Extension(cmstest.OIDSKI, false, []byte{0x04, 0x01, 0x07, 0x00})}}),
"a subjectKeyIdentifier an INTEGER": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.Extension(cmstest.OIDSKI, false, cmstest.Int(7))}}),
"an empty subjectKeyIdentifier": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.ExtSKI(nil)}}),
"a subjectKeyIdentifier of no bytes": cert(cmstest.CertSpec{Extensions: [][]byte{cmstest.Extension(cmstest.OIDSKI, false, nil)}}),
} {
if _, err := cms.ParseCert(b); err == nil {
t.Errorf("%s: accepted", name)
}
}
// A name that breaks the profile, as the subject beside a good issuer,
// and as the issuer beside a good subject.
good := cmstest.Name(cmstest.ATV(cmstest.OIDCommonName, cmstest.UTF8("A")))
for name, n := range map[string][]byte{
"an RDN as a SEQUENCE": cmstest.NameOf(cmstest.Seq(cmstest.ATV(cmstest.OIDCommonName, cmstest.UTF8("A")))),
"an empty RDN": cmstest.NameOf(cmstest.RDN()),
"an RDN that is an INTEGER": cmstest.NameOf(cmstest.Int(1)),
"an AttributeTypeAndValue as a SET": cmstest.NameOf(cmstest.RDN(cmstest.TLV(0x31, cmstest.OID(cmstest.OIDCommonName), cmstest.UTF8("A")))),
"an AttributeTypeAndValue of three": cmstest.NameOf(cmstest.RDN(cmstest.Seq(cmstest.OID(cmstest.OIDCommonName), cmstest.UTF8("A"), cmstest.Null()))),
"an AttributeTypeAndValue of one": cmstest.NameOf(cmstest.RDN(cmstest.Seq(cmstest.OID(cmstest.OIDCommonName)))),
"an AttributeTypeAndValue primitive": cmstest.NameOf(cmstest.RDN(cmstest.Int(3))),
"an attribute type that is an INTEGER": cmstest.NameOf(cmstest.RDN(cmstest.Seq(cmstest.Int(3), cmstest.UTF8("A")))),
} {
if _, err := cms.ParseCert(cert(cmstest.CertSpec{Subject: n, Issuer: good})); err == nil {
t.Errorf("the subject, %s: accepted", name)
}
if _, err := cms.ParseCert(cert(cmstest.CertSpec{Subject: good, Issuer: n})); err == nil {
t.Errorf("the issuer, %s: accepted", name)
}
}
// What the profile accepts: both unique identifiers, no extensions in a
// certificate of version 3, a critical subjectKeyIdentifier, an unknown
// extension, a GeneralizedTime before 2050, and any signature.
for name, spec := range map[string]cmstest.CertSpec{
"both unique identifiers": {UniqueIDs: [][]byte{cmstest.TLV(0x81, []byte{0, 1}), cmstest.TLV(0x82, []byte{0, 2})}},
"only subjectUniqueID": {UniqueIDs: [][]byte{cmstest.TLV(0x82, []byte{0, 2})}, NoExtensions: true},
"no extensions": {NoExtensions: true},
"a critical subjectKeyIdentifier": {Extensions: [][]byte{cmstest.Extension(cmstest.OIDSKI, true, cmstest.Octets([]byte{9}))}},
"an unknown extension": {Extensions: [][]byte{cmstest.Extension(oidX, false, nil), cmstest.ExtSKI([]byte{9})}},
"a GeneralizedTime in 2030": {NotAfter: cmstest.GeneralizedTime("20300101000000Z")},
"a signature that is no one's": {Signature: cmstest.BitString([]byte("not a signature"))},
"another signature algorithm": {SigAlg: cmstest.AlgID(asn1.ObjectIdentifier{1, 2, 3}, cmstest.Int(7))},
"an empty name": {Subject: cmstest.NameOf(), Issuer: cmstest.NameOf()},
} {
c, err := cms.ParseCert(cert(spec))
if err != nil {
t.Errorf("%s: %v", name, err)
continue
}
if name == "a critical subjectKeyIdentifier" && !bytes.Equal(c.SKI, []byte{9}) || name == "no extensions" && c.SKI != nil {
t.Errorf("%s: SKI %x", name, c.SKI)
}
}
}
var oidX = asn1.ObjectIdentifier{1, 2, 3, 4, 5}
// A NumericString is DER, and a certificate whose name holds one, as the INN
// of a Russian certificate, meets the profile: its value is no text, and the
// signature verifies (spec §29.10).
func TestNumericStringInName(t *testing.T) {
inn := cmstest.ATV(asn1.ObjectIdentifier{1, 2, 643, 3, 131, 1, 1}, cmstest.Numeric("123456789012"))
s := cmstest.NewCert(cmstest.CertSpec{Subject: cmstest.Name(cn(cmstest.UTF8("Ivan Petrov")), inn)}, ecKey)
if si := signerOf(t, "a NumericString", cmstest.Signature(msg, cmstest.Options{}, s)); si.Check(msg) != cms.Valid || si.Cert.Holder() != "Ivan Petrov" {
t.Errorf("a NumericString beside the commonName: %v %q", si.Check(msg), si.Cert.Holder())
}
if c := subject(cn(cmstest.Numeric("12345"))); c.Holder() != "" {
t.Errorf("a commonName in a NumericString: %q", c.Holder())
}
}
// cert returns the DER of the certificate of spec, of the key ecKey.
func cert(spec cmstest.CertSpec) []byte { return cmstest.NewCert(spec, ecKey).Cert.Raw }
// subject is a certificate of ecKey whose subject has the attributes given,
// each in a RelativeDistinguishedName of its own.
func subject(atvs ...[]byte) *cms.Cert {
return parsed(cmstest.CertSpec{Subject: cmstest.Name(atvs...)})
}
func parsed(spec cmstest.CertSpec) *cms.Cert {
c, err := cms.ParseCert(cert(spec))
if err != nil {
panic(err)
}
return c
}
func cn(v []byte) []byte { return cmstest.ATV(cmstest.OIDCommonName, v) }
func given(v []byte) []byte { return cmstest.ATV(cmstest.OIDGivenName, v) }
func surname(v []byte) []byte { return cmstest.ATV(cmstest.OIDSurname, v) }
func org(v []byte) []byte { return cmstest.ATV(cmstest.OIDOrganization, v) }
// The text of a name (spec §29.10): only of the five string types, in their
// alphabets, nothing removed, never from an attribute that appears twice; and
// the holder (spec §29.7): givenName and surname before commonName.
func TestCertNames(t *testing.T) {
utf8 := cmstest.UTF8
for name, tc := range map[string]struct {
c *cms.Cert
want string
}{
"a UTF8String": {subject(cn(utf8("Ana López"))), "Ana López"},
"a UTF8String that is not UTF-8": {subject(cn(utf8("Ana \xff"))), ""},
"a UTF8String with a BOM, kept": {subject(cn(utf8("\xef\xbb\xbfAna"))), "\xef\xbb\xbfAna"},
"a PrintableString of its whole alphabet": {subject(cn(cmstest.Printable("Ana O'Neil (1+2), x-y./:=? Z9"))), "Ana O'Neil (1+2), x-y./:=? Z9"},
"a PrintableString with an underscore": {subject(cn(cmstest.Printable("Ana_Lopez"))), ""},
"a PrintableString with an at sign": {subject(cn(cmstest.Printable("ana@example.com"))), ""},
"a PrintableString with a tilde": {subject(cn(cmstest.Printable("Ana~"))), ""},
"a PrintableString with an ampersand": {subject(cn(cmstest.Printable("A&B"))), ""},
"a PrintableString with an asterisk": {subject(cn(cmstest.Printable("A*B"))), ""},
"a PrintableString with a byte of 0xe9": {subject(cn(cmstest.Printable("L\xe9a"))), ""},
"an IA5String": {subject(cn(cmstest.IA5("ana@example.com"))), "ana@example.com"},
"an IA5String with a byte of 0x80": {subject(cn(cmstest.IA5("Ana\x80"))), ""},
"a TeletexString in ASCII": {subject(cn(cmstest.Teletex("Ana Lopez"))), "Ana Lopez"},
"a TeletexString with a byte of 0xe9": {subject(cn(cmstest.Teletex("L\xe9a"))), ""},
"a BMPString": {subject(cn(cmstest.BMPText("Ana López"))), "Ana López"},
"a BMPString above the surrogates": {subject(cn(cmstest.BMPText("Ana ¥"))), "Ana ¥"},
"a BMPString of odd length": {subject(cn(cmstest.BMP([]byte{0, 'A', 0}))), ""},
"a BMPString with a surrogate pair": {subject(cn(cmstest.BMPText("Ana \U0001F600"))), ""},
"a BMPString with a low surrogate": {subject(cn(cmstest.BMP([]byte{0, 'A', 0xdc, 0}))), ""},
"a BMPString with a high surrogate": {subject(cn(cmstest.BMP([]byte{0xd8, 0, 0, 'A'}))), ""},
"a VisibleString": {subject(cn(cmstest.Visible("Ana"))), ""},
"a UniversalString": {subject(cn(cmstest.TLV(0x1c, []byte{0, 0, 0, 'A'}))), ""},
"a NumericString": {subject(cn(cmstest.Numeric("12345"))), ""},
"two commonNames": {subject(cn(utf8("Ana")), cn(utf8("Luis"))), ""},
"two commonNames in one RDN": {parsed(cmstest.CertSpec{Subject: cmstest.NameOf(cmstest.RDN(cn(utf8("Ana")), cn(utf8("Luis"))))}), ""},
"a commonName in an RDN of two attributes": {parsed(cmstest.CertSpec{Subject: cmstest.NameOf(cmstest.RDN(org(utf8("Banco")), cn(utf8("Ana"))))}), "Ana"},
"no commonName": {subject(org(utf8("Banco"))), ""},
"givenName, surname and the NIF in the CN": {subject(cn(utf8("ESPAÑOL ESPAÑOL JUAN - 12345678Z")), given(utf8("JUAN")), surname(utf8("ESPAÑOL ESPAÑOL"))), "JUAN ESPAÑOL ESPAÑOL"},
"givenName and surname without a CN": {subject(given(utf8("Ana")), surname(utf8("López"))), "Ana López"},
"only a givenName": {subject(cn(utf8("Ana López")), given(utf8("Ana"))), "Ana López"},
"only a surname": {subject(cn(utf8("Ana López")), surname(utf8("López"))), "Ana López"},
"an empty givenName": {subject(cn(utf8("Ana López")), given(utf8("")), surname(utf8("López"))), "Ana López"},
"an empty surname": {subject(cn(utf8("Ana López")), given(utf8("Ana")), surname(utf8(""))), "Ana López"},
"a givenName that is not UTF-8": {subject(cn(utf8("Ana López")), given(utf8("An\xff")), surname(utf8("López"))), "Ana López"},
"a surname that is not UTF-8": {subject(cn(utf8("Ana López")), given(utf8("Ana")), surname(utf8("L\xff"))), "Ana López"},
"a givenName that is no text": {subject(cn(utf8("Ana López")), given(cmstest.Visible("Ana")), surname(utf8("López"))), "Ana López"},
"two givenNames": {subject(cn(utf8("Ana López")), given(utf8("Ana")), given(utf8("Eva")), surname(utf8("López"))), "Ana López"},
"a givenName with an escape, not the CN": {subject(cn(utf8("Ana López")), given(utf8("Ana\x1b")), surname(utf8("López"))), "Ana\x1b López"},
"a commonName with an escape, kept as text": {subject(cn(utf8("Ana\x1b[31m"))), "Ana\x1b[31m"},
} {
if got := tc.c.Holder(); got != tc.want {
t.Errorf("%s: holder %q, want %q", name, got, tc.want)
}
}
// The issuer: its commonName, or its organizationName without one; ""
// with neither, the caller showing then the hash of the name.
issuer := func(atvs ...[]byte) *cms.Cert {
return parsed(cmstest.CertSpec{Issuer: cmstest.Name(atvs...)})
}
for name, tc := range map[string]struct {
c *cms.Cert
want string
}{
"a commonName": {issuer(org(utf8("Banco")), cn(utf8("CA de prueba"))), "CA de prueba"},
"an organizationName without a CN": {issuer(org(utf8("Banco S.A."))), "Banco S.A."},
"a commonName with an escape, kept": {issuer(org(utf8("Banco")), cn(utf8("CA\x1b"))), "CA\x1b"},
"a commonName not UTF-8, then the O": {issuer(org(utf8("Banco")), cn(utf8("C\xff"))), "Banco"},
"two commonNames, then the O": {issuer(org(utf8("Banco")), cn(utf8("A")), cn(utf8("B"))), "Banco"},
"an organizationName that is not UTF-8": {issuer(org(utf8("B\xff"))), ""},
"neither": {issuer(cmstest.ATV(cmstest.OIDCountry, cmstest.Printable("ES"))), ""},
"an organizationName that is not text": {issuer(org(cmstest.Visible("Banco"))), ""},
} {
if got := tc.c.IssuerName(); got != tc.want {
t.Errorf("issuer, %s: %q, want %q", name, got, tc.want)
}
}
}

Powered by TurnKey Linux.