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) } } }