boulder/ca/ca_test.go

1305 lines
44 KiB
Go

package ca
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
"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/prometheus/client_golang/prometheus"
"github.com/zmap/zlint/v3/lint"
"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"
"github.com/letsencrypt/boulder/linter"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/must"
"github.com/letsencrypt/boulder/policy"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/test"
)
func TestImplementation(t *testing.T) {
t.Parallel()
test.AssertImplementsGRPCServer(t, &certificateAuthorityImpl{}, capb.UnimplementedCertificateAuthorityServer{})
}
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}
)
const arbitraryRegID int64 = 1001
func mustRead(path string) []byte {
return must.Do(os.ReadFile(path))
}
type testCtx struct {
pa core.PolicyAuthority
ocsp *ocspImpl
crl *crlImpl
defaultCertProfileName string
lints lint.Registry
certProfiles map[string]issuance.ProfileConfig
certExpiry time.Duration
certBackdate time.Duration
serialPrefix int
maxNames int
boulderIssuers []*issuance.Issuer
keyPolicy goodkey.KeyPolicy
fc clock.FakeClock
stats prometheus.Registerer
signatureCount *prometheus.CounterVec
signErrorCount *prometheus.CounterVec
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, 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["defaultBoulderCertificateProfile"] = issuance.ProfileConfig{
AllowMustStaple: true,
AllowCTPoison: true,
AllowSCTList: true,
AllowCommonName: true,
Policies: []issuance.PolicyConfig{
{OID: "2.23.140.1.2.1"},
},
MaxValidityPeriod: config.Duration{Duration: time.Hour * 8760},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
}
certProfiles["longerLived"] = issuance.ProfileConfig{
AllowMustStaple: true,
AllowCTPoison: true,
AllowSCTList: true,
AllowCommonName: true,
Policies: []issuance.PolicyConfig{
{OID: "2.23.140.1.2.1"},
},
MaxValidityPeriod: config.Duration{Duration: time.Hour * 8761},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
}
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),
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 := goodkey.KeyPolicy{
AllowRSA: true,
AllowECDSANISTP256: true,
AllowECDSANISTP384: true,
}
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"})
lints, err := linter.NewRegistry([]string{"w_subject_common_name_included"})
test.AssertNotError(t, err, "Failed to create zlint registry")
ocsp, err := NewOCSPImpl(
boulderIssuers,
24*time.Hour,
0,
time.Second,
blog.NewMock(),
metrics.NoopRegisterer,
signatureCount,
signErrorCount,
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},
},
"http://c.boulder.test",
100,
blog.NewMock(),
)
test.AssertNotError(t, err, "Failed to create crl impl")
return &testCtx{
pa: pa,
ocsp: ocsp,
crl: crl,
defaultCertProfileName: "defaultBoulderCertificateProfile",
lints: lints,
certProfiles: certProfiles,
certExpiry: 8760 * time.Hour,
certBackdate: time.Hour,
serialPrefix: 17,
maxNames: 2,
boulderIssuers: boulderIssuers,
keyPolicy: keyPolicy,
fc: fc,
stats: metrics.NoopRegisterer,
signatureCount: signatureCount,
signErrorCount: signErrorCount,
logger: blog.NewMock(),
}
}
func TestSerialPrefix(t *testing.T) {
t.Parallel()
testCtx := setup(t)
_, err := NewCertificateAuthorityImpl(
nil,
nil,
nil,
"",
nil,
nil,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
0,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.stats,
nil,
nil,
testCtx.fc)
test.AssertError(t, err, "CA should have failed with no SerialPrefix")
_, err = NewCertificateAuthorityImpl(
nil,
nil,
nil,
"",
nil,
nil,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
128,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.stats,
nil,
nil,
testCtx.fc)
test.AssertError(t, err, "CA should have failed with too-large SerialPrefix")
}
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},
{"ValidityUsesCAClock", CNandSANCSR, issueCertificateSubTestValidityUsesCAClock},
{"ProfileSelectionRSA", CNandSANCSR, issueCertificateSubTestProfileSelectionRSA},
{"ProfileSelectionECDSA", ECDSACSR, issueCertificateSubTestProfileSelectionECDSA},
{"MustStaple", MustStapleCSR, issueCertificateSubTestMustStaple},
{"UnknownExtension", UnsupportedExtensionCSR, issueCertificateSubTestUnknownExtension},
{"CTPoisonExtension", CTPoisonExtensionCSR, issueCertificateSubTestCTPoisonExtension},
{"CTPoisonExtensionEmpty", CTPoisonExtensionEmptyCSR, issueCertificateSubTestCTPoisonExtension},
}
for _, testCase := range testCases {
// TODO(#7454) Remove this rebinding
testCase := testCase
// 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, nil)
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: arbitraryRegID}
var certDER []byte
response, err := ca.IssuePrecertificate(ctx, issueReq)
test.AssertNotError(t, err, "Failed to issue precertificate")
certDER = response.DER
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)
})
}
}
}
func issueCertificateSubTestSetup(t *testing.T, e *ECDSAAllowList) (*certificateAuthorityImpl, *mockSA) {
testCtx := setup(t)
ecdsaAllowList := &ECDSAAllowList{}
if e == nil {
e = ecdsaAllowList
}
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
e,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.stats,
testCtx.signatureCount,
testCtx.signErrorCount,
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)
}
}
func issueCertificateSubTestValidityUsesCAClock(t *testing.T, i *TestCertificateIssuance) {
test.AssertEquals(t, i.cert.NotBefore, i.ca.clk.Now().Add(-1*i.ca.backdate))
test.AssertEquals(t, i.cert.NotAfter.Add(time.Second).Sub(i.cert.NotBefore), i.ca.validityPeriod)
}
// Test failure mode when no issuers are present.
func TestNoIssuers(t *testing.T) {
t.Parallel()
testCtx := setup(t)
sa := &mockSA{}
_, err := NewCertificateAuthorityImpl(
sa,
testCtx.pa,
nil, // No issuers
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.stats,
testCtx.signatureCount,
testCtx.signErrorCount,
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,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.stats,
testCtx.signatureCount,
testCtx.signErrorCount,
testCtx.fc)
test.AssertNotError(t, err, "Failed to remake CA")
selectedProfile := ca.certProfiles.defaultName
_, ok := ca.certProfiles.profileByName[selectedProfile]
test.Assert(t, ok, "Certificate profile was expected to exist")
// Test that an RSA CSR gets issuance from an RSA issuer.
issuedCert, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, CertProfileName: selectedProfile})
test.AssertNotError(t, err, "Failed to issue certificate")
cert, err := x509.ParseCertificate(issuedCert.DER)
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.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
// Test that an ECDSA CSR gets issuance from an ECDSA issuer.
issuedCert, err = ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID, CertProfileName: selectedProfile})
test.AssertNotError(t, err, "Failed to issue certificate")
cert, err = x509.ParseCertificate(issuedCert.DER)
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.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),
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,
testCtx.pa,
boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.stats,
testCtx.signatureCount,
testCtx.signErrorCount,
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: arbitraryRegID}
seenE2 := false
seenR3 := false
for i := 0; i < 20; i++ {
result, err := ca.IssuePrecertificate(ctx, req)
test.AssertNotError(t, err, "Failed to issue test certificate")
cert, err := x509.ParseCertificate(result.DER)
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 TestProfiles(t *testing.T) {
t.Parallel()
testCtx := setup(t)
test.AssertEquals(t, len(testCtx.certProfiles), 2)
sa := &mockSA{}
duplicateProfiles := make(map[string]issuance.ProfileConfig, 0)
// These profiles contain the same data which will produce an identical
// hash, even though the names are different.
duplicateProfiles["defaultBoulderCertificateProfile"] = issuance.ProfileConfig{
AllowMustStaple: false,
AllowCTPoison: false,
AllowSCTList: false,
AllowCommonName: false,
Policies: []issuance.PolicyConfig{
{OID: "2.23.140.1.2.1"},
},
MaxValidityPeriod: config.Duration{Duration: time.Hour * 8760},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
}
duplicateProfiles["uhoh_ohno"] = issuance.ProfileConfig{
AllowMustStaple: false,
AllowCTPoison: false,
AllowSCTList: false,
AllowCommonName: false,
Policies: []issuance.PolicyConfig{
{OID: "2.23.140.1.2.1"},
},
MaxValidityPeriod: config.Duration{Duration: time.Hour * 8760},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
}
test.AssertEquals(t, len(duplicateProfiles), 2)
jackedProfiles := make(map[string]issuance.ProfileConfig, 0)
jackedProfiles["ruhroh"] = issuance.ProfileConfig{
AllowMustStaple: false,
AllowCTPoison: false,
AllowSCTList: false,
AllowCommonName: false,
Policies: []issuance.PolicyConfig{
{OID: "2.23.140.1.2.1"},
},
MaxValidityPeriod: config.Duration{Duration: time.Hour * 9000},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
}
test.AssertEquals(t, len(jackedProfiles), 1)
type nameToHash struct {
name string
hash [32]byte
}
emptyMap := make(map[string]issuance.ProfileConfig, 0)
testCases := []struct {
name string
profileConfigs map[string]issuance.ProfileConfig
defaultName string
expectedErrSubstr string
expectedProfiles []nameToHash
}{
{
name: "no profiles",
profileConfigs: emptyMap,
expectedErrSubstr: "at least one certificate profile",
},
{
name: "nil profile map",
profileConfigs: nil,
expectedErrSubstr: "at least one certificate profile",
},
{
name: "duplicate hash",
profileConfigs: duplicateProfiles,
expectedErrSubstr: "duplicate certificate profile hash",
},
{
name: "default profiles from setup func",
profileConfigs: testCtx.certProfiles,
expectedProfiles: []nameToHash{
{
name: testCtx.defaultCertProfileName,
hash: [32]byte{205, 182, 88, 236, 32, 18, 154, 120, 148, 194, 42, 215, 117, 140, 13, 169, 127, 196, 219, 67, 82, 36, 147, 67, 254, 117, 65, 112, 202, 60, 185, 9},
},
{
name: "longerLived",
hash: [32]byte{80, 228, 198, 83, 7, 184, 187, 236, 113, 17, 103, 213, 226, 245, 172, 212, 135, 241, 125, 92, 122, 200, 34, 159, 139, 72, 191, 41, 1, 244, 86, 62},
},
},
},
{
name: "no profile matching default name",
profileConfigs: jackedProfiles,
expectedErrSubstr: "profile object was not found for that name",
},
{
name: "certificate profile hash changed mid-issuance",
profileConfigs: jackedProfiles,
defaultName: "ruhroh",
expectedProfiles: []nameToHash{
{
// We'll change the mapped hash key under the hood during
// the test.
name: "ruhroh",
hash: [32]byte{84, 131, 8, 59, 3, 244, 7, 36, 151, 161, 118, 68, 117, 183, 197, 177, 179, 232, 215, 10, 188, 48, 159, 195, 195, 140, 19, 204, 201, 182, 239, 235},
},
},
},
}
for _, tc := range testCases {
// TODO(#7454) Remove this rebinding
tc := tc
// This is handled by boulder-ca, not the CA package.
if tc.defaultName == "" {
tc.defaultName = testCtx.defaultCertProfileName
}
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tCA, err := NewCertificateAuthorityImpl(
sa,
testCtx.pa,
testCtx.boulderIssuers,
tc.defaultName,
tc.profileConfigs,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.stats,
testCtx.signatureCount,
testCtx.signErrorCount,
testCtx.fc,
)
if tc.expectedErrSubstr != "" {
test.AssertContains(t, err.Error(), tc.expectedErrSubstr)
test.AssertError(t, err, "No profile found during CA construction.")
} else {
test.AssertNotError(t, err, "Profiles should exist, but were not found")
}
if tc.expectedProfiles != nil {
test.AssertEquals(t, len(tc.expectedProfiles), len(tCA.certProfiles.profileByName))
}
for _, expected := range tc.expectedProfiles {
cpwid, ok := tCA.certProfiles.profileByName[expected.name]
test.Assert(t, ok, "Profile name was not found, but should have been")
test.AssertEquals(t, expected.hash, cpwid.hash)
if tc.name == "certificate profile hash changed mid-issuance" {
// This is an attempt to simulate the hash changing, but the
// name remaining the same on a CA node in the duration
// between CA1 sending capb.IssuePrecerticateResponse and
// before the RA calls
// capb.IssueCertificateForPrecertificate. We expect the
// receiving CA2 to error that the hash we expect could not
// be found in the map.
originalHash := cpwid.hash
cpwid.hash = [32]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 6}
test.AssertNotEquals(t, originalHash, cpwid.hash)
}
}
})
}
}
func TestECDSAAllowList(t *testing.T) {
t.Parallel()
req := &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID}
// With allowlist containing arbitraryRegID, issuance should come from ECDSA issuer.
regIDMap := makeRegIDsMap([]int64{arbitraryRegID})
ca, _ := issueCertificateSubTestSetup(t, &ECDSAAllowList{regIDMap})
result, err := ca.IssuePrecertificate(ctx, req)
test.AssertNotError(t, err, "Failed to issue certificate")
cert, err := x509.ParseCertificate(result.DER)
test.AssertNotError(t, err, "Certificate failed to parse")
test.AssertEquals(t, cert.SignatureAlgorithm, x509.ECDSAWithSHA384)
// With allowlist not containing arbitraryRegID, issuance should fall back to RSA issuer.
regIDMap = makeRegIDsMap([]int64{2002})
ca, _ = issueCertificateSubTestSetup(t, &ECDSAAllowList{regIDMap})
result, err = ca.IssuePrecertificate(ctx, req)
test.AssertNotError(t, err, "Failed to issue certificate")
cert, err = x509.ParseCertificate(result.DER)
test.AssertNotError(t, err, "Certificate failed to parse")
test.AssertEquals(t, cert.SignatureAlgorithm, x509.SHA256WithRSA)
// With empty allowlist but ECDSAForAll enabled, issuance should come from ECDSA issuer.
ca, _ = issueCertificateSubTestSetup(t, nil)
features.Set(features.Config{ECDSAForAll: true})
defer features.Reset()
result, err = ca.IssuePrecertificate(ctx, req)
test.AssertNotError(t, err, "Failed to issue certificate")
cert, err = x509.ParseCertificate(result.DER)
test.AssertNotError(t, err, "Certificate failed to parse")
test.AssertEquals(t, cert.SignatureAlgorithm, x509.ECDSAWithSHA384)
}
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 Go:
// * Random RSA public key.
// * CN = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com
// * DNSNames = [none]
{"RejectLongCommonName", "./testdata/long_cn.der.csr", nil, "Issued a certificate with a CN over 64 bytes.", 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 {
// TODO(#7454) Remove this rebinding
testCase := testCase
testCtx := setup(t)
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.stats,
testCtx.signatureCount,
testCtx.signErrorCount,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
serializedCSR := mustRead(testCase.csrPath)
issueReq := &capb.IssueCertificateRequest{Csr: serializedCSR, RegistrationID: arbitraryRegID}
_, err = ca.IssuePrecertificate(ctx, issueReq)
test.AssertErrorIs(t, err, testCase.errorType)
test.AssertMetricWithLabelsEquals(t, ca.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)
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.stats,
nil,
nil,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
// This time is a few minutes before the notAfter in testdata/ca_cert.pem
future, err := time.Parse(time.RFC3339, "2025-02-10T00:30:00Z")
test.AssertNotError(t, err, "Failed to parse time")
testCtx.fc.Set(future)
// Test that the CA rejects CSRs that would expire after the intermediate cert
_, err = ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID})
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 countMustStaple(t *testing.T, cert *x509.Certificate) (count int) {
oidTLSFeature := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
mustStapleFeatureValue := []byte{0x30, 0x03, 0x02, 0x01, 0x05}
for _, ext := range cert.Extensions {
if ext.Id.Equal(oidTLSFeature) {
test.Assert(t, !ext.Critical, "Extension was marked critical")
test.AssertByteEquals(t, ext.Value, mustStapleFeatureValue)
count++
}
}
return count
}
func issueCertificateSubTestMustStaple(t *testing.T, i *TestCertificateIssuance) {
test.AssertMetricWithLabelsEquals(t, i.ca.signatureCount, prometheus.Labels{"purpose": "precertificate"}, 1)
test.AssertEquals(t, countMustStaple(t, i.cert), 1)
}
func issueCertificateSubTestUnknownExtension(t *testing.T, i *TestCertificateIssuance) {
test.AssertMetricWithLabelsEquals(t, i.ca.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 := 9
test.AssertEquals(t, len(i.cert.Extensions), expectedExtensionCount)
}
func issueCertificateSubTestCTPoisonExtension(t *testing.T, i *TestCertificateIssuance) {
test.AssertMetricWithLabelsEquals(t, i.ca.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,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.stats,
testCtx.signatureCount,
testCtx.signErrorCount,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
_, ok := ca.certProfiles.profileByName[ca.certProfiles.defaultName]
test.Assert(t, ok, "Certificate profile was expected to exist")
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, OrderID: 0}
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
test.AssertNotError(t, err, "Failed to issue precert")
parsedPrecert, err := x509.ParseCertificate(precert.DER)
test.AssertNotError(t, err, "Failed to parse precert")
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
test.AssertMetricWithLabelsEquals(t, ca.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")
cert, err := ca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
DER: precert.DER,
SCTs: sctBytes,
RegistrationID: arbitraryRegID,
OrderID: 0,
CertProfileHash: precert.CertProfileHash,
})
test.AssertNotError(t, err, "Failed to issue cert from precert")
parsedCert, err := x509.ParseCertificate(cert.Der)
test.AssertNotError(t, err, "Failed to parse cert")
test.AssertMetricWithLabelsEquals(t, ca.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,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.stats,
testCtx.signatureCount,
testCtx.signErrorCount,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
selectedProfile := "longerLived"
certProfile, ok := ca.certProfiles.profileByName[selectedProfile]
test.Assert(t, ok, "Certificate profile was expected to exist")
issueReq := capb.IssueCertificateRequest{
Csr: CNandSANCSR,
RegistrationID: arbitraryRegID,
OrderID: 0,
CertProfileName: selectedProfile,
}
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
test.AssertNotError(t, err, "Failed to issue precert")
parsedPrecert, err := x509.ParseCertificate(precert.DER)
test.AssertNotError(t, err, "Failed to parse precert")
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
test.AssertMetricWithLabelsEquals(t, ca.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")
cert, err := ca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
DER: precert.DER,
SCTs: sctBytes,
RegistrationID: arbitraryRegID,
OrderID: 0,
CertProfileHash: certProfile.hash[:],
})
test.AssertNotError(t, err, "Failed to issue cert from precert")
parsedCert, err := x509.ParseCertificate(cert.Der)
test.AssertNotError(t, err, "Failed to parse cert")
test.AssertMetricWithLabelsEquals(t, ca.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,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.stats,
testCtx.signatureCount,
testCtx.signErrorCount,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
sctBytes, err := makeSCTs()
if err != nil {
t.Fatal(err)
}
selectedProfile := ca.certProfiles.defaultName
certProfile, ok := ca.certProfiles.profileByName[selectedProfile]
test.Assert(t, ok, "Certificate profile was expected to exist")
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, OrderID: 0}
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
test.AssertNotError(t, err, "Failed to issue precert")
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
_, err = ca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
DER: precert.DER,
SCTs: sctBytes,
RegistrationID: arbitraryRegID,
OrderID: 0,
CertProfileHash: certProfile.hash[:],
})
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.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,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.stats,
testCtx.signatureCount,
testCtx.signErrorCount,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
_, err = errorca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
DER: precert.DER,
SCTs: sctBytes,
RegistrationID: arbitraryRegID,
OrderID: 0,
CertProfileHash: certProfile.hash[:],
})
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.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()
}