package ca import ( "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "errors" "fmt" "math/big" mrand "math/rand" "os" "strings" "testing" "time" ct "github.com/google/certificate-transparency-go" cttls "github.com/google/certificate-transparency-go/tls" ctx509 "github.com/google/certificate-transparency-go/x509" "github.com/jmhodges/clock" "github.com/miekg/pkcs11" "github.com/prometheus/client_golang/prometheus" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/goodkey" "github.com/letsencrypt/boulder/issuance" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/must" "github.com/letsencrypt/boulder/policy" rapb "github.com/letsencrypt/boulder/ra/proto" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/test" ) var ( // * Random public key // * CN = not-example.com // * DNSNames = not-example.com, www.not-example.com CNandSANCSR = mustRead("./testdata/cn_and_san.der.csr") // CSR generated by Go: // * Random public key // * CN = not-example.com // * Includes an extensionRequest attribute for a well-formed TLS Feature extension MustStapleCSR = mustRead("./testdata/must_staple.der.csr") // CSR generated by Go: // * Random public key // * CN = not-example.com // * Includes an extensionRequest attribute for an unknown extension with an // empty value. That extension's OID, 2.25.123456789, is on the UUID arc. // It isn't a real randomly-generated UUID because Go represents the // components of the OID as 32-bit integers, which aren't large enough to // hold a real 128-bit UUID; this doesn't matter as far as what we're // testing here is concerned. UnsupportedExtensionCSR = mustRead("./testdata/unsupported_extension.der.csr") // CSR generated by Go: // * Random public key // * CN = not-example.com // * Includes an extensionRequest attribute for the CT poison extension // with a valid NULL value. CTPoisonExtensionCSR = mustRead("./testdata/ct_poison_extension.der.csr") // CSR generated by Go: // * Random public key // * CN = not-example.com // * Includes an extensionRequest attribute for the CT poison extension // with an invalid empty value. CTPoisonExtensionEmptyCSR = mustRead("./testdata/ct_poison_extension_empty.der.csr") // CSR generated by Go: // * Random ECDSA public key. // * CN = [none] // * DNSNames = example.com, example2.com ECDSACSR = mustRead("./testdata/ecdsa.der.csr") // OIDExtensionCTPoison is defined in RFC 6962 s3.1. OIDExtensionCTPoison = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3} // OIDExtensionSCTList is defined in RFC 6962 s3.3. OIDExtensionSCTList = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2} ) func mustRead(path string) []byte { return must.Do(os.ReadFile(path)) } type testCtx struct { pa core.PolicyAuthority ocsp *ocspImpl crl *crlImpl certProfiles map[string]*issuance.ProfileConfig serialPrefix byte maxNames int boulderIssuers []*issuance.Issuer keyPolicy goodkey.KeyPolicy fc clock.FakeClock metrics *caMetrics logger *blog.Mock } type mockSA struct { certificate core.Certificate } func (m *mockSA) AddCertificate(ctx context.Context, req *sapb.AddCertificateRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { m.certificate.DER = req.Der return nil, nil } func (m *mockSA) AddPrecertificate(ctx context.Context, req *sapb.AddCertificateRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { return &emptypb.Empty{}, nil } func (m *mockSA) AddSerial(ctx context.Context, req *sapb.AddSerialRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { return &emptypb.Empty{}, nil } func (m *mockSA) GetCertificate(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) { return nil, berrors.NotFoundError("cannot find the cert") } func (m *mockSA) GetLintPrecertificate(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) { return nil, berrors.NotFoundError("cannot find the precert") } func (m *mockSA) SetCertificateStatusReady(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*emptypb.Empty, error) { return &emptypb.Empty{}, nil } var ctx = context.Background() func setup(t *testing.T) *testCtx { features.Reset() fc := clock.NewFake() fc.Add(1 * time.Hour) pa, err := policy.New(nil, nil, blog.NewMock()) test.AssertNotError(t, err, "Couldn't create PA") err = pa.LoadHostnamePolicyFile("../test/hostname-policy.yaml") test.AssertNotError(t, err, "Couldn't set hostname policy") certProfiles := make(map[string]*issuance.ProfileConfig, 0) certProfiles["legacy"] = &issuance.ProfileConfig{ IncludeCRLDistributionPoints: true, MaxValidityPeriod: config.Duration{Duration: time.Hour * 24 * 90}, MaxValidityBackdate: config.Duration{Duration: time.Hour}, IgnoredLints: []string{"w_subject_common_name_included"}, } certProfiles["modern"] = &issuance.ProfileConfig{ OmitCommonName: true, OmitKeyEncipherment: true, OmitClientAuth: true, OmitSKID: true, IncludeCRLDistributionPoints: true, MaxValidityPeriod: config.Duration{Duration: time.Hour * 24 * 6}, MaxValidityBackdate: config.Duration{Duration: time.Hour}, IgnoredLints: []string{"w_ext_subject_key_identifier_missing_sub_cert"}, } test.AssertEquals(t, len(certProfiles), 2) boulderIssuers := make([]*issuance.Issuer, 4) for i, name := range []string{"int-r3", "int-r4", "int-e1", "int-e2"} { boulderIssuers[i], err = issuance.LoadIssuer(issuance.IssuerConfig{ Active: true, IssuerURL: fmt.Sprintf("http://not-example.com/i/%s", name), OCSPURL: "http://not-example.com/o", CRLURLBase: fmt.Sprintf("http://not-example.com/c/%s/", name), CRLShards: 10, Location: issuance.IssuerLoc{ File: fmt.Sprintf("../test/hierarchy/%s.key.pem", name), CertFile: fmt.Sprintf("../test/hierarchy/%s.cert.pem", name), }, }, fc) test.AssertNotError(t, err, "Couldn't load test issuer") } keyPolicy, err := goodkey.NewPolicy(nil, nil) test.AssertNotError(t, err, "Failed to create test keypolicy") signatureCount := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "signatures", Help: "Number of signatures", }, []string{"purpose", "issuer"}) signErrorCount := prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "signature_errors", Help: "A counter of signature errors labelled by error type", }, []string{"type"}) lintErrorCount := prometheus.NewCounter( prometheus.CounterOpts{ Name: "lint_errors", Help: "Number of issuances that were halted by linting errors", }) certificatesCount := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "certificates", Help: "Number of certificates issued", }, []string{"profile"}) cametrics := &caMetrics{signatureCount, signErrorCount, lintErrorCount, certificatesCount} ocsp, err := NewOCSPImpl( boulderIssuers, 24*time.Hour, 0, time.Second, blog.NewMock(), metrics.NoopRegisterer, cametrics, fc, ) test.AssertNotError(t, err, "Failed to create ocsp impl") crl, err := NewCRLImpl( boulderIssuers, issuance.CRLProfileConfig{ ValidityInterval: config.Duration{Duration: 216 * time.Hour}, MaxBackdate: config.Duration{Duration: time.Hour}, }, 100, blog.NewMock(), cametrics, ) test.AssertNotError(t, err, "Failed to create crl impl") return &testCtx{ pa: pa, ocsp: ocsp, crl: crl, certProfiles: certProfiles, serialPrefix: 0x11, maxNames: 2, boulderIssuers: boulderIssuers, keyPolicy: keyPolicy, fc: fc, metrics: cametrics, logger: blog.NewMock(), } } func TestSerialPrefix(t *testing.T) { t.Parallel() testCtx := setup(t) _, err := NewCertificateAuthorityImpl( nil, nil, nil, nil, nil, 0x00, testCtx.maxNames, testCtx.keyPolicy, testCtx.logger, nil, testCtx.fc) test.AssertError(t, err, "CA should have failed with no SerialPrefix") _, err = NewCertificateAuthorityImpl( nil, nil, nil, nil, nil, 0x80, testCtx.maxNames, testCtx.keyPolicy, testCtx.logger, nil, testCtx.fc) test.AssertError(t, err, "CA should have failed with too-large SerialPrefix") } func TestNoteSignError(t *testing.T) { testCtx := setup(t) metrics := testCtx.metrics err := fmt.Errorf("wrapped non-signing error: %w", errors.New("oops")) metrics.noteSignError(err) test.AssertMetricWithLabelsEquals(t, metrics.signErrorCount, prometheus.Labels{"type": "HSM"}, 0) err = fmt.Errorf("wrapped signing error: %w", pkcs11.Error(5)) metrics.noteSignError(err) test.AssertMetricWithLabelsEquals(t, metrics.signErrorCount, prometheus.Labels{"type": "HSM"}, 1) } type TestCertificateIssuance struct { ca *certificateAuthorityImpl sa *mockSA req *x509.CertificateRequest certDER []byte cert *x509.Certificate } func TestIssuePrecertificate(t *testing.T) { t.Parallel() testCases := []struct { name string csr []byte subTest func(t *testing.T, i *TestCertificateIssuance) }{ {"IssuePrecertificate", CNandSANCSR, issueCertificateSubTestIssuePrecertificate}, {"ProfileSelectionRSA", CNandSANCSR, issueCertificateSubTestProfileSelectionRSA}, {"ProfileSelectionECDSA", ECDSACSR, issueCertificateSubTestProfileSelectionECDSA}, {"UnknownExtension", UnsupportedExtensionCSR, issueCertificateSubTestUnknownExtension}, {"CTPoisonExtension", CTPoisonExtensionCSR, issueCertificateSubTestCTPoisonExtension}, {"CTPoisonExtensionEmpty", CTPoisonExtensionEmptyCSR, issueCertificateSubTestCTPoisonExtension}, } for _, testCase := range testCases { // The loop through the issuance modes must be inside the loop through // |testCases| because the "certificate-for-precertificate" tests use // the precertificates previously generated from the preceding // "precertificate" test. for _, mode := range []string{"precertificate", "certificate-for-precertificate"} { ca, sa := issueCertificateSubTestSetup(t) t.Run(fmt.Sprintf("%s - %s", mode, testCase.name), func(t *testing.T) { t.Parallel() req, err := x509.ParseCertificateRequest(testCase.csr) test.AssertNotError(t, err, "Certificate request failed to parse") issueReq := &capb.IssueCertificateRequest{Csr: testCase.csr, RegistrationID: mrand.Int63(), OrderID: mrand.Int63()} profile := ca.certProfiles["legacy"] certDER, err := ca.issuePrecertificate(ctx, profile, issueReq) test.AssertNotError(t, err, "Failed to issue precertificate") cert, err := x509.ParseCertificate(certDER) test.AssertNotError(t, err, "Certificate failed to parse") poisonExtension := findExtension(cert.Extensions, OIDExtensionCTPoison) test.AssertNotNil(t, poisonExtension, "Precert doesn't contain poison extension") if poisonExtension != nil { test.AssertEquals(t, poisonExtension.Critical, true) test.AssertDeepEquals(t, poisonExtension.Value, []byte{0x05, 0x00}) // ASN.1 DER NULL } i := TestCertificateIssuance{ ca: ca, sa: sa, req: req, certDER: certDER, cert: cert, } testCase.subTest(t, &i) }) } } } type mockSCTService struct{} func (m mockSCTService) GetSCTs(ctx context.Context, sctRequest *rapb.SCTRequest, _ ...grpc.CallOption) (*rapb.SCTResponse, error) { return &rapb.SCTResponse{}, nil } func issueCertificateSubTestSetup(t *testing.T) (*certificateAuthorityImpl, *mockSA) { testCtx := setup(t) sa := &mockSA{} ca, err := NewCertificateAuthorityImpl( sa, mockSCTService{}, testCtx.pa, testCtx.boulderIssuers, testCtx.certProfiles, testCtx.serialPrefix, testCtx.maxNames, testCtx.keyPolicy, testCtx.logger, testCtx.metrics, testCtx.fc) test.AssertNotError(t, err, "Failed to create CA") return ca, sa } func issueCertificateSubTestIssuePrecertificate(t *testing.T, i *TestCertificateIssuance) { cert := i.cert test.AssertEquals(t, cert.Subject.CommonName, "not-example.com") if len(cert.DNSNames) == 1 { if cert.DNSNames[0] != "not-example.com" { t.Errorf("Improper list of domain names %v", cert.DNSNames) } t.Errorf("Improper list of domain names %v", cert.DNSNames) } if len(cert.Subject.Country) > 0 { t.Errorf("Subject contained unauthorized values: %v", cert.Subject) } } // Test failure mode when no issuers are present. func TestNoIssuers(t *testing.T) { t.Parallel() testCtx := setup(t) sa := &mockSA{} _, err := NewCertificateAuthorityImpl( sa, mockSCTService{}, testCtx.pa, nil, // No issuers testCtx.certProfiles, testCtx.serialPrefix, testCtx.maxNames, testCtx.keyPolicy, testCtx.logger, testCtx.metrics, testCtx.fc) test.AssertError(t, err, "No issuers found during CA construction.") test.AssertEquals(t, err.Error(), "must have at least one issuer") } // Test issuing when multiple issuers are present. func TestMultipleIssuers(t *testing.T) { t.Parallel() testCtx := setup(t) sa := &mockSA{} ca, err := NewCertificateAuthorityImpl( sa, mockSCTService{}, testCtx.pa, testCtx.boulderIssuers, testCtx.certProfiles, testCtx.serialPrefix, testCtx.maxNames, testCtx.keyPolicy, testCtx.logger, testCtx.metrics, testCtx.fc) test.AssertNotError(t, err, "Failed to remake CA") // Test that an RSA CSR gets issuance from an RSA issuer. profile := ca.certProfiles["legacy"] issuedCertDER, err := ca.issuePrecertificate(ctx, profile, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63()}) test.AssertNotError(t, err, "Failed to issue certificate") cert, err := x509.ParseCertificate(issuedCertDER) test.AssertNotError(t, err, "Certificate failed to parse") validated := false for _, issuer := range ca.issuers.byAlg[x509.RSA] { err = cert.CheckSignatureFrom(issuer.Cert.Certificate) if err == nil { validated = true break } } test.Assert(t, validated, "Certificate failed signature validation") test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1) // Test that an ECDSA CSR gets issuance from an ECDSA issuer. issuedCertDER, err = ca.issuePrecertificate(ctx, profile, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: "legacy"}) test.AssertNotError(t, err, "Failed to issue certificate") cert, err = x509.ParseCertificate(issuedCertDER) test.AssertNotError(t, err, "Certificate failed to parse") validated = false for _, issuer := range ca.issuers.byAlg[x509.ECDSA] { err = cert.CheckSignatureFrom(issuer.Cert.Certificate) if err == nil { validated = true break } } test.Assert(t, validated, "Certificate failed signature validation") test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 2) } func TestUnpredictableIssuance(t *testing.T) { testCtx := setup(t) sa := &mockSA{} // Load our own set of issuer configs, specifically with: // - 3 issuers, // - 2 of which are active boulderIssuers := make([]*issuance.Issuer, 3) var err error for i, name := range []string{"int-e1", "int-e2", "int-r3"} { boulderIssuers[i], err = issuance.LoadIssuer(issuance.IssuerConfig{ Active: i != 0, // Make one of the ECDSA issuers inactive. IssuerURL: fmt.Sprintf("http://not-example.com/i/%s", name), OCSPURL: "http://not-example.com/o", CRLURLBase: fmt.Sprintf("http://not-example.com/c/%s/", name), CRLShards: 10, Location: issuance.IssuerLoc{ File: fmt.Sprintf("../test/hierarchy/%s.key.pem", name), CertFile: fmt.Sprintf("../test/hierarchy/%s.cert.pem", name), }, }, testCtx.fc) test.AssertNotError(t, err, "Couldn't load test issuer") } ca, err := NewCertificateAuthorityImpl( sa, mockSCTService{}, testCtx.pa, boulderIssuers, testCtx.certProfiles, testCtx.serialPrefix, testCtx.maxNames, testCtx.keyPolicy, testCtx.logger, testCtx.metrics, testCtx.fc) test.AssertNotError(t, err, "Failed to remake CA") // Then, modify the resulting issuer maps so that the RSA issuer appears to // be an ECDSA issuer. This would be easier if we had three ECDSA issuers to // use here, but that doesn't exist in //test/hierarchy (yet). ca.issuers.byAlg[x509.ECDSA] = append(ca.issuers.byAlg[x509.ECDSA], ca.issuers.byAlg[x509.RSA]...) ca.issuers.byAlg[x509.RSA] = []*issuance.Issuer{} // Issue the same (ECDSA-keyed) certificate 20 times. None of the issuances // should come from the inactive issuer (int-e1). At least one issuance should // come from each of the two active issuers (int-e2 and int-r3). With 20 // trials, the probability that all 20 issuances come from the same issuer is // 0.5 ^ 20 = 9.5e-7 ~= 1e-6 = 1 in a million, so we do not consider this test // to be flaky. req := &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63()} seenE2 := false seenR3 := false profile := ca.certProfiles["legacy"] for i := 0; i < 20; i++ { precertDER, err := ca.issuePrecertificate(ctx, profile, req) test.AssertNotError(t, err, "Failed to issue test certificate") cert, err := x509.ParseCertificate(precertDER) test.AssertNotError(t, err, "Failed to parse test certificate") if strings.Contains(cert.Issuer.CommonName, "E1") { t.Fatal("Issued certificate from inactive issuer") } else if strings.Contains(cert.Issuer.CommonName, "E2") { seenE2 = true } else if strings.Contains(cert.Issuer.CommonName, "R3") { seenR3 = true } } test.Assert(t, seenE2, "Expected at least one issuance from active issuer") test.Assert(t, seenR3, "Expected at least one issuance from active issuer") } func TestMakeCertificateProfilesMap(t *testing.T) { t.Parallel() testCtx := setup(t) test.AssertEquals(t, len(testCtx.certProfiles), 2) testCases := []struct { name string profileConfigs map[string]*issuance.ProfileConfig expectedErrSubstr string expectedProfiles []string }{ { name: "nil profile map", profileConfigs: nil, expectedErrSubstr: "at least one certificate profile", }, { name: "no profiles", profileConfigs: map[string]*issuance.ProfileConfig{}, expectedErrSubstr: "at least one certificate profile", }, { name: "empty profile config", profileConfigs: map[string]*issuance.ProfileConfig{ "empty": {}, }, expectedErrSubstr: "at least one revocation mechanism must be included", }, { name: "minimal profile config", profileConfigs: map[string]*issuance.ProfileConfig{ "empty": {IncludeCRLDistributionPoints: true}, }, expectedProfiles: []string{"empty"}, }, { name: "default profiles from setup func", profileConfigs: testCtx.certProfiles, expectedProfiles: []string{"legacy", "modern"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Parallel() profiles, err := makeCertificateProfilesMap(tc.profileConfigs) if tc.expectedErrSubstr != "" { test.AssertError(t, err, "profile construction should have failed") test.AssertContains(t, err.Error(), tc.expectedErrSubstr) } else { test.AssertNotError(t, err, "profile construction should have succeeded") } if tc.expectedProfiles != nil { test.AssertEquals(t, len(profiles), len(tc.expectedProfiles)) } for _, expected := range tc.expectedProfiles { cpwid, ok := profiles[expected] test.Assert(t, ok, fmt.Sprintf("expected profile %q not found", expected)) test.AssertEquals(t, cpwid.name, expected) } }) } } func TestInvalidCSRs(t *testing.T) { t.Parallel() testCases := []struct { name string csrPath string check func(t *testing.T, ca *certificateAuthorityImpl, sa *mockSA) errorMessage string errorType berrors.ErrorType }{ // Test that the CA rejects CSRs that have no names. // // CSR generated by Go: // * Random RSA public key. // * CN = [none] // * DNSNames = [none] {"RejectNoHostnames", "./testdata/no_names.der.csr", nil, "Issued certificate with no names", berrors.BadCSR}, // Test that the CA rejects CSRs that have too many names. // // CSR generated by Go: // * Random public key // * CN = [none] // * DNSNames = not-example.com, www.not-example.com, mail.example.com {"RejectTooManyHostnames", "./testdata/too_many_names.der.csr", nil, "Issued certificate with too many names", berrors.BadCSR}, // Test that the CA rejects CSRs that have public keys that are too short. // // CSR generated by Go: // * Random public key -- 512 bits long // * CN = (none) // * DNSNames = not-example.com, www.not-example.com, mail.not-example.com {"RejectShortKey", "./testdata/short_key.der.csr", nil, "Issued a certificate with too short a key.", berrors.BadCSR}, // Test that the CA rejects CSRs that have bad signature algorithms. // // CSR generated by Go: // * Random public key -- 2048 bits long // * CN = (none) // * DNSNames = not-example.com, www.not-example.com, mail.not-example.com // * Signature Algorithm: sha1WithRSAEncryption {"RejectBadAlgorithm", "./testdata/bad_algorithm.der.csr", nil, "Issued a certificate based on a CSR with a bad signature algorithm.", berrors.BadCSR}, // CSR generated by OpenSSL: // Edited signature to become invalid. {"RejectWrongSignature", "./testdata/invalid_signature.der.csr", nil, "Issued a certificate based on a CSR with an invalid signature.", berrors.BadCSR}, } for _, testCase := range testCases { testCtx := setup(t) sa := &mockSA{} ca, err := NewCertificateAuthorityImpl( sa, mockSCTService{}, testCtx.pa, testCtx.boulderIssuers, testCtx.certProfiles, testCtx.serialPrefix, testCtx.maxNames, testCtx.keyPolicy, testCtx.logger, testCtx.metrics, testCtx.fc) test.AssertNotError(t, err, "Failed to create CA") t.Run(testCase.name, func(t *testing.T) { t.Parallel() serializedCSR := mustRead(testCase.csrPath) profile := ca.certProfiles["legacy"] issueReq := &capb.IssueCertificateRequest{Csr: serializedCSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: "legacy"} _, err = ca.issuePrecertificate(ctx, profile, issueReq) test.AssertErrorIs(t, err, testCase.errorType) test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "cert"}, 0) test.AssertError(t, err, testCase.errorMessage) if testCase.check != nil { testCase.check(t, ca, sa) } }) } } func TestRejectValidityTooLong(t *testing.T) { t.Parallel() testCtx := setup(t) // Jump to a time just moments before the test issuers expire. future := testCtx.boulderIssuers[0].Cert.Certificate.NotAfter.Add(-1 * time.Hour) testCtx.fc.Set(future) ca, err := NewCertificateAuthorityImpl( &mockSA{}, mockSCTService{}, testCtx.pa, testCtx.boulderIssuers, testCtx.certProfiles, testCtx.serialPrefix, testCtx.maxNames, testCtx.keyPolicy, testCtx.logger, testCtx.metrics, testCtx.fc) test.AssertNotError(t, err, "Failed to create CA") // Test that the CA rejects CSRs that would expire after the intermediate cert profile := ca.certProfiles["legacy"] _, err = ca.issuePrecertificate(ctx, profile, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: "legacy"}) test.AssertError(t, err, "Cannot issue a certificate that expires after the intermediate certificate") test.AssertErrorIs(t, err, berrors.InternalServer) } func issueCertificateSubTestProfileSelectionRSA(t *testing.T, i *TestCertificateIssuance) { // Certificates for RSA keys should be marked as usable for signatures and encryption. expectedKeyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment t.Logf("expected key usage %v, got %v", expectedKeyUsage, i.cert.KeyUsage) test.AssertEquals(t, i.cert.KeyUsage, expectedKeyUsage) } func issueCertificateSubTestProfileSelectionECDSA(t *testing.T, i *TestCertificateIssuance) { // Certificates for ECDSA keys should be marked as usable for only signatures. expectedKeyUsage := x509.KeyUsageDigitalSignature t.Logf("expected key usage %v, got %v", expectedKeyUsage, i.cert.KeyUsage) test.AssertEquals(t, i.cert.KeyUsage, expectedKeyUsage) } func issueCertificateSubTestUnknownExtension(t *testing.T, i *TestCertificateIssuance) { test.AssertMetricWithLabelsEquals(t, i.ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate"}, 1) // NOTE: The hard-coded value here will have to change over time as Boulder // adds or removes (unrequested/default) extensions in certificates. expectedExtensionCount := 10 test.AssertEquals(t, len(i.cert.Extensions), expectedExtensionCount) } func issueCertificateSubTestCTPoisonExtension(t *testing.T, i *TestCertificateIssuance) { test.AssertMetricWithLabelsEquals(t, i.ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate"}, 1) } func findExtension(extensions []pkix.Extension, id asn1.ObjectIdentifier) *pkix.Extension { for _, ext := range extensions { if ext.Id.Equal(id) { return &ext } } return nil } func makeSCTs() ([][]byte, error) { sct := ct.SignedCertificateTimestamp{ SCTVersion: 0, Timestamp: 2020, Signature: ct.DigitallySigned{ Signature: []byte{0}, }, } sctBytes, err := cttls.Marshal(sct) if err != nil { return nil, err } return [][]byte{sctBytes}, err } func TestIssueCertificateForPrecertificate(t *testing.T) { t.Parallel() testCtx := setup(t) sa := &mockSA{} ca, err := NewCertificateAuthorityImpl( sa, mockSCTService{}, testCtx.pa, testCtx.boulderIssuers, testCtx.certProfiles, testCtx.serialPrefix, testCtx.maxNames, testCtx.keyPolicy, testCtx.logger, testCtx.metrics, testCtx.fc) test.AssertNotError(t, err, "Failed to create CA") profile := ca.certProfiles["legacy"] issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: "legacy"} precertDER, err := ca.issuePrecertificate(ctx, profile, &issueReq) test.AssertNotError(t, err, "Failed to issue precert") parsedPrecert, err := x509.ParseCertificate(precertDER) test.AssertNotError(t, err, "Failed to parse precert") test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1) test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 0) // Check for poison extension poisonExtension := findExtension(parsedPrecert.Extensions, OIDExtensionCTPoison) test.AssertNotNil(t, poisonExtension, "Couldn't find CTPoison extension") test.AssertEquals(t, poisonExtension.Critical, true) test.AssertDeepEquals(t, poisonExtension.Value, []byte{0x05, 0x00}) // ASN.1 DER NULL sctBytes, err := makeSCTs() if err != nil { t.Fatal(err) } test.AssertNotError(t, err, "Failed to marshal SCT") certDER, err := ca.issueCertificateForPrecertificate(ctx, profile, precertDER, sctBytes, mrand.Int63(), mrand.Int63()) test.AssertNotError(t, err, "Failed to issue cert from precert") parsedCert, err := x509.ParseCertificate(certDER) test.AssertNotError(t, err, "Failed to parse cert") test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 1) // Check for SCT list extension sctListExtension := findExtension(parsedCert.Extensions, OIDExtensionSCTList) test.AssertNotNil(t, sctListExtension, "Couldn't find SCTList extension") test.AssertEquals(t, sctListExtension.Critical, false) var rawValue []byte _, err = asn1.Unmarshal(sctListExtension.Value, &rawValue) test.AssertNotError(t, err, "Failed to unmarshal extension value") sctList, err := deserializeSCTList(rawValue) test.AssertNotError(t, err, "Failed to deserialize SCT list") test.Assert(t, len(sctList) == 1, fmt.Sprintf("Wrong number of SCTs, wanted: 1, got: %d", len(sctList))) } func TestIssueCertificateForPrecertificateWithSpecificCertificateProfile(t *testing.T) { t.Parallel() testCtx := setup(t) sa := &mockSA{} ca, err := NewCertificateAuthorityImpl( sa, mockSCTService{}, testCtx.pa, testCtx.boulderIssuers, testCtx.certProfiles, testCtx.serialPrefix, testCtx.maxNames, testCtx.keyPolicy, testCtx.logger, testCtx.metrics, testCtx.fc) test.AssertNotError(t, err, "Failed to create CA") selectedProfile := "modern" certProfile, ok := ca.certProfiles[selectedProfile] test.Assert(t, ok, "Certificate profile was expected to exist") issueReq := capb.IssueCertificateRequest{ Csr: CNandSANCSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: selectedProfile, } precertDER, err := ca.issuePrecertificate(ctx, certProfile, &issueReq) test.AssertNotError(t, err, "Failed to issue precert") parsedPrecert, err := x509.ParseCertificate(precertDER) test.AssertNotError(t, err, "Failed to parse precert") test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1) test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 0) // Check for poison extension poisonExtension := findExtension(parsedPrecert.Extensions, OIDExtensionCTPoison) test.AssertNotNil(t, poisonExtension, "Couldn't find CTPoison extension") test.AssertEquals(t, poisonExtension.Critical, true) test.AssertDeepEquals(t, poisonExtension.Value, []byte{0x05, 0x00}) // ASN.1 DER NULL sctBytes, err := makeSCTs() if err != nil { t.Fatal(err) } test.AssertNotError(t, err, "Failed to marshal SCT") certDER, err := ca.issueCertificateForPrecertificate(ctx, certProfile, precertDER, sctBytes, mrand.Int63(), mrand.Int63()) test.AssertNotError(t, err, "Failed to issue cert from precert") parsedCert, err := x509.ParseCertificate(certDER) test.AssertNotError(t, err, "Failed to parse cert") test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 1) // Check for SCT list extension sctListExtension := findExtension(parsedCert.Extensions, OIDExtensionSCTList) test.AssertNotNil(t, sctListExtension, "Couldn't find SCTList extension") test.AssertEquals(t, sctListExtension.Critical, false) var rawValue []byte _, err = asn1.Unmarshal(sctListExtension.Value, &rawValue) test.AssertNotError(t, err, "Failed to unmarshal extension value") sctList, err := deserializeSCTList(rawValue) test.AssertNotError(t, err, "Failed to deserialize SCT list") test.Assert(t, len(sctList) == 1, fmt.Sprintf("Wrong number of SCTs, wanted: 1, got: %d", len(sctList))) } // deserializeSCTList deserializes a list of SCTs. // Forked from github.com/cloudflare/cfssl/helpers func deserializeSCTList(serializedSCTList []byte) ([]ct.SignedCertificateTimestamp, error) { var sctList ctx509.SignedCertificateTimestampList rest, err := cttls.Unmarshal(serializedSCTList, &sctList) if err != nil { return nil, err } if len(rest) != 0 { return nil, errors.New("serialized SCT list contained trailing garbage") } list := make([]ct.SignedCertificateTimestamp, len(sctList.SCTList)) for i, serializedSCT := range sctList.SCTList { var sct ct.SignedCertificateTimestamp rest, err := cttls.Unmarshal(serializedSCT.Val, &sct) if err != nil { return nil, err } if len(rest) != 0 { return nil, errors.New("serialized SCT contained trailing garbage") } list[i] = sct } return list, nil } // dupeSA returns a non-error to GetCertificate in order to simulate a request // to issue a final certificate with a duplicate serial. type dupeSA struct { mockSA } func (m *dupeSA) GetCertificate(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) { return nil, nil } // getCertErrorSA always returns an error for GetCertificate type getCertErrorSA struct { mockSA } func (m *getCertErrorSA) GetCertificate(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) { return nil, fmt.Errorf("i don't like it") } func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) { t.Parallel() testCtx := setup(t) sa := &dupeSA{} ca, err := NewCertificateAuthorityImpl( sa, mockSCTService{}, testCtx.pa, testCtx.boulderIssuers, testCtx.certProfiles, testCtx.serialPrefix, testCtx.maxNames, testCtx.keyPolicy, testCtx.logger, testCtx.metrics, testCtx.fc) test.AssertNotError(t, err, "Failed to create CA") sctBytes, err := makeSCTs() if err != nil { t.Fatal(err) } profile := ca.certProfiles["legacy"] issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: "legacy"} precertDER, err := ca.issuePrecertificate(ctx, profile, &issueReq) test.AssertNotError(t, err, "Failed to issue precert") test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1) _, err = ca.issueCertificateForPrecertificate(ctx, profile, precertDER, sctBytes, mrand.Int63(), mrand.Int63()) if err == nil { t.Error("Expected error issuing duplicate serial but got none.") } if !strings.Contains(err.Error(), "issuance of duplicate final certificate requested") { t.Errorf("Wrong type of error issuing duplicate serial. Expected 'issuance of duplicate', got '%s'", err) } // The success metric doesn't increase when a duplicate certificate issuance // is attempted. test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 0) // Now check what happens if there is an error (e.g. timeout) while checking // for the duplicate. errorsa := &getCertErrorSA{} errorca, err := NewCertificateAuthorityImpl( errorsa, mockSCTService{}, testCtx.pa, testCtx.boulderIssuers, testCtx.certProfiles, testCtx.serialPrefix, testCtx.maxNames, testCtx.keyPolicy, testCtx.logger, testCtx.metrics, testCtx.fc) test.AssertNotError(t, err, "Failed to create CA") _, err = errorca.issueCertificateForPrecertificate(ctx, profile, precertDER, sctBytes, mrand.Int63(), mrand.Int63()) if err == nil { t.Fatal("Expected error issuing duplicate serial but got none.") } if !strings.Contains(err.Error(), "error checking for duplicate") { t.Fatalf("Wrong type of error issuing duplicate serial. Expected 'error checking for duplicate', got '%s'", err) } // The success metric doesn't increase when a duplicate certificate issuance // is attempted. test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 0) } func TestGenerateSKID(t *testing.T) { t.Parallel() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "Error generating key") sha256skid, err := generateSKID(key.Public()) test.AssertNotError(t, err, "Error generating SKID") test.AssertEquals(t, len(sha256skid), 20) test.AssertEquals(t, cap(sha256skid), 20) features.Reset() } func TestVerifyTBSCertIsDeterministic(t *testing.T) { t.Parallel() // Create first keypair and cert testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "unable to generate ECDSA private key") template := &x509.Certificate{ NotAfter: time.Now().Add(1 * time.Hour), DNSNames: []string{"example.com"}, SerialNumber: big.NewInt(1), } certDer1, err := x509.CreateCertificate(rand.Reader, template, template, &testKey.PublicKey, testKey) test.AssertNotError(t, err, "unable to create certificate") // Create second keypair and cert testKey2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "unable to generate ECDSA private key") template2 := &x509.Certificate{ NotAfter: time.Now().Add(2 * time.Hour), DNSNames: []string{"example.net"}, SerialNumber: big.NewInt(2), } certDer2, err := x509.CreateCertificate(rand.Reader, template2, template2, &testKey2.PublicKey, testKey2) test.AssertNotError(t, err, "unable to create certificate") testCases := []struct { name string lintCertBytes []byte leafCertBytes []byte errorSubstr string }{ { name: "Both nil", lintCertBytes: nil, leafCertBytes: nil, errorSubstr: "were nil", }, { name: "Missing a value, invalid input", lintCertBytes: nil, leafCertBytes: []byte{0x6, 0x6, 0x6}, errorSubstr: "were nil", }, { name: "Missing a value, valid input", lintCertBytes: nil, leafCertBytes: certDer1, errorSubstr: "were nil", }, { name: "Mismatched bytes, invalid input", lintCertBytes: []byte{0x6, 0x6, 0x6}, leafCertBytes: []byte{0x1, 0x2, 0x3}, errorSubstr: "malformed certificate", }, { name: "Mismatched bytes, invalider input", lintCertBytes: certDer1, leafCertBytes: []byte{0x1, 0x2, 0x3}, errorSubstr: "malformed certificate", }, { // This case is an example of when a linting cert's DER bytes are // mismatched compared to then precert or final cert created from // that linting cert's DER bytes. name: "Mismatched bytes, valid input", lintCertBytes: certDer1, leafCertBytes: certDer2, errorSubstr: "mismatch between", }, { // Take this with a grain of salt since this test is not actually // creating a linting certificate and performing two // x509.CreateCertificate() calls like // ca.IssueCertificateForPrecertificate and // ca.issuePrecertificateInner do. However, we're still going to // verify the equality. name: "Valid", lintCertBytes: certDer1, leafCertBytes: certDer1, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { t.Parallel() err := tbsCertIsDeterministic(testCase.lintCertBytes, testCase.leafCertBytes) if testCase.errorSubstr != "" { test.AssertError(t, err, "your lack of errors is disturbing") test.AssertContains(t, err.Error(), testCase.errorSubstr) } else { test.AssertNotError(t, err, "unexpected error") } }) } }