RA: Propagate profile name and hash from SA to CA (#7367)

When the order object retrieved from the SA contains a profile name,
propagate that into the request for the CA to issue a precertificate.
Similarly, when the CA's precertificate issuance response contains a
profile hash, propagate that into the request for the CA to issue the
corresponding final certificate.

Fixes https://github.com/letsencrypt/boulder/issues/7366
This commit is contained in:
Aaron Gable 2024-03-14 14:55:32 -07:00 committed by GitHub
parent 8d169a8dfb
commit 8ac88f557b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 13 deletions

View File

@ -2,15 +2,17 @@ package mocks
import ( import (
"context" "context"
"crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"time" "time"
capb "github.com/letsencrypt/boulder/ca/proto"
corepb "github.com/letsencrypt/boulder/core/proto"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
capb "github.com/letsencrypt/boulder/ca/proto"
corepb "github.com/letsencrypt/boulder/core/proto"
) )
// MockCA is a mock of a CA that always returns the cert from PEM in response to // MockCA is a mock of a CA that always returns the cert from PEM in response to
@ -20,7 +22,7 @@ type MockCA struct {
} }
// IssuePrecertificate is a mock // IssuePrecertificate is a mock
func (ca *MockCA) IssuePrecertificate(ctx context.Context, _ *capb.IssueCertificateRequest, _ ...grpc.CallOption) (*capb.IssuePrecertificateResponse, error) { func (ca *MockCA) IssuePrecertificate(ctx context.Context, req *capb.IssueCertificateRequest, _ ...grpc.CallOption) (*capb.IssuePrecertificateResponse, error) {
if ca.PEM == nil { if ca.PEM == nil {
return nil, fmt.Errorf("MockCA's PEM field must be set before calling IssueCertificate") return nil, fmt.Errorf("MockCA's PEM field must be set before calling IssueCertificate")
} }
@ -29,8 +31,10 @@ func (ca *MockCA) IssuePrecertificate(ctx context.Context, _ *capb.IssueCertific
if err != nil { if err != nil {
return nil, err return nil, err
} }
profHash := sha256.Sum256([]byte(req.CertProfileName))
return &capb.IssuePrecertificateResponse{ return &capb.IssuePrecertificateResponse{
DER: cert.Raw, DER: cert.Raw,
CertProfileHash: profHash[:8],
}, nil }, nil
} }

View File

@ -1225,7 +1225,7 @@ func (ra *RegistrationAuthorityImpl) issueCertificateOuter(
// Step 3: Issue the Certificate // Step 3: Issue the Certificate
cert, err := ra.issueCertificateInner( cert, err := ra.issueCertificateInner(
ctx, csr, accountID(order.RegistrationID), orderID(order.Id)) ctx, csr, order.CertificateProfileName, accountID(order.RegistrationID), orderID(order.Id))
// Step 4: Fail the order if necessary, and update metrics and log fields // Step 4: Fail the order if necessary, and update metrics and log fields
var result string var result string
@ -1282,6 +1282,7 @@ func (ra *RegistrationAuthorityImpl) issueCertificateOuter(
func (ra *RegistrationAuthorityImpl) issueCertificateInner( func (ra *RegistrationAuthorityImpl) issueCertificateInner(
ctx context.Context, ctx context.Context,
csr *x509.CertificateRequest, csr *x509.CertificateRequest,
profileName string,
acctID accountID, acctID accountID,
oID orderID) (*x509.Certificate, error) { oID orderID) (*x509.Certificate, error) {
if features.Get().AsyncFinalize { if features.Get().AsyncFinalize {
@ -1303,9 +1304,10 @@ func (ra *RegistrationAuthorityImpl) issueCertificateInner(
} }
issueReq := &capb.IssueCertificateRequest{ issueReq := &capb.IssueCertificateRequest{
Csr: csr.Raw, Csr: csr.Raw,
RegistrationID: int64(acctID), RegistrationID: int64(acctID),
OrderID: int64(oID), OrderID: int64(oID),
CertProfileName: profileName,
} }
precert, err := ra.CA.IssuePrecertificate(ctx, issueReq) precert, err := ra.CA.IssuePrecertificate(ctx, issueReq)
if err != nil { if err != nil {
@ -1323,10 +1325,11 @@ func (ra *RegistrationAuthorityImpl) issueCertificateInner(
} }
cert, err := ra.CA.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{ cert, err := ra.CA.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
DER: precert.DER, DER: precert.DER,
SCTs: scts, SCTs: scts,
RegistrationID: int64(acctID), RegistrationID: int64(acctID),
OrderID: int64(oID), OrderID: int64(oID),
CertProfileHash: precert.CertProfileHash,
}) })
if err != nil { if err != nil {
return nil, wrapError(err, "issuing certificate for precertificate") return nil, wrapError(err, "issuing certificate for precertificate")

View File

@ -3633,7 +3633,7 @@ func TestIssueCertificateInnerErrs(t *testing.T) {
// Mock the CA // Mock the CA
ra.CA = tc.Mock ra.CA = tc.Mock
// Attempt issuance // Attempt issuance
_, err = ra.issueCertificateInner(ctx, csrOb, accountID(Registration.Id), orderID(order.Id)) _, err = ra.issueCertificateInner(ctx, csrOb, order.CertificateProfileName, accountID(Registration.Id), orderID(order.Id))
// We expect all of the testcases to fail because all use mocked CAs that deliberately error // We expect all of the testcases to fail because all use mocked CAs that deliberately error
test.AssertError(t, err, "issueCertificateInner with failing mock CA did not fail") test.AssertError(t, err, "issueCertificateInner with failing mock CA did not fail")
// If there is an expected `error` then match the error message // If there is an expected `error` then match the error message
@ -3652,6 +3652,60 @@ func TestIssueCertificateInnerErrs(t *testing.T) {
} }
} }
type MockCARecordingProfile struct {
inner *mocks.MockCA
profileName string
profileHash []byte
}
func (ca *MockCARecordingProfile) IssuePrecertificate(ctx context.Context, req *capb.IssueCertificateRequest, _ ...grpc.CallOption) (*capb.IssuePrecertificateResponse, error) {
ca.profileName = req.CertProfileName
return ca.inner.IssuePrecertificate(ctx, req)
}
func (ca *MockCARecordingProfile) IssueCertificateForPrecertificate(ctx context.Context, req *capb.IssueCertificateForPrecertificateRequest, _ ...grpc.CallOption) (*corepb.Certificate, error) {
ca.profileHash = req.CertProfileHash
return ca.inner.IssueCertificateForPrecertificate(ctx, req)
}
func TestIssueCertificateInnerWithProfile(t *testing.T) {
_, _, ra, fc, cleanup := initAuthorities(t)
defer cleanup()
// Generate a reasonable-looking CSR and cert to pass the matchesCSR check.
testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "generating test key")
csrDER, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{DNSNames: []string{"example.com"}}, testKey)
test.AssertNotError(t, err, "creating test csr")
csr, err := x509.ParseCertificateRequest(csrDER)
test.AssertNotError(t, err, "parsing test csr")
certDER, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{
SerialNumber: big.NewInt(1),
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
BasicConstraintsValid: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
}, &x509.Certificate{}, testKey.Public(), testKey)
test.AssertNotError(t, err, "creating test cert")
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
// Use a mock CA that will record the profile name and profile hash included
// in the RA's request messages. Populate it with the cert generated above.
mockCA := MockCARecordingProfile{inner: &mocks.MockCA{PEM: certPEM}}
ra.CA = &mockCA
// The basic mocks.StorageAuthority always succeeds on FinalizeOrder, which is
// the only SA call that issueCertificateInner makes.
ra.SA = &mocks.StorageAuthority{}
// Call issueCertificateInner with the CSR generated above and the profile
// name "default", which will cause the mockCA to return a specific hash.
_, err = ra.issueCertificateInner(context.Background(), csr, "default", 1, 1)
test.AssertNotError(t, err, "issuing cert with profile name")
test.AssertEquals(t, mockCA.profileName, "default")
test.AssertByteEquals(t, mockCA.profileHash, []byte{0x37, 0xa8, 0xee, 0xc1, 0xce, 0x19, 0x68, 0x7d})
}
func TestNewOrderMaxNames(t *testing.T) { func TestNewOrderMaxNames(t *testing.T) {
_, _, ra, _, cleanUp := initAuthorities(t) _, _, ra, _, cleanUp := initAuthorities(t)
defer cleanUp() defer cleanUp()