Implement unpredictable issuance from similar intermediates (#7418)
Replace the CA's "useForRSA" and "useForECDSA" config keys with a single "active" boolean. When the CA starts up, all active RSA issuers will be used to issue precerts with RSA pubkeys, and all ECDSA issuers will be used to issue precerts with ECDSA pubkeys (if the ECDSAForAll flag is true; otherwise just those that are on the allow-list). All "inactive" issuers can still issue OCSP responses, CRLs, and (notably) final certificates. Instead of using the "useForRSA" and "useForECDSA" flags, plus implicit config ordering, to determine which issuer to use to handle a given issuance, simply use the issuer's public key algorithm to determine which issuances it should be handling. All implicit ordering considerations are removed, because the "active" certificates now just form a pool that is sampled from randomly. To facilitate this, update some unit and integration tests to be more flexible and try multiple potential issuing intermediates, particularly when constructing OCSP requests. For this change to be safe to deploy with no user-visible behavior changes, the CA configs must contain: - Exactly one RSA-keyed intermediate with "useForRSALeaves" set to true; and - Exactly one ECDSA-keyed intermediate with "useForECDSALeaves" set to true. If the configs contain more than one intermediate meeting one of the bullets above, then randomized issuance will begin immediately. Fixes https://github.com/letsencrypt/boulder/issues/7291 Fixes https://github.com/letsencrypt/boulder/issues/7290
This commit is contained in:
parent
13172ac3f1
commit
94d14689bf
89
ca/ca.go
89
ca/ca.go
|
@ -14,6 +14,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
mrand "math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -46,11 +47,11 @@ const (
|
|||
)
|
||||
|
||||
// Two maps of keys to Issuers. Lookup by PublicKeyAlgorithm is useful for
|
||||
// determining which issuer to use to sign a given (pre)cert, based on its
|
||||
// PublicKeyAlgorithm. Lookup by NameID is useful for looking up the appropriate
|
||||
// determining the set of issuers which can sign a given (pre)cert, based on its
|
||||
// PublicKeyAlgorithm. Lookup by NameID is useful for looking up a specific
|
||||
// issuer based on the issuer of a given (pre)certificate.
|
||||
type issuerMaps struct {
|
||||
byAlg map[x509.PublicKeyAlgorithm]*issuance.Issuer
|
||||
byAlg map[x509.PublicKeyAlgorithm][]*issuance.Issuer
|
||||
byNameID map[issuance.NameID]*issuance.Issuer
|
||||
}
|
||||
|
||||
|
@ -99,24 +100,29 @@ type certificateAuthorityImpl struct {
|
|||
lintErrorCount prometheus.Counter
|
||||
}
|
||||
|
||||
// makeIssuerMaps processes a list of issuers into a set of maps, mapping
|
||||
// nearly-unique identifiers of those issuers to the issuers themselves. Note
|
||||
// that, if two issuers have the same nearly-unique ID, the *latter* one in
|
||||
// the input list "wins".
|
||||
func makeIssuerMaps(issuers []*issuance.Issuer) issuerMaps {
|
||||
issuersByAlg := make(map[x509.PublicKeyAlgorithm]*issuance.Issuer, 2)
|
||||
// makeIssuerMaps processes a list of issuers into a set of maps for easy
|
||||
// lookup either by key algorithm (useful for picking an issuer for a precert)
|
||||
// or by unique ID (useful for final certs, OCSP, and CRLs). If two issuers with
|
||||
// the same unique ID are encountered, an error is returned.
|
||||
func makeIssuerMaps(issuers []*issuance.Issuer) (issuerMaps, error) {
|
||||
issuersByAlg := make(map[x509.PublicKeyAlgorithm][]*issuance.Issuer, 2)
|
||||
issuersByNameID := make(map[issuance.NameID]*issuance.Issuer, len(issuers))
|
||||
for _, issuer := range issuers {
|
||||
for _, alg := range issuer.Algs() {
|
||||
// TODO(#5259): Enforce that there is only one issuer for each algorithm,
|
||||
// instead of taking the first issuer for each algorithm type.
|
||||
if issuersByAlg[alg] == nil {
|
||||
issuersByAlg[alg] = issuer
|
||||
}
|
||||
if _, found := issuersByNameID[issuer.NameID()]; found {
|
||||
return issuerMaps{}, fmt.Errorf("two issuers with same NameID %d (%s) configured", issuer.NameID(), issuer.Name())
|
||||
}
|
||||
issuersByNameID[issuer.NameID()] = issuer
|
||||
if issuer.IsActive() {
|
||||
issuersByAlg[issuer.KeyType()] = append(issuersByAlg[issuer.KeyType()], issuer)
|
||||
}
|
||||
}
|
||||
return issuerMaps{issuersByAlg, issuersByNameID}
|
||||
if i, ok := issuersByAlg[x509.ECDSA]; !ok || len(i) == 0 {
|
||||
return issuerMaps{}, errors.New("no ECDSA issuers configured")
|
||||
}
|
||||
if i, ok := issuersByAlg[x509.RSA]; !ok || len(i) == 0 {
|
||||
return issuerMaps{}, errors.New("no RSA issuers configured")
|
||||
}
|
||||
return issuerMaps{issuersByAlg, issuersByNameID}, nil
|
||||
}
|
||||
|
||||
// makeCertificateProfilesMap processes a list of certificate issuance profile
|
||||
|
@ -237,7 +243,10 @@ func NewCertificateAuthorityImpl(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
issuers := makeIssuerMaps(boulderIssuers)
|
||||
issuers, err := makeIssuerMaps(boulderIssuers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lintErrorCount := prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
|
@ -393,27 +402,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: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] precert=[%s]",
|
||||
issuer.Name(), serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, hex.EncodeToString(precert.Raw))
|
||||
|
||||
_, issuanceToken, err := issuer.Prepare(certProfile.profile, issuanceReq)
|
||||
if err != nil {
|
||||
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)
|
||||
ca.log.AuditErrf("Preparing cert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
|
||||
issuer.Name(), 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] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
|
||||
serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, err)
|
||||
ca.log.AuditErrf("Signing cert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
|
||||
issuer.Name(), 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] certProfileName=[%s] certProfileHash=[%x]",
|
||||
serialHex, req.RegistrationID, names, hex.EncodeToString(certDER), certProfile.name, certProfile.hash)
|
||||
ca.log.AuditInfof("Signing cert success: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certificate=[%s] certProfileName=[%s] certProfileHash=[%x]",
|
||||
issuer.Name(), serialHex, req.RegistrationID, names, hex.EncodeToString(certDER), certProfile.name, certProfile.hash)
|
||||
|
||||
_, err = ca.sa.AddCertificate(ctx, &sapb.AddCertificateRequest{
|
||||
Der: certDER,
|
||||
|
@ -421,8 +430,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] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
|
||||
serialHex, hex.EncodeToString(certDER), issuer.NameID(), req.RegistrationID, req.OrderID, certProfile.name, certProfile.hash, err)
|
||||
ca.log.AuditErrf("Failed RPC to store at SA: issuer=[%s] serial=[%s] cert=[%s] regID=[%d] orderID=[%d] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
|
||||
issuer.Name(), serialHex, hex.EncodeToString(certDER), req.RegistrationID, req.OrderID, certProfile.name, certProfile.hash, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -514,17 +523,19 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Use the issuer which corresponds to the algorithm of the public key
|
||||
// contained in the CSR, unless we have an allowlist of registration IDs
|
||||
// for ECDSA, in which case switch all not-allowed accounts to RSA issuance.
|
||||
// Select which pool of issuers to use, based on the to-be-issued cert's key
|
||||
// type and whether we're using the ECDSA Allow List.
|
||||
alg := csr.PublicKeyAlgorithm
|
||||
if alg == x509.ECDSA && !features.Get().ECDSAForAll && ca.ecdsaAllowList != nil && !ca.ecdsaAllowList.permitted(issueReq.RegistrationID) {
|
||||
alg = x509.RSA
|
||||
}
|
||||
issuer, ok := ca.issuers.byAlg[alg]
|
||||
if !ok {
|
||||
return nil, nil, berrors.InternalServerError("no issuer found for public key algorithm %s", csr.PublicKeyAlgorithm)
|
||||
|
||||
// Select a random issuer from among the active issuers of this key type.
|
||||
issuerPool, ok := ca.issuers.byAlg[alg]
|
||||
if !ok || len(issuerPool) == 0 {
|
||||
return nil, nil, berrors.InternalServerError("no issuers found for public key algorithm %s", csr.PublicKeyAlgorithm)
|
||||
}
|
||||
issuer := issuerPool[mrand.Intn(len(issuerPool))]
|
||||
|
||||
if issuer.Cert.NotAfter.Before(validity.NotAfter) {
|
||||
err = berrors.InternalServerError("cannot issue a certificate that expires after the issuer certificate")
|
||||
|
@ -557,8 +568,8 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
|
|||
|
||||
lintCertBytes, issuanceToken, err := issuer.Prepare(certProfile.profile, req)
|
||||
if err != nil {
|
||||
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)
|
||||
ca.log.AuditErrf("Preparing precert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
|
||||
issuer.Name(), serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), certProfile.name, certProfile.hash, err)
|
||||
if errors.Is(err, linter.ErrLinting) {
|
||||
ca.lintErrorCount.Inc()
|
||||
}
|
||||
|
@ -579,14 +590,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] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
|
||||
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), certProfile.name, certProfile.hash, err)
|
||||
ca.log.AuditErrf("Signing precert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
|
||||
issuer.Name(), 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] certProfileName=[%s] certProfileHash=[%x]",
|
||||
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(certDER), certProfile.name, certProfile.hash)
|
||||
ca.log.AuditInfof("Signing precert success: issuer=[%s] serial=[%s] regID=[%d] names=[%s] precertificate=[%s] certProfileName=[%s] certProfileHash=[%x]",
|
||||
issuer.Name(), serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(certDER), certProfile.name, certProfile.hash)
|
||||
|
||||
return certDER, certProfile.hash[:], nil
|
||||
}
|
||||
|
|
149
ca/ca_test.go
149
ca/ca_test.go
|
@ -96,12 +96,6 @@ var (
|
|||
|
||||
const arbitraryRegID int64 = 1001
|
||||
|
||||
// Useful key and certificate files.
|
||||
const rsaIntKey = "../test/hierarchy/int-r3.key.pem"
|
||||
const rsaIntCert = "../test/hierarchy/int-r3.cert.pem"
|
||||
const ecdsaIntKey = "../test/hierarchy/int-e1.key.pem"
|
||||
const ecdsaIntCert = "../test/hierarchy/int-e1.cert.pem"
|
||||
|
||||
func mustRead(path string) []byte {
|
||||
return must.Do(os.ReadFile(path))
|
||||
}
|
||||
|
@ -192,28 +186,20 @@ func setup(t *testing.T) *testCtx {
|
|||
}
|
||||
test.AssertEquals(t, len(certProfiles), 2)
|
||||
|
||||
ecdsaOnlyIssuer, err := issuance.LoadIssuer(issuance.IssuerConfig{
|
||||
UseForRSALeaves: false,
|
||||
UseForECDSALeaves: true,
|
||||
IssuerURL: "http://not-example.com/issuer-url",
|
||||
OCSPURL: "http://not-example.com/ocsp",
|
||||
CRLURLBase: "http://not-example.com/crl/",
|
||||
Location: issuance.IssuerLoc{File: ecdsaIntKey, CertFile: ecdsaIntCert},
|
||||
}, fc)
|
||||
test.AssertNotError(t, err, "Couldn't load test issuer")
|
||||
|
||||
ecdsaAndRSAIssuer, err := issuance.LoadIssuer(issuance.IssuerConfig{
|
||||
UseForRSALeaves: true,
|
||||
UseForECDSALeaves: true,
|
||||
IssuerURL: "http://not-example.com/issuer-url",
|
||||
OCSPURL: "http://not-example.com/ocsp",
|
||||
CRLURLBase: "http://not-example.com/crl/",
|
||||
Location: issuance.IssuerLoc{File: rsaIntKey, CertFile: rsaIntCert},
|
||||
}, fc)
|
||||
test.AssertNotError(t, err, "Couldn't load test issuer")
|
||||
|
||||
// Must list ECDSA-only issuer first, so it is the default for ECDSA.
|
||||
boulderIssuers := []*issuance.Issuer{ecdsaOnlyIssuer, ecdsaAndRSAIssuer}
|
||||
boulderIssuers := make([]*issuance.Issuer, 4)
|
||||
for i, name := range []string{"int-r3", "int-r4", "int-e1", "int-e2"} {
|
||||
boulderIssuers[i], err = issuance.LoadIssuer(issuance.IssuerConfig{
|
||||
Active: true,
|
||||
IssuerURL: fmt.Sprintf("http://not-example.com/i/%s", name),
|
||||
OCSPURL: "http://not-example.com/o",
|
||||
CRLURLBase: fmt.Sprintf("http://not-example.com/c/%s/", name),
|
||||
Location: issuance.IssuerLoc{
|
||||
File: fmt.Sprintf("../test/hierarchy/%s.key.pem", name),
|
||||
CertFile: fmt.Sprintf("../test/hierarchy/%s.cert.pem", name),
|
||||
},
|
||||
}, fc)
|
||||
test.AssertNotError(t, err, "Couldn't load test issuer")
|
||||
}
|
||||
|
||||
keyPolicy := goodkey.KeyPolicy{
|
||||
AllowRSA: true,
|
||||
|
@ -496,25 +482,114 @@ func TestMultipleIssuers(t *testing.T) {
|
|||
_, 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.
|
||||
// Test that an RSA CSR gets issuance from an RSA issuer.
|
||||
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")
|
||||
validated := false
|
||||
for _, issuer := range ca.issuers.byAlg[x509.RSA] {
|
||||
err = cert.CheckSignatureFrom(issuer.Cert.Certificate)
|
||||
if err == nil {
|
||||
validated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
test.Assert(t, validated, "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.
|
||||
// Test that an ECDSA CSR gets issuance from an ECDSA issuer.
|
||||
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")
|
||||
validated = false
|
||||
for _, issuer := range ca.issuers.byAlg[x509.ECDSA] {
|
||||
err = cert.CheckSignatureFrom(issuer.Cert.Certificate)
|
||||
if err == nil {
|
||||
validated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
test.Assert(t, validated, "Certificate failed signature validation")
|
||||
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 2)
|
||||
}
|
||||
|
||||
func TestUnpredictableIssuance(t *testing.T) {
|
||||
testCtx := setup(t)
|
||||
sa := &mockSA{}
|
||||
|
||||
// Load our own set of issuer configs, specifically with:
|
||||
// - 3 issuers,
|
||||
// - 2 of which are active
|
||||
boulderIssuers := make([]*issuance.Issuer, 3)
|
||||
var err error
|
||||
for i, name := range []string{"int-e1", "int-e2", "int-r3"} {
|
||||
boulderIssuers[i], err = issuance.LoadIssuer(issuance.IssuerConfig{
|
||||
Active: i != 0, // Make one of the ECDSA issuers inactive.
|
||||
IssuerURL: fmt.Sprintf("http://not-example.com/i/%s", name),
|
||||
OCSPURL: "http://not-example.com/o",
|
||||
CRLURLBase: fmt.Sprintf("http://not-example.com/c/%s/", name),
|
||||
Location: issuance.IssuerLoc{
|
||||
File: fmt.Sprintf("../test/hierarchy/%s.key.pem", name),
|
||||
CertFile: fmt.Sprintf("../test/hierarchy/%s.cert.pem", name),
|
||||
},
|
||||
}, testCtx.fc)
|
||||
test.AssertNotError(t, err, "Couldn't load test issuer")
|
||||
}
|
||||
|
||||
ca, err := NewCertificateAuthorityImpl(
|
||||
sa,
|
||||
testCtx.pa,
|
||||
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 remake CA")
|
||||
|
||||
// Then, modify the resulting issuer maps so that the RSA issuer appears to
|
||||
// be an ECDSA issuer. This would be easier if we had three ECDSA issuers to
|
||||
// use here, but that doesn't exist in //test/hierarchy (yet).
|
||||
ca.issuers.byAlg[x509.ECDSA] = append(ca.issuers.byAlg[x509.ECDSA], ca.issuers.byAlg[x509.RSA]...)
|
||||
ca.issuers.byAlg[x509.RSA] = []*issuance.Issuer{}
|
||||
|
||||
// Issue the same (ECDSA-keyed) certificate 20 times. None of the issuances
|
||||
// should come from the inactive issuer (int-e1). At least one issuance should
|
||||
// come from each of the two active issuers (int-e2 and int-r3). With 20
|
||||
// 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}
|
||||
seenE2 := false
|
||||
seenR3 := false
|
||||
for i := 0; i < 20; i++ {
|
||||
result, err := ca.IssuePrecertificate(ctx, req)
|
||||
test.AssertNotError(t, err, "Failed to issue test certificate")
|
||||
cert, err := x509.ParseCertificate(result.DER)
|
||||
test.AssertNotError(t, err, "Failed to parse test certificate")
|
||||
if strings.Contains(cert.Issuer.CommonName, "E1") {
|
||||
t.Fatal("Issued certificate from inactive issuer")
|
||||
} else if strings.Contains(cert.Issuer.CommonName, "E2") {
|
||||
seenE2 = true
|
||||
} else if strings.Contains(cert.Issuer.CommonName, "R3") {
|
||||
seenR3 = true
|
||||
}
|
||||
}
|
||||
test.Assert(t, seenE2, "Expected at least one issuance from active issuer")
|
||||
test.Assert(t, seenR3, "Expected at least one issuance from active issuer")
|
||||
}
|
||||
|
||||
func TestProfiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := setup(t)
|
||||
|
@ -695,7 +770,7 @@ func TestECDSAAllowList(t *testing.T) {
|
|||
test.AssertNotError(t, err, "Failed to issue certificate")
|
||||
cert, err := x509.ParseCertificate(result.DER)
|
||||
test.AssertNotError(t, err, "Certificate failed to parse")
|
||||
test.AssertByteEquals(t, cert.RawIssuer, ca.issuers.byAlg[x509.ECDSA].Cert.RawSubject)
|
||||
test.AssertEquals(t, cert.SignatureAlgorithm, x509.ECDSAWithSHA384)
|
||||
|
||||
// With allowlist not containing arbitraryRegID, issuance should fall back to RSA issuer.
|
||||
regIDMap = makeRegIDsMap([]int64{2002})
|
||||
|
@ -704,7 +779,7 @@ func TestECDSAAllowList(t *testing.T) {
|
|||
test.AssertNotError(t, err, "Failed to issue certificate")
|
||||
cert, err = x509.ParseCertificate(result.DER)
|
||||
test.AssertNotError(t, err, "Certificate failed to parse")
|
||||
test.AssertByteEquals(t, cert.RawIssuer, ca.issuers.byAlg[x509.RSA].Cert.RawSubject)
|
||||
test.AssertEquals(t, cert.SignatureAlgorithm, x509.SHA256WithRSA)
|
||||
|
||||
// With empty allowlist but ECDSAForAll enabled, issuance should come from ECDSA issuer.
|
||||
ca, _ = issueCertificateSubTestSetup(t, nil)
|
||||
|
@ -714,7 +789,7 @@ func TestECDSAAllowList(t *testing.T) {
|
|||
test.AssertNotError(t, err, "Failed to issue certificate")
|
||||
cert, err = x509.ParseCertificate(result.DER)
|
||||
test.AssertNotError(t, err, "Certificate failed to parse")
|
||||
test.AssertByteEquals(t, cert.RawIssuer, ca.issuers.byAlg[x509.ECDSA].Cert.RawSubject)
|
||||
test.AssertEquals(t, cert.SignatureAlgorithm, x509.ECDSAWithSHA384)
|
||||
}
|
||||
|
||||
func TestInvalidCSRs(t *testing.T) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
capb "github.com/letsencrypt/boulder/ca/proto"
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/letsencrypt/boulder/issuance"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
"github.com/letsencrypt/boulder/metrics"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
|
@ -54,41 +55,43 @@ func TestOCSP(t *testing.T) {
|
|||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
ocspi := testCtx.ocsp
|
||||
|
||||
// Issue a certificate from the RSA issuer, then check OCSP comes from that same issuer.
|
||||
rsaIssuerID := ca.issuers.byAlg[x509.RSA].NameID()
|
||||
// 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})
|
||||
test.AssertNotError(t, err, "Failed to issue certificate")
|
||||
rsaCert, err := x509.ParseCertificate(rsaCertPB.DER)
|
||||
test.AssertNotError(t, err, "Failed to parse rsaCert")
|
||||
rsaIssuerID := issuance.IssuerNameID(rsaCert)
|
||||
rsaOCSPPB, err := ocspi.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{
|
||||
Serial: core.SerialToString(rsaCert.SerialNumber),
|
||||
IssuerID: int64(rsaIssuerID),
|
||||
Status: string(core.OCSPStatusGood),
|
||||
})
|
||||
test.AssertNotError(t, err, "Failed to generate OCSP")
|
||||
rsaOCSP, err := ocsp.ParseResponse(rsaOCSPPB.Response, testCtx.boulderIssuers[1].Cert.Certificate)
|
||||
rsaOCSP, err := ocsp.ParseResponse(rsaOCSPPB.Response, ca.issuers.byNameID[rsaIssuerID].Cert.Certificate)
|
||||
test.AssertNotError(t, err, "Failed to parse / validate OCSP for rsaCert")
|
||||
test.AssertEquals(t, rsaOCSP.Status, 0)
|
||||
test.AssertEquals(t, rsaOCSP.RevocationReason, 0)
|
||||
test.AssertEquals(t, rsaOCSP.SerialNumber.Cmp(rsaCert.SerialNumber), 0)
|
||||
|
||||
// Check that a different issuer cannot validate the OCSP response
|
||||
_, err = ocsp.ParseResponse(rsaOCSPPB.Response, testCtx.boulderIssuers[0].Cert.Certificate)
|
||||
_, err = ocsp.ParseResponse(rsaOCSPPB.Response, ca.issuers.byAlg[x509.ECDSA][0].Cert.Certificate)
|
||||
test.AssertError(t, err, "Parsed / validated OCSP for rsaCert, but should not have")
|
||||
|
||||
// Issue a certificate from an ECDSA issuer, then check OCSP comes from that same issuer.
|
||||
ecdsaIssuerID := ca.issuers.byAlg[x509.ECDSA].NameID()
|
||||
// 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})
|
||||
test.AssertNotError(t, err, "Failed to issue certificate")
|
||||
ecdsaCert, err := x509.ParseCertificate(ecdsaCertPB.DER)
|
||||
test.AssertNotError(t, err, "Failed to parse ecdsaCert")
|
||||
ecdsaIssuerID := issuance.IssuerNameID(ecdsaCert)
|
||||
ecdsaOCSPPB, err := ocspi.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{
|
||||
Serial: core.SerialToString(ecdsaCert.SerialNumber),
|
||||
IssuerID: int64(ecdsaIssuerID),
|
||||
Status: string(core.OCSPStatusGood),
|
||||
})
|
||||
test.AssertNotError(t, err, "Failed to generate OCSP")
|
||||
ecdsaOCSP, err := ocsp.ParseResponse(ecdsaOCSPPB.Response, testCtx.boulderIssuers[0].Cert.Certificate)
|
||||
ecdsaOCSP, err := ocsp.ParseResponse(ecdsaOCSPPB.Response, ca.issuers.byNameID[ecdsaIssuerID].Cert.Certificate)
|
||||
test.AssertNotError(t, err, "Failed to parse / validate OCSP for ecdsaCert")
|
||||
test.AssertEquals(t, ecdsaOCSP.Status, 0)
|
||||
test.AssertEquals(t, ecdsaOCSP.RevocationReason, 0)
|
||||
|
|
|
@ -83,18 +83,15 @@ func NewProfile(profileConfig ProfileConfig, skipLints []string) (*Profile, erro
|
|||
// request doesn't match the signing profile an error is returned.
|
||||
func (i *Issuer) requestValid(clk clock.Clock, prof *Profile, req *IssuanceRequest) error {
|
||||
switch req.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
if !i.useForRSALeaves {
|
||||
return errors.New("cannot sign RSA public keys")
|
||||
}
|
||||
case *ecdsa.PublicKey:
|
||||
if !i.useForECDSALeaves {
|
||||
return errors.New("cannot sign ECDSA public keys")
|
||||
}
|
||||
case *rsa.PublicKey, *ecdsa.PublicKey:
|
||||
default:
|
||||
return errors.New("unsupported public key type")
|
||||
}
|
||||
|
||||
if len(req.precertDER) == 0 && !i.active {
|
||||
return errors.New("inactive issuer cannot issue precert")
|
||||
}
|
||||
|
||||
if len(req.SubjectKeyId) != 20 {
|
||||
return errors.New("unexpected subject key ID length")
|
||||
}
|
||||
|
|
|
@ -53,23 +53,23 @@ func TestRequestValid(t *testing.T) {
|
|||
expectedError: "unsupported public key type",
|
||||
},
|
||||
{
|
||||
name: "cannot sign rsa",
|
||||
name: "inactive (rsa)",
|
||||
issuer: &Issuer{},
|
||||
profile: &Profile{},
|
||||
request: &IssuanceRequest{PublicKey: &rsa.PublicKey{}},
|
||||
expectedError: "cannot sign RSA public keys",
|
||||
expectedError: "inactive issuer cannot issue precert",
|
||||
},
|
||||
{
|
||||
name: "cannot sign ecdsa",
|
||||
name: "inactive (ecdsa)",
|
||||
issuer: &Issuer{},
|
||||
profile: &Profile{},
|
||||
request: &IssuanceRequest{PublicKey: &ecdsa.PublicKey{}},
|
||||
expectedError: "cannot sign ECDSA public keys",
|
||||
expectedError: "inactive issuer cannot issue precert",
|
||||
},
|
||||
{
|
||||
name: "skid too short",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{},
|
||||
request: &IssuanceRequest{
|
||||
|
@ -81,7 +81,7 @@ func TestRequestValid(t *testing.T) {
|
|||
{
|
||||
name: "must staple not allowed",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{},
|
||||
request: &IssuanceRequest{
|
||||
|
@ -94,7 +94,7 @@ func TestRequestValid(t *testing.T) {
|
|||
{
|
||||
name: "ct poison not allowed",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{},
|
||||
request: &IssuanceRequest{
|
||||
|
@ -107,7 +107,7 @@ func TestRequestValid(t *testing.T) {
|
|||
{
|
||||
name: "sct list not allowed",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{},
|
||||
request: &IssuanceRequest{
|
||||
|
@ -120,7 +120,7 @@ func TestRequestValid(t *testing.T) {
|
|||
{
|
||||
name: "sct list and ct poison not allowed",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{
|
||||
allowCTPoison: true,
|
||||
|
@ -137,7 +137,7 @@ func TestRequestValid(t *testing.T) {
|
|||
{
|
||||
name: "common name not allowed",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{},
|
||||
request: &IssuanceRequest{
|
||||
|
@ -150,7 +150,7 @@ func TestRequestValid(t *testing.T) {
|
|||
{
|
||||
name: "negative validity",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{},
|
||||
request: &IssuanceRequest{
|
||||
|
@ -164,7 +164,7 @@ func TestRequestValid(t *testing.T) {
|
|||
{
|
||||
name: "validity larger than max",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{
|
||||
maxValidity: time.Minute,
|
||||
|
@ -180,7 +180,7 @@ func TestRequestValid(t *testing.T) {
|
|||
{
|
||||
name: "validity larger than max due to inclusivity",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{
|
||||
maxValidity: time.Hour,
|
||||
|
@ -196,7 +196,7 @@ func TestRequestValid(t *testing.T) {
|
|||
{
|
||||
name: "validity backdated more than max",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{
|
||||
maxValidity: time.Hour * 2,
|
||||
|
@ -213,7 +213,7 @@ func TestRequestValid(t *testing.T) {
|
|||
{
|
||||
name: "validity is forward dated",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{
|
||||
maxValidity: time.Hour * 2,
|
||||
|
@ -230,7 +230,7 @@ func TestRequestValid(t *testing.T) {
|
|||
{
|
||||
name: "serial too short",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{
|
||||
maxValidity: time.Hour * 2,
|
||||
|
@ -247,7 +247,7 @@ func TestRequestValid(t *testing.T) {
|
|||
{
|
||||
name: "serial too long",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{
|
||||
maxValidity: time.Hour * 2,
|
||||
|
@ -264,7 +264,7 @@ func TestRequestValid(t *testing.T) {
|
|||
{
|
||||
name: "good",
|
||||
issuer: &Issuer{
|
||||
useForECDSALeaves: true,
|
||||
active: true,
|
||||
},
|
||||
profile: &Profile{
|
||||
maxValidity: time.Hour * 2,
|
||||
|
|
|
@ -152,7 +152,28 @@ func LoadChain(certFiles []string) ([]*Certificate, error) {
|
|||
|
||||
// IssuerConfig describes the constraints on and URLs used by a single issuer.
|
||||
type IssuerConfig struct {
|
||||
UseForRSALeaves bool
|
||||
// Active determines if the issuer can be used to sign precertificates. All
|
||||
// issuers, regardless of this field, can be used to sign final certificates
|
||||
// (for which an issuance token is presented), OCSP responses, and CRLs.
|
||||
// All Active issuers of a given key type (RSA or ECDSA) are part of a pool
|
||||
// and each precertificate will be issued randomly from a selected pool.
|
||||
// The selection of which pool depends on the precertificate's key algorithm,
|
||||
// the ECDSAForAll feature flag, and the ECDSAAllowListFilename config field.
|
||||
Active bool
|
||||
|
||||
// UseForRSALeaves is a synonym for Active. Note that, despite the name,
|
||||
// setting this field to true cannot add an issuer to a pool different than
|
||||
// its key type. An active issuer will always be part of a pool based on its
|
||||
// key type.
|
||||
//
|
||||
// Deprecated: use Active instead.
|
||||
UseForRSALeaves bool
|
||||
// UseForECDSALeaves is a synonym for Active. Note that, despite the name,
|
||||
// setting this field to true cannot add an issuer to a pool different than
|
||||
// its key type. An active issuer will always be part of a pool based on its
|
||||
// key type.
|
||||
//
|
||||
// Deprecated: use Active instead.
|
||||
UseForECDSALeaves bool
|
||||
|
||||
IssuerURL string `validate:"required,url"`
|
||||
|
@ -188,9 +209,9 @@ type Issuer struct {
|
|||
Signer crypto.Signer
|
||||
Linter *linter.Linter
|
||||
|
||||
sigAlg x509.SignatureAlgorithm
|
||||
useForRSALeaves bool
|
||||
useForECDSALeaves bool
|
||||
keyAlg x509.PublicKeyAlgorithm
|
||||
sigAlg x509.SignatureAlgorithm
|
||||
active bool
|
||||
|
||||
// Used to set the Authority Information Access caIssuers URL in issued
|
||||
// certificates.
|
||||
|
@ -208,11 +229,14 @@ type Issuer struct {
|
|||
// newIssuer constructs a new Issuer from the in-memory certificate and signer.
|
||||
// It exists as a helper for LoadIssuer to make testing simpler.
|
||||
func newIssuer(config IssuerConfig, cert *Certificate, signer crypto.Signer, clk clock.Clock) (*Issuer, error) {
|
||||
var keyAlg x509.PublicKeyAlgorithm
|
||||
var sigAlg x509.SignatureAlgorithm
|
||||
switch k := cert.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
keyAlg = x509.RSA
|
||||
sigAlg = x509.SHA256WithRSA
|
||||
case *ecdsa.PublicKey:
|
||||
keyAlg = x509.ECDSA
|
||||
switch k.Curve {
|
||||
case elliptic.P256():
|
||||
sigAlg = x509.ECDSAWithSHA256
|
||||
|
@ -258,32 +282,31 @@ func newIssuer(config IssuerConfig, cert *Certificate, signer crypto.Signer, clk
|
|||
}
|
||||
|
||||
i := &Issuer{
|
||||
Cert: cert,
|
||||
Signer: signer,
|
||||
Linter: lintSigner,
|
||||
sigAlg: sigAlg,
|
||||
useForRSALeaves: config.UseForRSALeaves,
|
||||
useForECDSALeaves: config.UseForECDSALeaves,
|
||||
issuerURL: config.IssuerURL,
|
||||
ocspURL: config.OCSPURL,
|
||||
crlURLBase: config.CRLURLBase,
|
||||
clk: clk,
|
||||
Cert: cert,
|
||||
Signer: signer,
|
||||
Linter: lintSigner,
|
||||
keyAlg: keyAlg,
|
||||
sigAlg: sigAlg,
|
||||
active: config.Active || config.UseForRSALeaves || config.UseForECDSALeaves,
|
||||
issuerURL: config.IssuerURL,
|
||||
ocspURL: config.OCSPURL,
|
||||
crlURLBase: config.CRLURLBase,
|
||||
clk: clk,
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// Algs provides the list of leaf certificate public key algorithms for which
|
||||
// this issuer is willing to issue. This is not necessarily the same as the
|
||||
// public key algorithm or signature algorithm in this issuer's own cert.
|
||||
func (i *Issuer) Algs() []x509.PublicKeyAlgorithm {
|
||||
var algs []x509.PublicKeyAlgorithm
|
||||
if i.useForRSALeaves {
|
||||
algs = append(algs, x509.RSA)
|
||||
}
|
||||
if i.useForECDSALeaves {
|
||||
algs = append(algs, x509.ECDSA)
|
||||
}
|
||||
return algs
|
||||
// KeyType returns either x509.RSA or x509.ECDSA, depending on whether the
|
||||
// issuer has an RSA or ECDSA keypair. This is useful for determining which
|
||||
// issuance requests should be routed to this issuer.
|
||||
func (i *Issuer) KeyType() x509.PublicKeyAlgorithm {
|
||||
return i.keyAlg
|
||||
}
|
||||
|
||||
// IsActive is true if the issuer is willing to issue precertificates, and false
|
||||
// if the issuer is only willing to issue final certificates, OCSP, and CRLs.
|
||||
func (i *Issuer) IsActive() bool {
|
||||
return i.active
|
||||
}
|
||||
|
||||
// Name provides the Common Name specified in the issuer's certificate.
|
||||
|
|
|
@ -35,11 +35,10 @@ func defaultProfileConfig() ProfileConfig {
|
|||
|
||||
func defaultIssuerConfig() IssuerConfig {
|
||||
return IssuerConfig{
|
||||
UseForECDSALeaves: true,
|
||||
UseForRSALeaves: true,
|
||||
IssuerURL: "http://issuer-url.example.org",
|
||||
OCSPURL: "http://ocsp-url.example.org",
|
||||
CRLURLBase: "http://crl-url.example.org/",
|
||||
Active: true,
|
||||
IssuerURL: "http://issuer-url.example.org",
|
||||
OCSPURL: "http://ocsp-url.example.org",
|
||||
CRLURLBase: "http://crl-url.example.org/",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,8 +64,7 @@
|
|||
},
|
||||
"issuers": [
|
||||
{
|
||||
"useForRSALeaves": false,
|
||||
"useForECDSALeaves": true,
|
||||
"active": true,
|
||||
"issuerURL": "http://127.0.0.1:4502/int ecdsa a",
|
||||
"ocspURL": "http://127.0.0.1:4002/",
|
||||
"crlURLBase": "http://127.0.0.1:4501/ecdsa-a/",
|
||||
|
@ -76,8 +75,29 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"useForRSALeaves": true,
|
||||
"useForECDSALeaves": true,
|
||||
"active": true,
|
||||
"issuerURL": "http://127.0.0.1:4502/int ecdsa b",
|
||||
"ocspURL": "http://127.0.0.1:4002/",
|
||||
"crlURLBase": "http://127.0.0.1:4501/ecdsa-b/",
|
||||
"location": {
|
||||
"configFile": "/hierarchy/int-ecdsa-b.pkcs11.json",
|
||||
"certFile": "/hierarchy/int-ecdsa-b.cert.pem",
|
||||
"numSessions": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"active": false,
|
||||
"issuerURL": "http://127.0.0.1:4502/int ecdsa c",
|
||||
"ocspURL": "http://127.0.0.1:4002/",
|
||||
"crlURLBase": "http://127.0.0.1:4501/ecdsa-c/",
|
||||
"location": {
|
||||
"configFile": "/hierarchy/int-ecdsa-c.pkcs11.json",
|
||||
"certFile": "/hierarchy/int-ecdsa-c.cert.pem",
|
||||
"numSessions": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"issuerURL": "http://127.0.0.1:4502/int rsa a",
|
||||
"ocspURL": "http://127.0.0.1:4002/",
|
||||
"crlURLBase": "http://127.0.0.1:4501/rsa-a/",
|
||||
|
@ -88,8 +108,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"useForRSALeaves": false,
|
||||
"useForECDSALeaves": false,
|
||||
"active": true,
|
||||
"issuerURL": "http://127.0.0.1:4502/int rsa b",
|
||||
"ocspURL": "http://127.0.0.1:4002/",
|
||||
"crlURLBase": "http://127.0.0.1:4501/rsa-b/",
|
||||
|
@ -98,6 +117,17 @@
|
|||
"certFile": "/hierarchy/int-rsa-b.cert.pem",
|
||||
"numSessions": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"active": false,
|
||||
"issuerURL": "http://127.0.0.1:4502/int rsa c",
|
||||
"ocspURL": "http://127.0.0.1:4002/",
|
||||
"crlURLBase": "http://127.0.0.1:4501/rsa-c/",
|
||||
"location": {
|
||||
"configFile": "/hierarchy/int-rsa-c.pkcs11.json",
|
||||
"certFile": "/hierarchy/int-rsa-c.cert.pem",
|
||||
"numSessions": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignoredLints": [
|
||||
|
|
|
@ -23,7 +23,10 @@
|
|||
"issuerCerts": [
|
||||
"/hierarchy/int-rsa-a.cert.pem",
|
||||
"/hierarchy/int-rsa-b.cert.pem",
|
||||
"/hierarchy/int-ecdsa-a.cert.pem"
|
||||
"/hierarchy/int-rsa-c.cert.pem",
|
||||
"/hierarchy/int-ecdsa-a.cert.pem",
|
||||
"/hierarchy/int-ecdsa-b.cert.pem",
|
||||
"/hierarchy/int-ecdsa-c.cert.pem"
|
||||
],
|
||||
"s3Endpoint": "http://localhost:4501",
|
||||
"s3Bucket": "lets-encrypt-crls",
|
||||
|
|
|
@ -38,7 +38,10 @@
|
|||
"issuerCerts": [
|
||||
"/hierarchy/int-rsa-a.cert.pem",
|
||||
"/hierarchy/int-rsa-b.cert.pem",
|
||||
"/hierarchy/int-ecdsa-a.cert.pem"
|
||||
"/hierarchy/int-rsa-c.cert.pem",
|
||||
"/hierarchy/int-ecdsa-a.cert.pem",
|
||||
"/hierarchy/int-ecdsa-b.cert.pem",
|
||||
"/hierarchy/int-ecdsa-c.cert.pem"
|
||||
],
|
||||
"numShards": 10,
|
||||
"shardWidth": "240h",
|
||||
|
|
|
@ -46,7 +46,10 @@
|
|||
"issuerCerts": [
|
||||
"/hierarchy/int-rsa-a.cert.pem",
|
||||
"/hierarchy/int-rsa-b.cert.pem",
|
||||
"/hierarchy/int-ecdsa-a.cert.pem"
|
||||
"/hierarchy/int-rsa-c.cert.pem",
|
||||
"/hierarchy/int-ecdsa-a.cert.pem",
|
||||
"/hierarchy/int-ecdsa-b.cert.pem",
|
||||
"/hierarchy/int-ecdsa-c.cert.pem"
|
||||
],
|
||||
"liveSigningPeriod": "60h",
|
||||
"timeout": "4.9s",
|
||||
|
|
|
@ -16,7 +16,10 @@
|
|||
"issuerCerts": [
|
||||
"/hierarchy/int-rsa-a.cert.pem",
|
||||
"/hierarchy/int-rsa-b.cert.pem",
|
||||
"/hierarchy/int-ecdsa-a.cert.pem"
|
||||
"/hierarchy/int-rsa-c.cert.pem",
|
||||
"/hierarchy/int-ecdsa-a.cert.pem",
|
||||
"/hierarchy/int-ecdsa-b.cert.pem",
|
||||
"/hierarchy/int-ecdsa-c.cert.pem"
|
||||
],
|
||||
"tls": {
|
||||
"caCertFile": "test/grpc-creds/minica.pem",
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import base64
|
||||
import os
|
||||
import urllib
|
||||
import time
|
||||
import re
|
||||
import random
|
||||
import json
|
||||
import requests
|
||||
import socket
|
||||
import tempfile
|
||||
import shutil
|
||||
import atexit
|
||||
import base64
|
||||
import errno
|
||||
import glob
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import requests
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import urllib
|
||||
|
||||
import challtestsrv
|
||||
|
||||
|
@ -96,7 +96,21 @@ def ocsp_verify(cert_file, issuer_file, ocsp_response):
|
|||
raise(Exception("OCSP verify failure"))
|
||||
return output
|
||||
|
||||
def verify_ocsp(cert_file, issuer_file, url, status="revoked", reason=None):
|
||||
def verify_ocsp(cert_file, issuer_glob, url, status="revoked", reason=None):
|
||||
# Try to verify the OCSP response using every issuer identified by the glob.
|
||||
# If one works, great. If none work, re-raise the exception produced by the
|
||||
# last attempt
|
||||
lastException = None
|
||||
for issuer_file in glob.glob(issuer_glob):
|
||||
try:
|
||||
output = try_verify_ocsp(cert_file, issuer_file, url, status, reason)
|
||||
return output
|
||||
except Exception as e:
|
||||
lastException = e
|
||||
continue
|
||||
raise(lastException)
|
||||
|
||||
def try_verify_ocsp(cert_file, issuer_file, url, status="revoked", reason=None):
|
||||
ocsp_request = make_ocsp_req(cert_file, issuer_file)
|
||||
responses = fetch_ocsp(ocsp_request, url)
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
|
@ -36,10 +37,10 @@ func TestSubordinateCAChainsServedByWFE(t *testing.T) {
|
|||
seenECDSACrossSignedIntermediate := false
|
||||
for _, certUrl := range chains.certs {
|
||||
for _, cert := range certUrl {
|
||||
if cert.Subject.String() == "CN=int ecdsa a,O=good guys,C=US" && cert.Issuer.String() == "CN=root ecdsa,O=good guys,C=US" {
|
||||
if strings.Contains(cert.Subject.CommonName, "int ecdsa") && cert.Issuer.CommonName == "root ecdsa" {
|
||||
seenECDSAIntermediate = true
|
||||
}
|
||||
if cert.Subject.String() == "CN=int ecdsa a,O=good guys,C=US" && cert.Issuer.String() == "CN=root rsa,O=good guys,C=US" {
|
||||
if strings.Contains(cert.Subject.CommonName, "int ecdsa") && cert.Issuer.CommonName == "root rsa" {
|
||||
seenECDSACrossSignedIntermediate = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -679,7 +679,7 @@ def test_revoke_by_account_unspecified():
|
|||
reset_akamai_purges()
|
||||
client.revoke(josepy.ComparableX509(cert), 0)
|
||||
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-a.cert.pem", "http://localhost:4002", "revoked")
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-*.cert.pem", "http://localhost:4002", "revoked")
|
||||
verify_akamai_purge()
|
||||
|
||||
def test_revoke_by_account_with_reason():
|
||||
|
@ -693,7 +693,7 @@ def test_revoke_by_account_with_reason():
|
|||
# Requesting revocation for keyCompromise should work, but not block the
|
||||
# key.
|
||||
client.revoke(josepy.ComparableX509(cert), 1)
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-a.cert.pem", "http://localhost:4002", "revoked", "keyCompromise")
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-*.cert.pem", "http://localhost:4002", "revoked", "keyCompromise")
|
||||
|
||||
verify_akamai_purge()
|
||||
|
||||
|
@ -712,7 +712,7 @@ def test_revoke_by_authz():
|
|||
# Even though we requested reason 1 ("keyCompromise"), the result should be
|
||||
# 5 ("cessationOfOperation") due to the authorization method.
|
||||
client.revoke(josepy.ComparableX509(cert), 1)
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-a.cert.pem", "http://localhost:4002", "revoked", "cessationOfOperation")
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-*.cert.pem", "http://localhost:4002", "revoked", "cessationOfOperation")
|
||||
|
||||
verify_akamai_purge()
|
||||
|
||||
|
@ -755,7 +755,7 @@ def test_revoke_by_privkey():
|
|||
# Even though we requested reason 0 ("unspecified"), the result should be
|
||||
# 1 ("keyCompromise") due to the authorization method.
|
||||
revoke_client.revoke(josepy.ComparableX509(cert), 0)
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-a.cert.pem", "http://localhost:4002", "revoked", "keyCompromise")
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-*.cert.pem", "http://localhost:4002", "revoked", "keyCompromise")
|
||||
|
||||
verify_akamai_purge()
|
||||
|
||||
|
@ -797,7 +797,7 @@ def test_double_revocation():
|
|||
|
||||
# First revoke for any reason.
|
||||
sub_client.revoke(josepy.ComparableX509(cert), 0)
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-a.cert.pem", "http://localhost:4002", "revoked")
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-*.cert.pem", "http://localhost:4002", "revoked")
|
||||
verify_akamai_purge()
|
||||
|
||||
# Re-revocation for anything other than keyCompromise should fail.
|
||||
|
@ -812,7 +812,7 @@ def test_double_revocation():
|
|||
# via the cert key to demonstrate said compromise.
|
||||
reset_akamai_purges()
|
||||
cert_client.revoke(josepy.ComparableX509(cert), 1)
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-a.cert.pem", "http://localhost:4002", "revoked", "keyCompromise")
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-*.cert.pem", "http://localhost:4002", "revoked", "keyCompromise")
|
||||
verify_akamai_purge()
|
||||
|
||||
# A subsequent attempt should fail, because the cert is already revoked
|
||||
|
@ -1229,7 +1229,7 @@ def test_auth_deactivation_v2():
|
|||
def test_ocsp():
|
||||
cert_file = temppath('test_ocsp.pem')
|
||||
chisel2.auth_and_issue([random_domain()], cert_output=cert_file.name)
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-a.cert.pem", "http://localhost:4002", "good")
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-*.cert.pem", "http://localhost:4002", "good")
|
||||
|
||||
def test_ct_submission():
|
||||
hostname = random_domain()
|
||||
|
@ -1301,15 +1301,22 @@ ocsp_exp_unauth_setup_data = {}
|
|||
def ocsp_exp_unauth_setup():
|
||||
client = chisel2.make_client(None)
|
||||
cert_file = temppath('ocsp_exp_unauth_setup.pem')
|
||||
order = chisel2.auth_and_issue([random_domain()], client=client, cert_output=cert_file.name)
|
||||
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem)
|
||||
chisel2.auth_and_issue([random_domain()], client=client, cert_output=cert_file.name)
|
||||
|
||||
# Since our servers are pretending to be in the past, but the openssl cli
|
||||
# isn't, we'll get an expired OCSP response. Just check that it exists;
|
||||
# don't do the full verification (which would fail).
|
||||
check_ocsp_basic_oid(cert_file.name, "/hierarchy/int-rsa-a.cert.pem", "http://localhost:4002")
|
||||
global ocsp_exp_unauth_setup_data
|
||||
ocsp_exp_unauth_setup_data['cert_file'] = cert_file.name
|
||||
lastException = None
|
||||
for issuer_file in glob.glob("/hierarchy/int-rsa-*.cert.pem"):
|
||||
try:
|
||||
check_ocsp_basic_oid(cert_file.name, issuer_file, "http://localhost:4002")
|
||||
global ocsp_exp_unauth_setup_data
|
||||
ocsp_exp_unauth_setup_data['cert_file'] = cert_file.name
|
||||
return
|
||||
except Exception as e:
|
||||
lastException = e
|
||||
continue
|
||||
raise(lastException)
|
||||
|
||||
def test_ocsp_exp_unauth():
|
||||
tries = 0
|
||||
|
@ -1319,7 +1326,7 @@ def test_ocsp_exp_unauth():
|
|||
last_error = ""
|
||||
while tries < 5:
|
||||
try:
|
||||
verify_ocsp(cert_file, "/hierarchy/int-rsa-a.cert.pem", "http://localhost:4002", "XXX")
|
||||
verify_ocsp(cert_file, "/hierarchy/int-rsa-*.cert.pem", "http://localhost:4002", "XXX")
|
||||
raise(Exception("Unexpected return from verify_ocsp"))
|
||||
except subprocess.CalledProcessError as cpe:
|
||||
last_error = cpe.output
|
||||
|
@ -1590,7 +1597,7 @@ def test_admin_revoker_cert():
|
|||
"-reason", "keyCompromise"])
|
||||
|
||||
# Wait for OCSP response to indicate revocation took place
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-a.cert.pem", "http://localhost:4002", "revoked", "keyCompromise")
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-*.cert.pem", "http://localhost:4002", "revoked", "keyCompromise")
|
||||
verify_akamai_purge()
|
||||
|
||||
def test_admin_revoker_batched():
|
||||
|
@ -1615,7 +1622,7 @@ def test_admin_revoker_batched():
|
|||
"-parallelism", "2"])
|
||||
|
||||
for cert_file in cert_files:
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-a.cert.pem", "http://localhost:4002", "revoked", "unspecified")
|
||||
verify_ocsp(cert_file.name, "/hierarchy/int-rsa-*.cert.pem", "http://localhost:4002", "revoked", "unspecified")
|
||||
|
||||
def test_sct_embedding():
|
||||
order = chisel2.auth_and_issue([random_domain()])
|
||||
|
@ -1661,9 +1668,9 @@ def test_auth_deactivation():
|
|||
if resp.body.status is not messages.STATUS_DEACTIVATED:
|
||||
raise Exception("unexpected authorization status")
|
||||
|
||||
def get_ocsp_response_and_reason(cert_file, issuer_file, url):
|
||||
def get_ocsp_response_and_reason(cert_file, issuer_glob, url):
|
||||
"""Returns the ocsp response output and revocation reason."""
|
||||
output = verify_ocsp(cert_file, issuer_file, url, None)
|
||||
output = verify_ocsp(cert_file, issuer_glob, url, None)
|
||||
m = re.search('Reason: (\w+)', output)
|
||||
reason = m.group(1) if m is not None else ""
|
||||
return output, reason
|
||||
|
@ -1687,7 +1694,7 @@ def ocsp_resigning_setup():
|
|||
client.revoke(josepy.ComparableX509(cert), 5)
|
||||
|
||||
ocsp_response, reason = get_ocsp_response_and_reason(
|
||||
cert_file.name, "/hierarchy/int-rsa-a.cert.pem", "http://localhost:4002")
|
||||
cert_file.name, "/hierarchy/int-rsa-*.cert.pem", "http://localhost:4002")
|
||||
global ocsp_resigning_setup_data
|
||||
ocsp_resigning_setup_data = {
|
||||
'cert_file': cert_file.name,
|
||||
|
@ -1703,7 +1710,7 @@ def test_ocsp_resigning():
|
|||
tries = 0
|
||||
while tries < 5:
|
||||
resp, reason = get_ocsp_response_and_reason(
|
||||
ocsp_resigning_setup_data['cert_file'], "/hierarchy/int-rsa-a.cert.pem", "http://localhost:4002")
|
||||
ocsp_resigning_setup_data['cert_file'], "/hierarchy/int-rsa-*.cert.pem", "http://localhost:4002")
|
||||
if resp != ocsp_resigning_setup_data['response']:
|
||||
break
|
||||
tries += 1
|
||||
|
|
Loading…
Reference in New Issue