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:
parent
8d169a8dfb
commit
8ac88f557b
12
mocks/ca.go
12
mocks/ca.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
19
ra/ra.go
19
ra/ra.go
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue