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:
parent
a88bd68ead
commit
1e1f6ff254
186
ca/ca.go
186
ca/ca.go
|
@ -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
|
||||
}
|
||||
|
|
456
ca/ca_test.go
456
ca/ca_test.go
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
package notmain
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue