CA: Require RA to always provide profile name (#7991)

Deprecate the CA's DefaultCertificateProfileName config key, now that
default profile selection is being handled by the RA instead.

Part of https://github.com/letsencrypt/boulder/issues/7986
This commit is contained in:
Aaron Gable 2025-02-11 13:10:29 -08:00 committed by GitHub
parent 0efb2a026d
commit a9e3ad1143
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 63 additions and 112 deletions

View File

@ -87,13 +87,8 @@ type certProfileWithID struct {
}
// 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.
// profile to retrieve the actual profile.
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
}
@ -194,17 +189,11 @@ func makeIssuerMaps(issuers []*issuance.Issuer) (issuerMaps, error) {
// - 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.ProfileConfigNew) (certProfilesMaps, error) {
func makeCertificateProfilesMap(profiles map[string]*issuance.ProfileConfigNew) (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)
}
profilesByName := make(map[string]*certProfileWithID, len(profiles))
profilesByHash := make(map[[32]byte]*certProfileWithID, len(profiles))
@ -230,7 +219,7 @@ func makeCertificateProfilesMap(defaultName string, profiles map[string]*issuanc
profilesByHash[hash] = &withID
}
return certProfilesMaps{defaultName, profilesByHash, profilesByName}, nil
return certProfilesMaps{profilesByHash, profilesByName}, nil
}
// NewCertificateAuthorityImpl creates a CA instance that can sign certificates
@ -240,7 +229,6 @@ func NewCertificateAuthorityImpl(
sa sapb.StorageAuthorityCertificateClient,
pa core.PolicyAuthority,
boulderIssuers []*issuance.Issuer,
defaultCertProfileName string,
certificateProfiles map[string]*issuance.ProfileConfigNew,
serialPrefix byte,
maxNames int,
@ -261,7 +249,7 @@ func NewCertificateAuthorityImpl(
return nil, errors.New("must have at least one issuer")
}
certProfiles, err := makeCertificateProfilesMap(defaultCertProfileName, certificateProfiles)
certProfiles, err := makeCertificateProfilesMap(certificateProfiles)
if err != nil {
return nil, err
}
@ -306,18 +294,14 @@ var ocspStatusToCode = map[string]int{
// [issuance cycle]: https://github.com/letsencrypt/boulder/blob/main/docs/ISSUANCE-CYCLE.md
func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, issueReq *capb.IssueCertificateRequest) (*capb.IssuePrecertificateResponse, error) {
// issueReq.orderID may be zero, for ACMEv1 requests.
if core.IsAnyNilOrZero(issueReq, issueReq.Csr, issueReq.RegistrationID) {
if core.IsAnyNilOrZero(issueReq, issueReq.Csr, issueReq.RegistrationID, issueReq.CertProfileName) {
return nil, berrors.InternalServerError("Incomplete issue certificate request")
}
// 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
}
// profile name. We check the name here, because the RA is not able to
// precompute profile hashes. All issuance requests must come with a profile
// name, and the RA handles selecting the default.
certProfile, ok := ca.certProfiles.profileByName[issueReq.CertProfileName]
if !ok {
return nil, fmt.Errorf("the CA is incapable of using a profile named %s", issueReq.CertProfileName)

View File

@ -98,18 +98,17 @@ func mustRead(path string) []byte {
}
type testCtx struct {
pa core.PolicyAuthority
ocsp *ocspImpl
crl *crlImpl
defaultCertProfileName string
certProfiles map[string]*issuance.ProfileConfigNew
serialPrefix byte
maxNames int
boulderIssuers []*issuance.Issuer
keyPolicy goodkey.KeyPolicy
fc clock.FakeClock
metrics *caMetrics
logger *blog.Mock
pa core.PolicyAuthority
ocsp *ocspImpl
crl *crlImpl
certProfiles map[string]*issuance.ProfileConfigNew
serialPrefix byte
maxNames int
boulderIssuers []*issuance.Issuer
keyPolicy goodkey.KeyPolicy
fc clock.FakeClock
metrics *caMetrics
logger *blog.Mock
}
type mockSA struct {
@ -232,18 +231,17 @@ func setup(t *testing.T) *testCtx {
test.AssertNotError(t, err, "Failed to create crl impl")
return &testCtx{
pa: pa,
ocsp: ocsp,
crl: crl,
defaultCertProfileName: "legacy",
certProfiles: certProfiles,
serialPrefix: 0x11,
maxNames: 2,
boulderIssuers: boulderIssuers,
keyPolicy: keyPolicy,
fc: fc,
metrics: cametrics,
logger: blog.NewMock(),
pa: pa,
ocsp: ocsp,
crl: crl,
certProfiles: certProfiles,
serialPrefix: 0x11,
maxNames: 2,
boulderIssuers: boulderIssuers,
keyPolicy: keyPolicy,
fc: fc,
metrics: cametrics,
logger: blog.NewMock(),
}
}
@ -255,7 +253,6 @@ func TestSerialPrefix(t *testing.T) {
nil,
nil,
nil,
"",
nil,
0x00,
testCtx.maxNames,
@ -269,7 +266,6 @@ func TestSerialPrefix(t *testing.T) {
nil,
nil,
nil,
"",
nil,
0x80,
testCtx.maxNames,
@ -328,7 +324,7 @@ func TestIssuePrecertificate(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}
issueReq := &capb.IssueCertificateRequest{Csr: testCase.csr, RegistrationID: arbitraryRegID, CertProfileName: "legacy"}
var certDER []byte
response, err := ca.IssuePrecertificate(ctx, issueReq)
@ -365,7 +361,6 @@ func issueCertificateSubTestSetup(t *testing.T) (*certificateAuthorityImpl, *moc
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -404,7 +399,6 @@ func TestNoIssuers(t *testing.T) {
sa,
testCtx.pa,
nil, // No issuers
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -425,7 +419,6 @@ func TestMultipleIssuers(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -435,12 +428,8 @@ 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 an RSA issuer.
issuedCert, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, CertProfileName: selectedProfile})
issuedCert, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, CertProfileName: "legacy"})
test.AssertNotError(t, err, "Failed to issue certificate")
cert, err := x509.ParseCertificate(issuedCert.DER)
test.AssertNotError(t, err, "Certificate failed to parse")
@ -456,7 +445,7 @@ func TestMultipleIssuers(t *testing.T) {
test.AssertMetricWithLabelsEquals(t, ca.metrics.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})
issuedCert, err = ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID, CertProfileName: "legacy"})
test.AssertNotError(t, err, "Failed to issue certificate")
cert, err = x509.ParseCertificate(issuedCert.DER)
test.AssertNotError(t, err, "Certificate failed to parse")
@ -499,7 +488,6 @@ func TestUnpredictableIssuance(t *testing.T) {
sa,
testCtx.pa,
boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -521,7 +509,7 @@ func TestUnpredictableIssuance(t *testing.T) {
// 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}
req := &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID, CertProfileName: "legacy"}
seenE2 := false
seenR3 := false
for i := 0; i < 20; i++ {
@ -559,7 +547,6 @@ func TestMakeCertificateProfilesMap(t *testing.T) {
testCases := []struct {
name string
defaultName string
profileConfigs map[string]*issuance.ProfileConfigNew
expectedErrSubstr string
expectedProfiles []nameToHash
@ -575,16 +562,7 @@ func TestMakeCertificateProfilesMap(t *testing.T) {
expectedErrSubstr: "at least one certificate profile",
},
{
name: "no profile matching default name",
defaultName: "default",
profileConfigs: map[string]*issuance.ProfileConfigNew{
"notDefault": &testProfile,
},
expectedErrSubstr: "profile object was not found for that name",
},
{
name: "duplicate hash",
defaultName: "default",
name: "duplicate hash",
profileConfigs: map[string]*issuance.ProfileConfigNew{
"default": &testProfile,
"default2": &testProfile,
@ -592,8 +570,7 @@ func TestMakeCertificateProfilesMap(t *testing.T) {
expectedErrSubstr: "duplicate certificate profile hash",
},
{
name: "empty profile config",
defaultName: "empty",
name: "empty profile config",
profileConfigs: map[string]*issuance.ProfileConfigNew{
"empty": {},
},
@ -606,7 +583,6 @@ func TestMakeCertificateProfilesMap(t *testing.T) {
},
{
name: "default profiles from setup func",
defaultName: testCtx.defaultCertProfileName,
profileConfigs: testCtx.certProfiles,
expectedProfiles: []nameToHash{
{
@ -624,7 +600,7 @@ func TestMakeCertificateProfilesMap(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
profiles, err := makeCertificateProfilesMap(tc.defaultName, tc.profileConfigs)
profiles, err := makeCertificateProfilesMap(tc.profileConfigs)
if tc.expectedErrSubstr != "" {
test.AssertError(t, err, "profile construction should have failed")
@ -704,7 +680,6 @@ func TestInvalidCSRs(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -717,7 +692,7 @@ func TestInvalidCSRs(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
serializedCSR := mustRead(testCase.csrPath)
issueReq := &capb.IssueCertificateRequest{Csr: serializedCSR, RegistrationID: arbitraryRegID}
issueReq := &capb.IssueCertificateRequest{Csr: serializedCSR, RegistrationID: arbitraryRegID, CertProfileName: "legacy"}
_, err = ca.IssuePrecertificate(ctx, issueReq)
test.AssertErrorIs(t, err, testCase.errorType)
@ -743,7 +718,6 @@ func TestRejectValidityTooLong(t *testing.T) {
&mockSA{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -754,7 +728,7 @@ func TestRejectValidityTooLong(t *testing.T) {
test.AssertNotError(t, err, "Failed to create CA")
// Test that the CA rejects CSRs that would expire after the intermediate cert
_, err = ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID})
_, err = ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, CertProfileName: "legacy"})
test.AssertError(t, err, "Cannot issue a certificate that expires after the intermediate certificate")
test.AssertErrorIs(t, err, berrors.InternalServer)
}
@ -836,7 +810,6 @@ func TestIssueCertificateForPrecertificate(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -846,10 +819,7 @@ 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}
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, OrderID: 0, CertProfileName: "legacy"}
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
test.AssertNotError(t, err, "Failed to issue precert")
parsedPrecert, err := x509.ParseCertificate(precert.DER)
@ -901,7 +871,6 @@ func TestIssueCertificateForPrecertificateWithSpecificCertificateProfile(t *test
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -911,7 +880,7 @@ func TestIssueCertificateForPrecertificateWithSpecificCertificateProfile(t *test
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
selectedProfile := "legacy"
selectedProfile := "modern"
certProfile, ok := ca.certProfiles.profileByName[selectedProfile]
test.Assert(t, ok, "Certificate profile was expected to exist")
@ -1017,7 +986,6 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -1032,11 +1000,7 @@ 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}
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, OrderID: 0, CertProfileName: "legacy"}
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
test.AssertNotError(t, err, "Failed to issue precert")
test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
@ -1045,7 +1009,7 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
SCTs: sctBytes,
RegistrationID: arbitraryRegID,
OrderID: 0,
CertProfileHash: certProfile.hash[:],
CertProfileHash: ca.certProfiles.profileByName["legacy"].hash[:],
})
if err == nil {
t.Error("Expected error issuing duplicate serial but got none.")
@ -1064,7 +1028,6 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
errorsa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -1079,7 +1042,7 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
SCTs: sctBytes,
RegistrationID: arbitraryRegID,
OrderID: 0,
CertProfileHash: certProfile.hash[:],
CertProfileHash: ca.certProfiles.profileByName["legacy"].hash[:],
})
if err == nil {
t.Fatal("Expected error issuing duplicate serial but got none.")

View File

@ -33,7 +33,6 @@ func TestOCSP(t *testing.T) {
&mockSA{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -46,7 +45,7 @@ func TestOCSP(t *testing.T) {
// Issue a certificate from an RSA issuer, request OCSP from the same issuer,
// and make sure it works.
rsaCertPB, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID})
rsaCertPB, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, CertProfileName: "legacy"})
test.AssertNotError(t, err, "Failed to issue certificate")
rsaCert, err := x509.ParseCertificate(rsaCertPB.DER)
test.AssertNotError(t, err, "Failed to parse rsaCert")
@ -69,7 +68,7 @@ func TestOCSP(t *testing.T) {
// Issue a certificate from an ECDSA issuer, request OCSP from the same issuer,
// and make sure it works.
ecdsaCertPB, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID})
ecdsaCertPB, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID, CertProfileName: "legacy"})
test.AssertNotError(t, err, "Failed to issue certificate")
ecdsaCert, err := x509.ParseCertificate(ecdsaCertPB.DER)
test.AssertNotError(t, err, "Failed to parse ecdsaCert")

View File

@ -37,10 +37,12 @@ type Config struct {
// 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.
//
// Deprecated: set the defaultProfileName in the RA config instead.
DefaultCertificateProfileName string `validate:"omitempty,alphanum,min=1,max=32"`
// One of the profile names must match the value of
// DefaultCertificateProfileName or boulder-ca will fail to start.
// One of the profile names must match the value of ra.defaultProfileName
// or large amounts of issuance will fail.
CertProfiles map[string]*issuance.ProfileConfigNew `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.
@ -194,11 +196,6 @@ func main() {
logger.Infof("Loaded issuer: name=[%s] keytype=[%s] nameID=[%v] isActive=[%t]", issuer.Name(), issuer.KeyType(), issuer.NameID(), issuer.IsActive())
}
if c.CA.Issuance.DefaultCertificateProfileName == "" {
c.CA.Issuance.DefaultCertificateProfileName = "defaultBoulderCertificateProfile"
}
logger.Infof("Configured default certificate profile name set to: %s", c.CA.Issuance.DefaultCertificateProfileName)
if len(c.CA.Issuance.CertProfiles) == 0 {
cmd.Fail("At least one profile must be configured")
}
@ -251,7 +248,6 @@ func main() {
sa,
pa,
issuers,
c.CA.Issuance.DefaultCertificateProfileName,
c.CA.Issuance.CertProfiles,
serialPrefix,
c.CA.MaxNames,

View File

@ -42,7 +42,6 @@
"hostOverride": "sa.boulder"
},
"issuance": {
"defaultCertificateProfileName": "legacy",
"certProfiles": {
"legacy": {
"allowMustStaple": true,

View File

@ -29,12 +29,9 @@
"debugAddr": ":8002",
"hostnamePolicyFile": "test/hostname-policy.yaml",
"maxNames": 100,
"authorizationLifetimeDays": 30,
"pendingAuthorizationLifetimeDays": 7,
"goodkey": {
"fermatRounds": 100
},
"orderLifetime": "168h",
"issuerCerts": [
"test/certs/webpki/int-rsa-a.cert.pem",
"test/certs/webpki/int-rsa-b.cert.pem",
@ -43,6 +40,19 @@
"test/certs/webpki/int-ecdsa-b.cert.pem",
"test/certs/webpki/int-ecdsa-c.cert.pem"
],
"validationProfiles": {
"legacy": {
"pendingAuthzLifetime": "168h",
"validAuthzLifetime": "720h",
"orderLifetime": "168h"
},
"modern": {
"pendingAuthzLifetime": "7h",
"validAuthzLifetime": "7h",
"orderLifetime": "7h"
}
},
"defaultProfileName": "legacy",
"tls": {
"caCertFile": "test/certs/ipki/minica.pem",
"certFile": "test/certs/ipki/ra.boulder/cert.pem",