CA: Load multiple certificate profiles (#7325)

This change introduces a new config key `certProfiles` which contains a
map of `profiles`. Only one of `profile` or `certProfiles` should be
used, because configuring both will result in the CA erroring and
shutting down. Further, the singular `profile` is now
[deprecated](https://github.com/letsencrypt/boulder/issues/7414).

The CA pre-computes several maps at startup; 
* A human-readable name to a `*issuance.Profile` which is referred to as
"name".
* A SHA-256 sum over the entire contents of the given profile to the
`*issuance.Profile`. We'll refer to this as "hash".

Internally, CA methods no longer pass an `*issuance.Profile`, instead
they pass a structure containing maps of certificate profile
identifiers. To determine the default profile used by the CA, a new
config field `defaultCertificateProfileName` has been added to the
Issuance struct. Absence of `defaultCertificateProfileName` will cause
the CA to use the default value of `defaultBoulderCertificateProfile`
such as for the the deprecated `profile`. The key for each given
certificate profile will be used as the "name". Duplicate names or
hashes will cause the CA to error during initialization and shutdown.

When the RA calls `ra.CA.IssuePrecertificate`, it will pass an arbitrary
certificate profile name to the CA triggering the CA to lookup if the
name exists in its internal mapping. The RA maintains no state or
knowledge of configured certificate profiles and relies on the CA to
provide this information. If the name exists in the CA's map, it will
return the hash along with the precertificate bytes in a
`capb.IssuePrecertificateResponse`. The RA will then call
`ra.CA.IssueCertificateForPrecertificate` with that same hash. The CA
will lookup the hash to determine if it exists in its map, and if so
will continue on with certificate issuance.

Precertificate and certificate issuance audit logs will now include the
certificate profile name and hex representation of the hash that they
were issued with.

Fixes https://github.com/letsencrypt/boulder/issues/6966

There are no required config or SQL changes.
This commit is contained in:
Phil Porada 2024-04-08 12:52:46 -04:00 committed by GitHub
parent a88bd68ead
commit 1e1f6ff254
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 574 additions and 139 deletions

186
ca/ca.go
View File

@ -1,6 +1,7 @@
package ca
import (
"bytes"
"context"
"crypto"
"crypto/rand"
@ -8,6 +9,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/gob"
"encoding/hex"
"errors"
"fmt"
@ -52,14 +54,35 @@ type issuerMaps struct {
byNameID map[issuance.NameID]*issuance.Issuer
}
type certProfileWithID struct {
// name is a human readable name used to refer to the certificate profile.
name string
// hash is SHA256 sum over every exported field of an issuance.ProfileConfig
// used to generate the embedded *issuance.Profile.
hash [32]byte
profile *issuance.Profile
}
// certProfilesMaps allows looking up the human-readable name of a certificate
// profile to retrieve the actual profile. The default profile to be used is
// stored alongside the maps.
type certProfilesMaps struct {
// The name of the profile that will be selected if no explicit profile name
// is provided via gRPC.
defaultName string
profileByHash map[[32]byte]*certProfileWithID
profileByName map[string]*certProfileWithID
}
// certificateAuthorityImpl represents a CA that signs certificates.
// It can sign OCSP responses as well, but only via delegation to an ocspImpl.
type certificateAuthorityImpl struct {
capb.UnimplementedCertificateAuthorityServer
sa sapb.StorageAuthorityCertificateClient
pa core.PolicyAuthority
issuers issuerMaps
profile *issuance.Profile
sa sapb.StorageAuthorityCertificateClient
pa core.PolicyAuthority
issuers issuerMaps
certProfiles certProfilesMaps
// This is temporary, and will be used for testing and slow roll-out
// of ECDSA issuance, but will then be removed.
@ -96,6 +119,78 @@ func makeIssuerMaps(issuers []*issuance.Issuer) issuerMaps {
return issuerMaps{issuersByAlg, issuersByNameID}
}
// makeCertificateProfilesMap processes a list of certificate issuance profile
// configs and an option slice of zlint lint names to ignore into a set of
// pre-computed maps: 1) a human-readable name to the profile and 2) a unique
// hash over contents of the profile to the profile itself. It returns the maps
// or an error if a duplicate name or hash is found.
//
// The unique hash is used in the case of
// - RA instructs CA1 to issue a precertificate
// - CA1 returns the precertificate DER bytes and profile hash to the RA
// - RA instructs CA2 to issue a final certificate, but CA2 does not contain a
// profile corresponding to that hash and an issuance is prevented.
func makeCertificateProfilesMap(defaultName string, profiles map[string]issuance.ProfileConfig, ignoredLints []string) (certProfilesMaps, error) {
if len(profiles) <= 0 {
return certProfilesMaps{}, fmt.Errorf("must pass at least one certificate profile")
}
// Check that a profile exists with the configured default profile name.
_, ok := profiles[defaultName]
if !ok {
return certProfilesMaps{}, fmt.Errorf("defaultCertificateProfileName:\"%s\" was configured, but a profile object was not found for that name", defaultName)
}
profileByName := make(map[string]*certProfileWithID, len(profiles))
profileByHash := make(map[[32]byte]*certProfileWithID, len(profiles))
for name, profileConfig := range profiles {
profile, err := issuance.NewProfile(profileConfig, ignoredLints)
if err != nil {
return certProfilesMaps{}, err
}
// gob can only encode exported fields, of which an issuance.Profile has
// none. However, since we're already in a loop iteration having access
// to the issuance.ProfileConfig used to generate the issuance.Profile,
// we'll generate the hash from that.
var encodedProfile bytes.Buffer
enc := gob.NewEncoder(&encodedProfile)
err = enc.Encode(profileConfig)
if err != nil {
return certProfilesMaps{}, err
}
if len(encodedProfile.Bytes()) <= 0 {
return certProfilesMaps{}, fmt.Errorf("certificate profile encoding returned 0 bytes")
}
hash := sha256.Sum256(encodedProfile.Bytes())
_, ok := profileByName[name]
if !ok {
profileByName[name] = &certProfileWithID{
name: name,
hash: hash,
profile: profile,
}
} else {
return certProfilesMaps{}, fmt.Errorf("duplicate certificate profile name %s", name)
}
_, ok = profileByHash[hash]
if !ok {
profileByHash[hash] = &certProfileWithID{
name: name,
hash: hash,
profile: profile,
}
} else {
return certProfilesMaps{}, fmt.Errorf("duplicate certificate profile hash %d", hash)
}
}
return certProfilesMaps{defaultName, profileByHash, profileByName}, nil
}
// NewCertificateAuthorityImpl creates a CA instance that can sign certificates
// from any number of issuance.Issuers according to their profiles, and can sign
// OCSP (via delegation to an ocspImpl and its issuers).
@ -103,7 +198,9 @@ func NewCertificateAuthorityImpl(
sa sapb.StorageAuthorityCertificateClient,
pa core.PolicyAuthority,
boulderIssuers []*issuance.Issuer,
certificateProfile *issuance.Profile,
defaultCertProfileName string,
ignoredCertProfileLints []string,
certificateProfiles map[string]issuance.ProfileConfig,
ecdsaAllowList *ECDSAAllowList,
certExpiry time.Duration,
certBackdate time.Duration,
@ -135,8 +232,9 @@ func NewCertificateAuthorityImpl(
return nil, errors.New("must have at least one issuer")
}
if certificateProfile == nil {
return nil, errors.New("must have at least one certificate profile")
certProfiles, err := makeCertificateProfilesMap(defaultCertProfileName, certificateProfiles, ignoredCertProfileLints)
if err != nil {
return nil, err
}
issuers := makeIssuerMaps(boulderIssuers)
@ -152,7 +250,7 @@ func NewCertificateAuthorityImpl(
sa: sa,
pa: pa,
issuers: issuers,
profile: certificateProfile,
certProfiles: certProfiles,
validityPeriod: certExpiry,
backdate: certBackdate,
prefix: serialPrefix,
@ -185,6 +283,8 @@ var ocspStatusToCode = map[string]int{
func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, issueReq *capb.IssueCertificateRequest) (*capb.IssuePrecertificateResponse, error) {
// issueReq.orderID may be zero, for ACMEv1 requests.
// issueReq.CertProfileName may be empty and will be populated in
// issuePrecertificateInner if so.
if core.IsAnyNilOrZero(issueReq, issueReq.Csr, issueReq.RegistrationID) {
return nil, berrors.InternalServerError("Incomplete issue certificate request")
}
@ -206,7 +306,7 @@ func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, iss
return nil, err
}
precertDER, _, err := ca.issuePrecertificateInner(ctx, issueReq, serialBigInt, validity)
precertDER, certProfileHash, err := ca.issuePrecertificateInner(ctx, issueReq, serialBigInt, validity)
if err != nil {
return nil, err
}
@ -217,7 +317,8 @@ func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, iss
}
return &capb.IssuePrecertificateResponse{
DER: precertDER,
DER: precertDER,
CertProfileHash: certProfileHash,
}, nil
}
@ -245,10 +346,19 @@ func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, iss
// serial number at the same time.
func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx context.Context, req *capb.IssueCertificateForPrecertificateRequest) (*corepb.Certificate, error) {
// issueReq.orderID may be zero, for ACMEv1 requests.
if core.IsAnyNilOrZero(req, req.DER, req.SCTs, req.RegistrationID) {
if core.IsAnyNilOrZero(req, req.DER, req.SCTs, req.RegistrationID, req.CertProfileHash) {
return nil, berrors.InternalServerError("Incomplete cert for precertificate request")
}
// The certificate profile hash is checked here instead of the name because
// the hash is over the entire contents of a *ProfileConfig giving assurance
// that the certificate profile has remained unchanged during the roundtrip
// from a CA, to the RA, then back to a (potentially different) CA node.
certProfile, ok := ca.certProfiles.profileByHash[[32]byte(req.CertProfileHash)]
if !ok {
return nil, fmt.Errorf("the CA is incapable of using a profile with hash %d", req.CertProfileHash)
}
precert, err := x509.ParseCertificate(req.DER)
if err != nil {
return nil, err
@ -283,28 +393,27 @@ func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
}
names := strings.Join(issuanceReq.DNSNames, ", ")
ca.log.AuditInfof("Signing cert: serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] precert=[%s]",
serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, hex.EncodeToString(precert.Raw))
ca.log.AuditInfof("Signing cert: serial=[%s] regID=[%d] names=[%s] precert=[%s]",
serialHex, req.RegistrationID, names, hex.EncodeToString(precert.Raw))
_, issuanceToken, err := issuer.Prepare(ca.profile, issuanceReq)
_, issuanceToken, err := issuer.Prepare(certProfile.profile, issuanceReq)
if err != nil {
ca.log.AuditErrf("Preparing cert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]",
serialHex, req.RegistrationID, names, err)
ca.log.AuditErrf("Preparing cert failed: serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, err)
return nil, berrors.InternalServerError("failed to prepare certificate signing: %s", err)
}
certDER, err := issuer.Issue(issuanceToken)
if err != nil {
ca.noteSignError(err)
ca.log.AuditErrf("Signing cert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]",
serialHex, req.RegistrationID, names, err)
ca.log.AuditErrf("Signing cert failed: serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, err)
return nil, berrors.InternalServerError("failed to sign certificate: %s", err)
}
ca.signatureCount.With(prometheus.Labels{"purpose": string(certType), "issuer": issuer.Name()}).Inc()
ca.log.AuditInfof("Signing cert success: serial=[%s] regID=[%d] names=[%s] certificate=[%s]",
serialHex, req.RegistrationID, names, hex.EncodeToString(certDER))
ca.log.AuditInfof("Signing cert success: serial=[%s] regID=[%d] names=[%s] certificate=[%s] certProfileName=[%s] certProfileHash=[%x]",
serialHex, req.RegistrationID, names, hex.EncodeToString(certDER), certProfile.name, certProfile.hash)
_, err = ca.sa.AddCertificate(ctx, &sapb.AddCertificateRequest{
Der: certDER,
@ -312,8 +421,8 @@ func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
Issued: timestamppb.New(ca.clk.Now()),
})
if err != nil {
ca.log.AuditErrf("Failed RPC to store at SA: serial=[%s], cert=[%s], issuerID=[%d], regID=[%d], orderID=[%d], err=[%v]",
serialHex, hex.EncodeToString(certDER), issuer.NameID(), req.RegistrationID, req.OrderID, err)
ca.log.AuditErrf("Failed RPC to store at SA: serial=[%s] cert=[%s] issuerID=[%d] regID=[%d] orderID=[%d] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
serialHex, hex.EncodeToString(certDER), issuer.NameID(), req.RegistrationID, req.OrderID, certProfile.name, certProfile.hash, err)
return nil, err
}
@ -378,7 +487,20 @@ func generateSKID(pk crypto.PublicKey) ([]byte, error) {
return skid[0:20:20], nil
}
func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context, issueReq *capb.IssueCertificateRequest, serialBigInt *big.Int, validity validity) ([]byte, *issuance.Issuer, error) {
func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context, issueReq *capb.IssueCertificateRequest, serialBigInt *big.Int, validity validity) ([]byte, []byte, error) {
// The CA must check if it is capable of issuing for the given certificate
// profile name. The name is checked here instead of the hash because the RA
// is unaware of what certificate profiles exist. Pre-existing orders stored
// in the database may not have an associated certificate profile name and
// will take the default name stored alongside the map.
if issueReq.CertProfileName == "" {
issueReq.CertProfileName = ca.certProfiles.defaultName
}
certProfile, ok := ca.certProfiles.profileByName[issueReq.CertProfileName]
if !ok {
return nil, nil, fmt.Errorf("the CA is incapable of using a profile named %s", issueReq.CertProfileName)
}
csr, err := x509.ParseCertificateRequest(issueReq.Csr)
if err != nil {
return nil, nil, err
@ -433,10 +555,10 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
NotAfter: validity.NotAfter,
}
lintCertBytes, issuanceToken, err := issuer.Prepare(ca.profile, req)
lintCertBytes, issuanceToken, err := issuer.Prepare(certProfile.profile, req)
if err != nil {
ca.log.AuditErrf("Preparing precert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]",
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), err)
ca.log.AuditErrf("Preparing precert failed: serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), certProfile.name, certProfile.hash, err)
if errors.Is(err, linter.ErrLinting) {
ca.lintErrorCount.Inc()
}
@ -457,14 +579,14 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
certDER, err := issuer.Issue(issuanceToken)
if err != nil {
ca.noteSignError(err)
ca.log.AuditErrf("Signing precert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]",
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), err)
ca.log.AuditErrf("Signing precert failed: serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), certProfile.name, certProfile.hash, err)
return nil, nil, berrors.InternalServerError("failed to sign precertificate: %s", err)
}
ca.signatureCount.With(prometheus.Labels{"purpose": string(precertType), "issuer": issuer.Name()}).Inc()
ca.log.AuditInfof("Signing precert success: serial=[%s] regID=[%d] names=[%s] precertificate=[%s]",
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(certDER))
ca.log.AuditInfof("Signing precert success: serial=[%s] regID=[%d] names=[%s] precertificate=[%s] certProfileName=[%s] certProfileHash=[%x]",
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(certDER), certProfile.name, certProfile.hash)
return certDER, issuer, nil
return certDER, certProfile.hash[:], nil
}

View File

@ -106,21 +106,23 @@ func mustRead(path string) []byte {
}
type testCtx struct {
pa core.PolicyAuthority
ocsp *ocspImpl
crl *crlImpl
profile *issuance.Profile
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
pa core.PolicyAuthority
ocsp *ocspImpl
crl *crlImpl
defaultCertProfileName string
ignoredCertProfileLints []string
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 {
@ -164,21 +166,30 @@ func setup(t *testing.T) *testCtx {
err = pa.LoadHostnamePolicyFile("../test/hostname-policy.yaml")
test.AssertNotError(t, err, "Couldn't set hostname policy")
boulderProfile, err := issuance.NewProfile(
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 := 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"},
},
[]string{"w_subject_common_name_included"},
)
test.AssertNotError(t, err, "Couldn't create test profile")
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)
ecdsaOnlyIssuer, err := issuance.LoadIssuer(issuance.IssuerConfig{
UseForRSALeaves: false,
@ -245,21 +256,23 @@ func setup(t *testing.T) *testCtx {
test.AssertNotError(t, err, "Failed to create crl impl")
return &testCtx{
pa: pa,
ocsp: ocsp,
crl: crl,
profile: boulderProfile,
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(),
pa: pa,
ocsp: ocsp,
crl: crl,
defaultCertProfileName: "defaultBoulderCertificateProfile",
ignoredCertProfileLints: []string{"w_subject_common_name_included"},
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(),
}
}
@ -270,6 +283,8 @@ func TestSerialPrefix(t *testing.T) {
nil,
nil,
nil,
"",
nil,
nil,
nil,
testCtx.certExpiry,
@ -288,6 +303,8 @@ func TestSerialPrefix(t *testing.T) {
nil,
nil,
nil,
"",
nil,
nil,
nil,
testCtx.certExpiry,
@ -334,11 +351,9 @@ func TestIssuePrecertificate(t *testing.T) {
// "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) {
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
@ -349,7 +364,6 @@ func TestIssuePrecertificate(t *testing.T) {
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 {
@ -382,7 +396,9 @@ func issueCertificateSubTestSetup(t *testing.T, e *ECDSAAllowList) (*certificate
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles,
e,
testCtx.certExpiry,
testCtx.certBackdate,
@ -395,6 +411,7 @@ func issueCertificateSubTestSetup(t *testing.T, e *ECDSAAllowList) (*certificate
testCtx.signErrorCount,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
return ca, sa
}
@ -428,7 +445,9 @@ func TestNoIssuers(t *testing.T) {
sa,
testCtx.pa,
nil, // No issuers
testCtx.profile,
testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -452,7 +471,9 @@ func TestMultipleIssuers(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -466,44 +487,195 @@ func TestMultipleIssuers(t *testing.T) {
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 the RSA issuer.
issuedCert, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID})
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")
err = cert.CheckSignatureFrom(testCtx.boulderIssuers[1].Cert.Certificate)
test.AssertNotError(t, err, "Certificate failed signature validation")
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
// Test that an ECDSA CSR gets issuance from the ECDSA issuer.
issuedCert, err = ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID})
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")
err = cert.CheckSignatureFrom(testCtx.boulderIssuers[0].Cert.Certificate)
test.AssertNotError(t, err, "Certificate failed signature validation")
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 2)
}
func TestNoProfile(t *testing.T) {
testCtx := setup(t)
func TestProfiles(t *testing.T) {
ctx := setup(t)
test.AssertEquals(t, len(ctx.certProfiles), 2)
sa := &mockSA{}
_, err := NewCertificateAuthorityImpl(
sa,
testCtx.pa,
testCtx.boulderIssuers,
nil, // no profile
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 profile found during CA construction.")
test.AssertEquals(t, err.Error(), "must have at least one certificate profile")
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: ctx.certProfiles,
expectedProfiles: []nameToHash{
{
name: ctx.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 {
// This is handled by boulder-ca, not the CA package.
if tc.defaultName == "" {
tc.defaultName = ctx.defaultCertProfileName
}
t.Run(tc.name, func(t *testing.T) {
tCA, err := NewCertificateAuthorityImpl(
sa,
ctx.pa,
ctx.boulderIssuers,
tc.defaultName,
ctx.ignoredCertProfileLints,
tc.profileConfigs,
nil,
ctx.certExpiry,
ctx.certBackdate,
ctx.serialPrefix,
ctx.maxNames,
ctx.keyPolicy,
ctx.logger,
ctx.stats,
ctx.signatureCount,
ctx.signErrorCount,
ctx.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) {
@ -597,7 +769,9 @@ func TestInvalidCSRs(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -634,7 +808,9 @@ func TestRejectValidityTooLong(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -735,7 +911,9 @@ func TestIssueCertificateForPrecertificate(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -749,11 +927,16 @@ func TestIssueCertificateForPrecertificate(t *testing.T) {
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)
@ -768,14 +951,92 @@ func TestIssueCertificateForPrecertificate(t *testing.T) {
test.AssertNotError(t, err, "Failed to marshal SCT")
cert, err := ca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
DER: precert.DER,
SCTs: sctBytes,
RegistrationID: arbitraryRegID,
OrderID: 0,
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) {
testCtx := setup(t)
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles,
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)
@ -841,7 +1102,9 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -860,14 +1123,20 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
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,
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.")
@ -875,6 +1144,9 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
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.
@ -883,7 +1155,9 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
errorsa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -898,10 +1172,11 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
test.AssertNotError(t, err, "Failed to create CA")
_, err = errorca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
DER: precert.DER,
SCTs: sctBytes,
RegistrationID: arbitraryRegID,
OrderID: 0,
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.")
@ -909,6 +1184,9 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
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) {

View File

@ -35,7 +35,9 @@ func TestOCSP(t *testing.T) {
&mockSA{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
testCtx.defaultCertProfileName,
testCtx.ignoredCertProfileLints,
testCtx.certProfiles,
nil,
testCtx.certExpiry,
testCtx.certBackdate,

View File

@ -4,6 +4,7 @@ import (
"context"
"flag"
"os"
"reflect"
"time"
"github.com/prometheus/client_golang/prometheus"
@ -34,7 +35,20 @@ type Config struct {
// Issuance contains all information necessary to load and initialize issuers.
Issuance struct {
Profile issuance.ProfileConfig
// The name of the certificate profile to use if one wasn't provided
// by the RA during NewOrder and Finalize requests. Must match a
// configured certificate profile or boulder-ca will fail to start.
DefaultCertificateProfileName string `validate:"omitempty,alphanum,min=1,max=32"`
// TODO(#7414) Remove this deprecated field.
// Deprecated: Use CertProfiles instead. Profile implicitly takes
// the internal Boulder default value of ca.DefaultCertProfileName.
Profile issuance.ProfileConfig `validate:"required_without=CertProfiles,structonly"`
// One of the profile names must match the value of
// DefaultCertificateProfileName or boulder-ca will fail to start.
CertProfiles map[string]issuance.ProfileConfig `validate:"dive,keys,alphanum,min=1,max=32,endkeys,required_without=Profile,structonly"`
// TODO(#7159): Make this required once all live configs are using it.
CRLProfile issuance.CRLProfileConfig `validate:"-"`
Issuers []issuance.IssuerConfig `validate:"min=1,dive"`
@ -203,8 +217,22 @@ func main() {
issuers = append(issuers, issuer)
}
profile, err := issuance.NewProfile(c.CA.Issuance.Profile, c.CA.Issuance.IgnoredLints)
cmd.FailOnError(err, "Couldn't load issuance profile")
if c.CA.Issuance.DefaultCertificateProfileName == "" {
c.CA.Issuance.DefaultCertificateProfileName = "defaultBoulderCertificateProfile"
}
logger.Infof("Configured default certificate profile name set to: %s", c.CA.Issuance.DefaultCertificateProfileName)
// TODO(#7414) Remove this check.
if !reflect.ValueOf(c.CA.Issuance.Profile).IsZero() && len(c.CA.Issuance.CertProfiles) > 0 {
cmd.Fail("Only one of Issuance.Profile or Issuance.CertProfiles can be configured")
}
// TODO(#7414) Remove this check.
// Use the deprecated Profile as a CertProfiles
if len(c.CA.Issuance.CertProfiles) == 0 {
c.CA.Issuance.CertProfiles = make(map[string]issuance.ProfileConfig, 0)
c.CA.Issuance.CertProfiles[c.CA.Issuance.DefaultCertificateProfileName] = c.CA.Issuance.Profile
}
tlsConfig, err := c.CA.TLS.Load(scope)
cmd.FailOnError(err, "TLS config")
@ -266,7 +294,9 @@ func main() {
sa,
pa,
issuers,
profile,
c.CA.Issuance.DefaultCertificateProfileName,
c.CA.Issuance.IgnoredLints,
c.CA.Issuance.CertProfiles,
ecdsaAllowList,
c.CA.Expiry.Duration,
c.CA.Backdate.Duration,

View File

@ -1 +0,0 @@
package notmain

View File

@ -722,6 +722,7 @@ func TestMismatchedProfiles(t *testing.T) {
"e_scts_from_same_operator",
})
test.AssertNotError(t, err, "NewProfile failed")
issuer2, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")

View File

@ -42,18 +42,21 @@
"hostOverride": "sa.boulder"
},
"issuance": {
"profile": {
"allowMustStaple": true,
"allowCTPoison": true,
"allowSCTList": true,
"allowCommonName": true,
"policies": [
{
"oid": "2.23.140.1.2.1"
}
],
"maxValidityPeriod": "7776000s",
"maxValidityBackdate": "1h5m"
"defaultCertificateProfileName": "defaultBoulderCertificateProfile",
"certProfiles": {
"defaultBoulderCertificateProfile": {
"allowMustStaple": true,
"allowCTPoison": true,
"allowSCTList": true,
"allowCommonName": true,
"policies": [
{
"oid": "2.23.140.1.2.1"
}
],
"maxValidityPeriod": "7776000s",
"maxValidityBackdate": "1h5m"
}
},
"crlProfile": {
"validityInterval": "216h",