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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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.