Separate issuance.Profile out from issuance.Issuer (#7285)

Remove the Profile field from issuance.Issuer, to reflect the fact that
profiles are in fact independent pieces of configuration which can be
shared across (and are configured independently of) multiple issuers.

Move the IssuerURL, OCSPUrl, and CRLURL fields from issuance.Profile to
issuance.Issuer, since they reflect fundamental attributes of the
issuer, rather than attributes of a particular profile. This also
reflects the location at which those values are configured, in
issuance.IssuerConfig.

All other changes are fallout from the above: adding a Profile argument
to various methods in the issuance and linting packages, adding a
profile field to the caImpl struct, etc. This change paves the way for
two future changes: moving OCSP and CRL creation into the issuance
package, and supporting multiple simultaneous profiles that the CA can
select between.

Part of https://github.com/letsencrypt/boulder/issues/7159
Part of https://github.com/letsencrypt/boulder/issues/6316
Part of https://github.com/letsencrypt/boulder/issues/6966
This commit is contained in:
Aaron Gable 2024-02-06 17:06:56 -08:00 committed by GitHub
parent 0e9f5d3545
commit af2c1a5963
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 547 additions and 481 deletions

View File

@ -60,6 +60,7 @@ type certificateAuthorityImpl struct {
sa sapb.StorageAuthorityCertificateClient
pa core.PolicyAuthority
issuers issuerMaps
profile *issuance.Profile
// This is temporary, and will be used for testing and slow roll-out
// of ECDSA issuance, but will then be removed.
@ -103,6 +104,7 @@ func NewCertificateAuthorityImpl(
sa sapb.StorageAuthorityCertificateClient,
pa core.PolicyAuthority,
boulderIssuers []*issuance.Issuer,
certificateProfile *issuance.Profile,
ecdsaAllowList *ECDSAAllowList,
certExpiry time.Duration,
certBackdate time.Duration,
@ -134,6 +136,10 @@ 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")
}
issuers := makeIssuerMaps(boulderIssuers)
lintErrorCount := prometheus.NewCounter(
@ -147,6 +153,7 @@ func NewCertificateAuthorityImpl(
sa: sa,
pa: pa,
issuers: issuers,
profile: certificateProfile,
validityPeriod: certExpiry,
backdate: certBackdate,
prefix: serialPrefix,
@ -281,7 +288,7 @@ func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
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(issuanceReq)
_, issuanceToken, err := issuer.Prepare(ca.profile, issuanceReq)
if err != nil {
ca.log.AuditErrf("Preparing cert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]",
serialHex, req.RegistrationID, names, err)
@ -439,7 +446,7 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
NotAfter: validity.NotAfter,
}
lintCertBytes, issuanceToken, err := issuer.Prepare(req)
lintCertBytes, issuanceToken, err := issuer.Prepare(ca.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)

View File

@ -2,7 +2,6 @@ package ca
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
@ -32,7 +31,6 @@ import (
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey"
"github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/linter"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/must"
@ -110,6 +108,7 @@ type testCtx struct {
pa core.PolicyAuthority
ocsp *ocspImpl
crl *crlImpl
profile *issuance.Profile
certExpiry time.Duration
certBackdate time.Duration
serialPrefix int
@ -152,30 +151,8 @@ func (m *mockSA) SetCertificateStatusReady(ctx context.Context, req *sapb.Serial
return &emptypb.Empty{}, nil
}
var caKey crypto.Signer
var caCert *issuance.Certificate
var caCert2 *issuance.Certificate
var caLinter *linter.Linter
var caLinter2 *linter.Linter
var ctx = context.Background()
func init() {
var err error
caCert, caKey, err = issuance.LoadIssuer(issuance.IssuerLoc{
File: caKeyFile,
CertFile: caCertFile,
})
if err != nil {
panic(fmt.Sprintf("Unable to load %q and %q: %s", caKeyFile, caCertFile, err))
}
caCert2, err = issuance.LoadCertificate(caCertFile2)
if err != nil {
panic(fmt.Sprintf("Unable to parse %q: %s", caCertFile2, err))
}
caLinter, _ = linter.New(caCert.Certificate, caKey, []string{"w_subject_common_name_included"})
caLinter2, _ = linter.New(caCert2.Certificate, caKey, []string{"w_subject_common_name_included"})
}
func setup(t *testing.T) *testCtx {
features.Reset()
fc := clock.NewFake()
@ -186,46 +163,44 @@ func setup(t *testing.T) *testCtx {
err = pa.LoadHostnamePolicyFile("../test/hostname-policy.yaml")
test.AssertNotError(t, err, "Couldn't set hostname policy")
boulderProfile := func(rsa, ecdsa bool) *issuance.Profile {
res, _ := 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},
boulderProfile, err := issuance.NewProfile(
issuance.ProfileConfig{
AllowMustStaple: true,
AllowCTPoison: true,
AllowSCTList: true,
AllowCommonName: true,
Policies: []issuance.PolicyConfig{
{OID: "2.23.140.1.2.1"},
},
issuance.IssuerConfig{
UseForECDSALeaves: ecdsa,
UseForRSALeaves: rsa,
IssuerURL: "http://not-example.com/issuer-url",
OCSPURL: "http://not-example.com/ocsp",
CRLURL: "http://not-example.com/crl",
},
)
return res
}
boulderIssuers := []*issuance.Issuer{
// Must list ECDSA-only issuer first, so it is the default for ECDSA.
{
Cert: caCert2,
Signer: caKey,
Profile: boulderProfile(false, true),
Linter: caLinter2,
Clk: fc,
MaxValidityPeriod: config.Duration{Duration: time.Hour * 8760},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
},
{
Cert: caCert,
Signer: caKey,
Profile: boulderProfile(true, true),
Linter: caLinter,
Clk: fc,
},
}
[]string{"w_subject_common_name_included"},
)
test.AssertNotError(t, err, "Couldn't create test profile")
ecdsaOnlyIssuer, err := issuance.LoadIssuer(issuance.IssuerConfig{
UseForRSALeaves: false,
UseForECDSALeaves: true,
IssuerURL: "http://not-example.com/issuer-url",
OCSPURL: "http://not-example.com/ocsp",
CRLURL: "http://not-example.com/crl",
Location: issuance.IssuerLoc{File: caKeyFile, CertFile: caCertFile2},
}, 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",
CRLURL: "http://not-example.com/crl",
Location: issuance.IssuerLoc{File: caKeyFile, CertFile: caCertFile},
}, 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}
keyPolicy := goodkey.KeyPolicy{
AllowRSA: true,
@ -258,6 +233,7 @@ func setup(t *testing.T) *testCtx {
crl, err := NewCRLImpl(
boulderIssuers,
boulderProfile.Lints,
time.Hour,
"http://c.boulder.test",
100,
@ -269,6 +245,7 @@ func setup(t *testing.T) *testCtx {
pa: pa,
ocsp: ocsp,
crl: crl,
profile: boulderProfile,
certExpiry: 8760 * time.Hour,
certBackdate: time.Hour,
serialPrefix: 17,
@ -291,6 +268,7 @@ func TestSerialPrefix(t *testing.T) {
nil,
nil,
nil,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
0,
@ -308,6 +286,7 @@ func TestSerialPrefix(t *testing.T) {
nil,
nil,
nil,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
128,
@ -400,6 +379,7 @@ func issueCertificateSubTestSetup(t *testing.T, e *ECDSAAllowList) (*certificate
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
e,
testCtx.certExpiry,
testCtx.certBackdate,
@ -445,6 +425,7 @@ func TestNoIssuers(t *testing.T) {
sa,
testCtx.pa,
nil, // No issuers
testCtx.profile,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -468,6 +449,7 @@ func TestMultipleIssuers(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -481,23 +463,46 @@ func TestMultipleIssuers(t *testing.T) {
testCtx.fc)
test.AssertNotError(t, err, "Failed to remake CA")
// Test that an RSA CSR gets issuance from the RSA issuer, caCert.
// Test that an RSA CSR gets issuance from the RSA issuer.
issuedCert, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID})
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(caCert2.Certificate)
err = cert.CheckSignatureFrom(testCtx.boulderIssuers[1].Cert.Certificate)
test.AssertNotError(t, err, "Certificate failed signature validation")
// Test that an ECDSA CSR gets issuance from the ECDSA issuer, caCert2.
// Test that an ECDSA CSR gets issuance from the ECDSA issuer.
issuedCert, err = ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID})
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(caCert2.Certificate)
err = cert.CheckSignatureFrom(testCtx.boulderIssuers[0].Cert.Certificate)
test.AssertNotError(t, err, "Certificate failed signature validation")
}
func TestNoProfile(t *testing.T) {
testCtx := setup(t)
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")
}
func TestECDSAAllowList(t *testing.T) {
req := &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID}
@ -508,7 +513,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, caCert2.RawSubject)
test.AssertByteEquals(t, cert.RawIssuer, ca.issuers.byAlg[x509.ECDSA].Cert.RawSubject)
// With allowlist not containing arbitraryRegID, issuance should fall back to RSA issuer.
regIDMap = makeRegIDsMap([]int64{2002})
@ -517,7 +522,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, caCert.RawSubject)
test.AssertByteEquals(t, cert.RawIssuer, ca.issuers.byAlg[x509.RSA].Cert.RawSubject)
// With empty allowlist but ECDSAForAll enabled, issuance should come from ECDSA issuer.
ca, _ = issueCertificateSubTestSetup(t, nil)
@ -527,7 +532,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, caCert2.RawSubject)
test.AssertByteEquals(t, cert.RawIssuer, ca.issuers.byAlg[x509.ECDSA].Cert.RawSubject)
}
func TestInvalidCSRs(t *testing.T) {
@ -589,6 +594,7 @@ func TestInvalidCSRs(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -625,6 +631,7 @@ func TestRejectValidityTooLong(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -725,6 +732,7 @@ func TestIssueCertificateForPrecertificate(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -830,6 +838,7 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
sa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -871,6 +880,7 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
errorsa,
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
nil,
testCtx.certExpiry,
testCtx.certBackdate,

View File

@ -12,6 +12,8 @@ import (
"strings"
"time"
"github.com/zmap/zlint/v3/lint"
capb "github.com/letsencrypt/boulder/ca/proto"
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
@ -22,7 +24,9 @@ import (
type crlImpl struct {
capb.UnimplementedCRLGeneratorServer
issuers map[issuance.NameID]*issuance.Issuer
issuers map[issuance.NameID]*issuance.Issuer
// TODO(#7159): Move this into the CRL profile
lints lint.Registry
lifetime time.Duration
idpBase string
maxLogLen int
@ -34,7 +38,7 @@ type crlImpl struct {
// issue CRLs from. lifetime sets the validity period (inclusive) of the
// resulting CRLs. idpBase is the base URL from which IssuingDistributionPoint
// URIs will constructed; it must use the http:// scheme.
func NewCRLImpl(issuers []*issuance.Issuer, lifetime time.Duration, idpBase string, maxLogLen int, logger blog.Logger) (*crlImpl, error) {
func NewCRLImpl(issuers []*issuance.Issuer, lints lint.Registry, lifetime time.Duration, idpBase string, maxLogLen int, logger blog.Logger) (*crlImpl, error) {
issuersByNameID := make(map[issuance.NameID]*issuance.Issuer, len(issuers))
for _, issuer := range issuers {
issuersByNameID[issuer.NameID()] = issuer
@ -58,6 +62,7 @@ func NewCRLImpl(issuers []*issuance.Issuer, lifetime time.Duration, idpBase stri
return &crlImpl{
issuers: issuersByNameID,
lints: lints,
lifetime: lifetime,
idpBase: idpBase,
maxLogLen: maxLogLen,
@ -152,7 +157,7 @@ func (ci *crlImpl) GenerateCRL(stream capb.CRLGenerator_GenerateCRLServer) error
template.RevokedCertificateEntries = rcs
err = issuer.Linter.CheckCRL(template)
err = issuer.Linter.CheckCRL(template, ci.lints)
if err != nil {
return err
}

View File

@ -7,12 +7,13 @@ import (
"testing"
"time"
"golang.org/x/crypto/ocsp"
capb "github.com/letsencrypt/boulder/ca/proto"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/test"
"golang.org/x/crypto/ocsp"
)
func TestImplementationOCSP(t *testing.T) {
@ -34,6 +35,7 @@ func TestOCSP(t *testing.T) {
&mockSA{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.profile,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
@ -60,7 +62,7 @@ func TestOCSP(t *testing.T) {
Status: string(core.OCSPStatusGood),
})
test.AssertNotError(t, err, "Failed to generate OCSP")
rsaOCSP, err := ocsp.ParseResponse(rsaOCSPPB.Response, caCert.Certificate)
rsaOCSP, err := ocsp.ParseResponse(rsaOCSPPB.Response, testCtx.boulderIssuers[1].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)
@ -78,7 +80,7 @@ func TestOCSP(t *testing.T) {
Status: string(core.OCSPStatusGood),
})
test.AssertNotError(t, err, "Failed to generate OCSP")
ecdsaOCSP, err := ocsp.ParseResponse(ecdsaOCSPPB.Response, caCert2.Certificate)
ecdsaOCSP, err := ocsp.ParseResponse(ecdsaOCSPPB.Response, testCtx.boulderIssuers[0].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)

View File

@ -18,7 +18,6 @@ import (
"github.com/letsencrypt/boulder/goodkey/sagoodkey"
bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/linter"
"github.com/letsencrypt/boulder/policy"
sapb "github.com/letsencrypt/boulder/sa/proto"
)
@ -115,34 +114,6 @@ type Config struct {
OpenTelemetry cmd.OpenTelemetryConfig
}
func loadBoulderIssuers(profileConfig issuance.ProfileConfig, issuerConfigs []issuance.IssuerConfig, ignoredLints []string) ([]*issuance.Issuer, error) {
issuers := make([]*issuance.Issuer, 0, len(issuerConfigs))
for _, issuerConfig := range issuerConfigs {
profile, err := issuance.NewProfile(profileConfig, issuerConfig)
if err != nil {
return nil, err
}
cert, signer, err := issuance.LoadIssuer(issuerConfig.Location)
if err != nil {
return nil, err
}
linter, err := linter.New(cert.Certificate, signer, ignoredLints)
if err != nil {
return nil, err
}
issuer, err := issuance.NewIssuer(cert, signer, profile, linter, cmd.Clock())
if err != nil {
return nil, err
}
issuers = append(issuers, issuer)
}
return issuers, nil
}
func main() {
grpcAddr := flag.String("addr", "", "gRPC listen address override")
debugAddr := flag.String("debug-addr", "", "Debug server address override")
@ -212,9 +183,15 @@ func main() {
cmd.FailOnError(err, "Failed to load CT Log List")
}
var boulderIssuers []*issuance.Issuer
boulderIssuers, err = loadBoulderIssuers(c.CA.Issuance.Profile, c.CA.Issuance.Issuers, c.CA.Issuance.IgnoredLints)
cmd.FailOnError(err, "Couldn't load issuers")
issuers := make([]*issuance.Issuer, 0, len(c.CA.Issuance.Issuers))
for _, issuerConfig := range c.CA.Issuance.Issuers {
issuer, err := issuance.LoadIssuer(issuerConfig, cmd.Clock())
cmd.FailOnError(err, "Loading issuer")
issuers = append(issuers, issuer)
}
profile, err := issuance.NewProfile(c.CA.Issuance.Profile, c.CA.Issuance.IgnoredLints)
cmd.FailOnError(err, "Couldn't load issuance profile")
tlsConfig, err := c.CA.TLS.Load(scope)
cmd.FailOnError(err, "TLS config")
@ -241,7 +218,7 @@ func main() {
if !c.CA.DisableOCSPService {
ocspi, err := ca.NewOCSPImpl(
boulderIssuers,
issuers,
c.CA.LifespanOCSP.Duration,
c.CA.OCSPLogMaxLength,
c.CA.OCSPLogPeriod.Duration,
@ -260,7 +237,8 @@ func main() {
if !c.CA.DisableCRLService {
crli, err := ca.NewCRLImpl(
boulderIssuers,
issuers,
profile.Lints,
c.CA.LifespanCRL.Duration,
c.CA.CRLDPBase,
c.CA.OCSPLogMaxLength,
@ -275,7 +253,8 @@ func main() {
cai, err := ca.NewCertificateAuthorityImpl(
sa,
pa,
boulderIssuers,
issuers,
profile,
ecdsaAllowList,
c.CA.Expiry.Duration,
c.CA.Backdate.Duration,

View File

@ -10,6 +10,8 @@ import (
"testing"
"time"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/test"
@ -52,10 +54,16 @@ func TestValidate(t *testing.T) {
}
func TestDiff(t *testing.T) {
issuer, signer, err := issuance.LoadIssuer(issuance.IssuerLoc{
File: "../../test/hierarchy/int-e1.key.pem",
CertFile: "../../test/hierarchy/int-e1.cert.pem",
})
issuer, err := issuance.LoadIssuer(
issuance.IssuerConfig{
Location: issuance.IssuerLoc{
File: "../../test/hierarchy/int-e1.key.pem",
CertFile: "../../test/hierarchy/int-e1.cert.pem",
},
IssuerURL: "http://not-example.com/issuer-url",
OCSPURL: "http://not-example.com/ocsp",
CRLURL: "http://not-example.com/crl",
}, clock.NewFake())
test.AssertNotError(t, err, "loading test issuer")
now := time.Now()
@ -75,7 +83,7 @@ func TestDiff(t *testing.T) {
},
}
oldCRLDER, err := x509.CreateRevocationList(rand.Reader, &template, issuer.Certificate, signer)
oldCRLDER, err := x509.CreateRevocationList(rand.Reader, &template, issuer.Cert.Certificate, issuer.Signer)
test.AssertNotError(t, err, "creating old crl")
oldCRL, err := x509.ParseRevocationList(oldCRLDER)
test.AssertNotError(t, err, "parsing old crl")
@ -97,7 +105,7 @@ func TestDiff(t *testing.T) {
},
}
newCRLDER, err := x509.CreateRevocationList(rand.Reader, &template, issuer.Certificate, signer)
newCRLDER, err := x509.CreateRevocationList(rand.Reader, &template, issuer.Cert.Certificate, issuer.Signer)
test.AssertNotError(t, err, "creating old crl")
newCRL, err := x509.ParseRevocationList(newCRLDER)
test.AssertNotError(t, err, "parsing old crl")

View File

@ -57,20 +57,26 @@ func setupTestUploadCRL(t *testing.T) (*crlStorer, *issuance.Issuer) {
r3, err := issuance.LoadCertificate("../../test/hierarchy/int-r3.cert.pem")
test.AssertNotError(t, err, "loading fake RSA issuer cert")
e1, e1Signer, err := issuance.LoadIssuer(issuance.IssuerLoc{
File: "../../test/hierarchy/int-e1.key.pem",
CertFile: "../../test/hierarchy/int-e1.cert.pem",
})
issuerE1, err := issuance.LoadIssuer(
issuance.IssuerConfig{
Location: issuance.IssuerLoc{
File: "../../test/hierarchy/int-e1.key.pem",
CertFile: "../../test/hierarchy/int-e1.cert.pem",
},
IssuerURL: "http://not-example.com/issuer-url",
OCSPURL: "http://not-example.com/ocsp",
CRLURL: "http://not-example.com/crl",
}, clock.NewFake())
test.AssertNotError(t, err, "loading fake ECDSA issuer cert")
storer, err := New(
[]*issuance.Certificate{r3, e1},
[]*issuance.Certificate{r3, issuerE1.Cert},
nil, "le-crl.s3.us-west.amazonaws.com",
metrics.NoopRegisterer, blog.NewMock(), clock.NewFake(),
)
test.AssertNotError(t, err, "creating test crl-storer")
return storer, &issuance.Issuer{Cert: e1, Signer: e1Signer}
return storer, issuerE1
}
// Test that we get an error when no metadata is sent.

View File

@ -19,8 +19,10 @@ import (
cttls "github.com/google/certificate-transparency-go/tls"
ctx509 "github.com/google/certificate-transparency-go/x509"
"github.com/jmhodges/clock"
"github.com/zmap/zlint/v3/lint"
"github.com/letsencrypt/boulder/config"
"github.com/letsencrypt/boulder/linter"
"github.com/letsencrypt/boulder/precert"
)
@ -45,45 +47,34 @@ type PolicyConfig struct {
// Profile is the validated structure created by reading in ProfileConfigs and IssuerConfigs
type Profile struct {
useForRSALeaves bool
useForECDSALeaves bool
allowMustStaple bool
allowCTPoison bool
allowSCTList bool
allowCommonName bool
sigAlg x509.SignatureAlgorithm
ocspURL string
crlURL string
issuerURL string
maxBackdate time.Duration
maxValidity time.Duration
// TODO(#7159): Make this private when CRLs have their own profile.
Lints lint.Registry
}
// NewProfile synthesizes the profile config and issuer config into a single
// object, and checks various aspects for correctness.
func NewProfile(profileConfig ProfileConfig, issuerConfig IssuerConfig) (*Profile, error) {
if issuerConfig.IssuerURL == "" {
return nil, errors.New("Issuer URL is required")
}
if issuerConfig.OCSPURL == "" {
return nil, errors.New("OCSP URL is required")
func NewProfile(profileConfig ProfileConfig, skipLints []string) (*Profile, error) {
reg, err := linter.NewRegistry(skipLints)
if err != nil {
return nil, fmt.Errorf("creating lint registry: %w", err)
}
sp := &Profile{
useForRSALeaves: issuerConfig.UseForRSALeaves,
useForECDSALeaves: issuerConfig.UseForECDSALeaves,
allowMustStaple: profileConfig.AllowMustStaple,
allowCTPoison: profileConfig.AllowCTPoison,
allowSCTList: profileConfig.AllowSCTList,
allowCommonName: profileConfig.AllowCommonName,
issuerURL: issuerConfig.IssuerURL,
crlURL: issuerConfig.CRLURL,
ocspURL: issuerConfig.OCSPURL,
maxBackdate: profileConfig.MaxValidityBackdate.Duration,
maxValidity: profileConfig.MaxValidityPeriod.Duration,
allowMustStaple: profileConfig.AllowMustStaple,
allowCTPoison: profileConfig.AllowCTPoison,
allowSCTList: profileConfig.AllowSCTList,
allowCommonName: profileConfig.AllowCommonName,
maxBackdate: profileConfig.MaxValidityBackdate.Duration,
maxValidity: profileConfig.MaxValidityPeriod.Duration,
Lints: reg,
}
return sp, nil
@ -91,14 +82,14 @@ func NewProfile(profileConfig ProfileConfig, issuerConfig IssuerConfig) (*Profil
// requestValid verifies the passed IssuanceRequest against the profile. If the
// request doesn't match the signing profile an error is returned.
func (p *Profile) requestValid(clk clock.Clock, req *IssuanceRequest) error {
func (i *Issuer) requestValid(clk clock.Clock, prof *Profile, req *IssuanceRequest) error {
switch req.PublicKey.(type) {
case *rsa.PublicKey:
if !p.useForRSALeaves {
if !i.useForRSALeaves {
return errors.New("cannot sign RSA public keys")
}
case *ecdsa.PublicKey:
if !p.useForECDSALeaves {
if !i.useForECDSALeaves {
return errors.New("cannot sign ECDSA public keys")
}
default:
@ -109,15 +100,15 @@ func (p *Profile) requestValid(clk clock.Clock, req *IssuanceRequest) error {
return errors.New("unexpected subject key ID length")
}
if !p.allowMustStaple && req.IncludeMustStaple {
if !prof.allowMustStaple && req.IncludeMustStaple {
return errors.New("must-staple extension cannot be included")
}
if !p.allowCTPoison && req.IncludeCTPoison {
if !prof.allowCTPoison && req.IncludeCTPoison {
return errors.New("ct poison extension cannot be included")
}
if !p.allowSCTList && req.sctList != nil {
if !prof.allowSCTList && req.sctList != nil {
return errors.New("sct list extension cannot be included")
}
@ -125,7 +116,7 @@ func (p *Profile) requestValid(clk clock.Clock, req *IssuanceRequest) error {
return errors.New("cannot include both ct poison and sct list extensions")
}
if !p.allowCommonName && req.CommonName != "" {
if !prof.allowCommonName && req.CommonName != "" {
return errors.New("common name cannot be included")
}
@ -135,12 +126,12 @@ func (p *Profile) requestValid(clk clock.Clock, req *IssuanceRequest) error {
if validity <= 0 {
return errors.New("NotAfter must be after NotBefore")
}
if validity > p.maxValidity {
return fmt.Errorf("validity period is more than the maximum allowed period (%s>%s)", validity, p.maxValidity)
if validity > prof.maxValidity {
return fmt.Errorf("validity period is more than the maximum allowed period (%s>%s)", validity, prof.maxValidity)
}
backdatedBy := clk.Now().Sub(req.NotBefore)
if backdatedBy > p.maxBackdate {
return fmt.Errorf("NotBefore is backdated more than the maximum allowed period (%s>%s)", backdatedBy, p.maxBackdate)
if backdatedBy > prof.maxBackdate {
return fmt.Errorf("NotBefore is backdated more than the maximum allowed period (%s>%s)", backdatedBy, prof.maxBackdate)
}
if backdatedBy < 0 {
return errors.New("NotBefore is in the future")
@ -156,24 +147,22 @@ func (p *Profile) requestValid(clk clock.Clock, req *IssuanceRequest) error {
return nil
}
var defaultEKU = []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
}
func (p *Profile) generateTemplate() *x509.Certificate {
func (i *Issuer) generateTemplate() *x509.Certificate {
template := &x509.Certificate{
SignatureAlgorithm: p.sigAlg,
ExtKeyUsage: defaultEKU,
OCSPServer: []string{p.ocspURL},
IssuingCertificateURL: []string{p.issuerURL},
SignatureAlgorithm: i.sigAlg,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
},
OCSPServer: []string{i.ocspURL},
IssuingCertificateURL: []string{i.issuerURL},
BasicConstraintsValid: true,
// Baseline Requirements, Section 7.1.6.1: domain-validated
PolicyIdentifiers: []asn1.ObjectIdentifier{{2, 23, 140, 1, 2, 1}},
}
if p.crlURL != "" {
template.CRLDistributionPoints = []string{p.crlURL}
if i.crlURL != "" {
template.CRLDistributionPoints = []string{i.crlURL}
}
return template
@ -261,20 +250,21 @@ type issuanceToken struct {
issuer *Issuer
}
// Prepare applies this Issuer's profile to create a template certificate. It
// then generates a linting certificate from that template and runs the linter
// over it. If successful, returns both the linting certificate (which can be
// stored) and an issuanceToken. The issuanceToken can be used to sign a
// matching certificate with this Issuer's private key.
func (i *Issuer) Prepare(req *IssuanceRequest) ([]byte, *issuanceToken, error) {
// Prepare combines the given profile and request with the Issuer's information
// to create a template certificate. It then generates a linting certificate
// from that template and runs the linter over it. If successful, returns both
// the linting certificate (which can be stored) and an issuanceToken. The
// issuanceToken can be used to sign a matching certificate with this Issuer's
// private key.
func (i *Issuer) Prepare(prof *Profile, req *IssuanceRequest) ([]byte, *issuanceToken, error) {
// check request is valid according to the issuance profile
err := i.Profile.requestValid(i.Clk, req)
err := i.requestValid(i.Clk, prof, req)
if err != nil {
return nil, nil, err
}
// generate template from the issuance profile
template := i.Profile.generateTemplate()
// generate template from the issuer's data
template := i.generateTemplate()
// populate template from the issuance request
template.NotBefore, template.NotAfter = req.NotBefore, req.NotAfter
@ -314,7 +304,7 @@ func (i *Issuer) Prepare(req *IssuanceRequest) ([]byte, *issuanceToken, error) {
// check that the tbsCertificate is properly formed by signing it
// with a throwaway key and then linting it using zlint
lintCertBytes, err := i.Linter.Check(template, req.PublicKey)
lintCertBytes, err := i.Linter.Check(template, req.PublicKey, prof.Lints)
if err != nil {
return nil, nil, fmt.Errorf("tbsCertificate linting failed: %w", err)
}
@ -352,6 +342,9 @@ func (i *Issuer) Issue(token *issuanceToken) ([]byte, error) {
return x509.CreateCertificate(rand.Reader, template, i.Cert.Certificate, token.pubKey, i.Signer)
}
// ContainsMustStaple returns true if the provided set of extensions includes
// an entry whose OID and value both match the expected values for the OCSP
// Must-Staple (a.k.a. id-pe-tlsFeature) extension.
func ContainsMustStaple(extensions []pkix.Extension) bool {
for _, ext := range extensions {
if ext.Id.Equal(mustStapleExt.Id) && bytes.Equal(ext.Value, mustStapleExt.Value) {
@ -361,6 +354,9 @@ func ContainsMustStaple(extensions []pkix.Extension) bool {
return false
}
// containsCTPoison returns true if the provided set of extensions includes
// an entry whose OID and value both match the expected values for the CT
// Poison extension.
func containsCTPoison(extensions []pkix.Extension) bool {
for _, ext := range extensions {
if ext.Id.Equal(ctPoisonExt.Id) && bytes.Equal(ext.Value, asn1.NullBytes) {

View File

@ -27,70 +27,63 @@ var (
)
func defaultProfile() *Profile {
p, _ := NewProfile(defaultProfileConfig(), defaultIssuerConfig())
p, _ := NewProfile(defaultProfileConfig(), []string{
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
})
return p
}
func TestNewProfileNoIssuerURL(t *testing.T) {
_, err := NewProfile(ProfileConfig{}, IssuerConfig{})
test.AssertError(t, err, "NewProfile didn't fail with no issuer URL")
test.AssertEquals(t, err.Error(), "Issuer URL is required")
}
func TestNewProfileNoOCSPURL(t *testing.T) {
_, err := NewProfile(ProfileConfig{}, IssuerConfig{IssuerURL: "issuer-url"})
test.AssertError(t, err, "NewProfile didn't fail with no OCSP URL")
test.AssertEquals(t, err.Error(), "OCSP URL is required")
}
func TestRequestValid(t *testing.T) {
fc := clock.NewFake()
fc.Add(time.Hour * 24)
tests := []struct {
name string
issuer *Issuer
profile *Profile
request *IssuanceRequest
expectedError string
}{
{
name: "unsupported key type",
issuer: &Issuer{},
profile: &Profile{},
request: &IssuanceRequest{PublicKey: &dsa.PublicKey{}},
expectedError: "unsupported public key type",
},
{
name: "cannot sign rsa",
issuer: &Issuer{},
profile: &Profile{},
request: &IssuanceRequest{PublicKey: &rsa.PublicKey{}},
expectedError: "cannot sign RSA public keys",
},
{
name: "cannot sign ecdsa",
issuer: &Issuer{},
profile: &Profile{},
request: &IssuanceRequest{PublicKey: &ecdsa.PublicKey{}},
expectedError: "cannot sign ECDSA public keys",
},
{
name: "skid too short",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
maxValidity: time.Hour * 2,
},
profile: &Profile{},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: []byte{0, 1, 2, 3, 4},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
},
expectedError: "unexpected subject key ID length",
},
{
name: "must staple not allowed",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
},
profile: &Profile{},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
@ -100,9 +93,10 @@ func TestRequestValid(t *testing.T) {
},
{
name: "ct poison not allowed",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
},
profile: &Profile{},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
@ -112,9 +106,10 @@ func TestRequestValid(t *testing.T) {
},
{
name: "sct list not allowed",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
},
profile: &Profile{},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
@ -124,10 +119,12 @@ func TestRequestValid(t *testing.T) {
},
{
name: "sct list and ct poison not allowed",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
allowCTPoison: true,
allowSCTList: true,
},
profile: &Profile{
allowCTPoison: true,
allowSCTList: true,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
@ -139,9 +136,10 @@ func TestRequestValid(t *testing.T) {
},
{
name: "common name not allowed",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
},
profile: &Profile{},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
@ -151,9 +149,10 @@ func TestRequestValid(t *testing.T) {
},
{
name: "negative validity",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
},
profile: &Profile{},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
@ -164,9 +163,11 @@ func TestRequestValid(t *testing.T) {
},
{
name: "validity larger than max",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
maxValidity: time.Minute,
},
profile: &Profile{
maxValidity: time.Minute,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
@ -178,9 +179,11 @@ func TestRequestValid(t *testing.T) {
},
{
name: "validity larger than max due to inclusivity",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
maxValidity: time.Hour,
},
profile: &Profile{
maxValidity: time.Hour,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
@ -192,10 +195,12 @@ func TestRequestValid(t *testing.T) {
},
{
name: "validity backdated more than max",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
maxValidity: time.Hour * 2,
maxBackdate: time.Hour,
},
profile: &Profile{
maxValidity: time.Hour * 2,
maxBackdate: time.Hour,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
@ -207,10 +212,12 @@ func TestRequestValid(t *testing.T) {
},
{
name: "validity is forward dated",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
maxValidity: time.Hour * 2,
maxBackdate: time.Hour,
},
profile: &Profile{
maxValidity: time.Hour * 2,
maxBackdate: time.Hour,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
@ -222,9 +229,11 @@ func TestRequestValid(t *testing.T) {
},
{
name: "serial too short",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
maxValidity: time.Hour * 2,
},
profile: &Profile{
maxValidity: time.Hour * 2,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
@ -237,9 +246,11 @@ func TestRequestValid(t *testing.T) {
},
{
name: "serial too long",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
maxValidity: time.Hour * 2,
},
profile: &Profile{
maxValidity: time.Hour * 2,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
@ -252,9 +263,11 @@ func TestRequestValid(t *testing.T) {
},
{
name: "good",
profile: &Profile{
issuer: &Issuer{
useForECDSALeaves: true,
maxValidity: time.Hour * 2,
},
profile: &Profile{
maxValidity: time.Hour * 2,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
@ -267,7 +280,7 @@ func TestRequestValid(t *testing.T) {
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.profile.requestValid(fc, tc.request)
err := tc.issuer.requestValid(fc, tc.profile, tc.request)
if err != nil {
if tc.expectedError == "" {
t.Errorf("failed with unexpected error: %s", err)
@ -285,19 +298,22 @@ func TestRequestValid(t *testing.T) {
func TestGenerateTemplate(t *testing.T) {
tests := []struct {
name string
profile *Profile
issuer *Issuer
expectedTemplate *x509.Certificate
}{
{
name: "crl url",
profile: &Profile{
issuer: &Issuer{
crlURL: "crl-url",
sigAlg: x509.SHA256WithRSA,
},
expectedTemplate: &x509.Certificate{
BasicConstraintsValid: true,
SignatureAlgorithm: x509.SHA256WithRSA,
ExtKeyUsage: defaultEKU,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
},
IssuingCertificateURL: []string{""},
OCSPServer: []string{""},
CRLDistributionPoints: []string{"crl-url"},
@ -308,7 +324,7 @@ func TestGenerateTemplate(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
template := tc.profile.generateTemplate()
template := tc.issuer.generateTemplate()
test.AssertDeepEquals(t, *template, *tc.expectedTemplate)
})
}
@ -338,25 +354,14 @@ func TestIssue(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
linter, err := linter.New(
issuerCert.Certificate,
issuerSigner,
[]string{
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
"w_subject_common_name_included",
},
)
test.AssertNotError(t, err, "failed to create linter")
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := tc.generateFunc()
test.AssertNotError(t, err, "failed to generate test key")
lintCertBytes, issuanceToken, err := signer.Prepare(&IssuanceRequest{
lintCertBytes, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
CommonName: "example.com",
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
@ -372,7 +377,6 @@ func TestIssue(t *testing.T) {
err = cert.CheckSignatureFrom(issuerCert.Certificate)
test.AssertNotError(t, err, "signature validation failed")
test.AssertDeepEquals(t, cert.DNSNames, []string{"example.com"})
test.AssertEquals(t, cert.Subject.CommonName, "example.com")
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
test.AssertEquals(t, len(cert.Extensions), 9) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, Poison
@ -381,58 +385,17 @@ func TestIssue(t *testing.T) {
}
}
func TestIssueRSA(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
linter, err := linter.New(
issuerCert.Certificate,
issuerSigner,
[]string{
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
},
)
test.AssertNotError(t, err, "failed to create linter")
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := rsa.GenerateKey(rand.Reader, 2048)
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
IncludeCTPoison: true,
})
test.AssertNotError(t, err, "failed to prepare lint certificate")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "failed to parse certificate")
cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
err = cert.CheckSignatureFrom(issuerCert.Certificate)
test.AssertNotError(t, err, "signature validation failed")
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
test.AssertEquals(t, len(cert.Extensions), 9) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, Poison
test.AssertEquals(t, cert.KeyUsage, x509.KeyUsageDigitalSignature|x509.KeyUsageKeyEncipherment)
}
func TestIssueCommonName(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
linter, err := linter.New(
issuerCert.Certificate,
issuerSigner,
[]string{
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
"w_subject_common_name_included",
},
)
test.AssertNotError(t, err, "failed to create linter")
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
cnProfile, err := NewProfile(defaultProfileConfig(), []string{
"w_subject_common_name_included",
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
})
test.AssertNotError(t, err, "NewProfile failed")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
@ -447,7 +410,7 @@ func TestIssueCommonName(t *testing.T) {
IncludeCTPoison: true,
}
_, issuanceToken, err := signer.Prepare(ir)
_, issuanceToken, err := signer.Prepare(cnProfile, ir)
test.AssertNotError(t, err, "Prepare failed")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
@ -455,12 +418,12 @@ func TestIssueCommonName(t *testing.T) {
test.AssertNotError(t, err, "failed to parse certificate")
test.AssertEquals(t, cert.Subject.CommonName, "example.com")
signer.Profile.allowCommonName = false
_, _, err = signer.Prepare(ir)
cnProfile.allowCommonName = false
_, _, err = signer.Prepare(cnProfile, ir)
test.AssertError(t, err, "Prepare should have failed")
ir.CommonName = ""
_, issuanceToken, err = signer.Prepare(ir)
_, issuanceToken, err = signer.Prepare(cnProfile, ir)
test.AssertNotError(t, err, "Prepare failed")
certBytes, err = signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
@ -473,20 +436,12 @@ func TestIssueCommonName(t *testing.T) {
func TestIssueCTPoison(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
linter, err := linter.New(
issuerCert.Certificate,
issuerSigner,
[]string{
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
},
)
test.AssertNotError(t, err, "failed to create linter")
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
_, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
@ -519,19 +474,17 @@ func mustDecodeB64(b string) []byte {
func TestIssueSCTList(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
err := loglist.InitLintList("../test/ct-test-srv/log_list.json")
test.AssertNotError(t, err, "failed to load log list")
linter, err := linter.New(
issuerCert.Certificate,
issuerSigner,
[]string{},
)
test.AssertNotError(t, err, "failed to create linter")
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
enforceSCTsProfile, err := NewProfile(defaultProfileConfig(), []string{})
test.AssertNotError(t, err, "NewProfile failed")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
_, issuanceToken, err := signer.Prepare(enforceSCTsProfile, &IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
@ -560,7 +513,7 @@ func TestIssueSCTList(t *testing.T) {
request2, err := RequestFromPrecert(precert, sctList)
test.AssertNotError(t, err, "generating request from precert")
_, issuanceToken2, err := signer.Prepare(request2)
_, issuanceToken2, err := signer.Prepare(enforceSCTsProfile, request2)
test.AssertNotError(t, err, "preparing final cert issuance")
finalCertBytes, err := signer.Issue(issuanceToken2)
@ -590,20 +543,12 @@ func TestIssueSCTList(t *testing.T) {
func TestIssueMustStaple(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
linter, err := linter.New(
issuerCert.Certificate,
issuerSigner,
[]string{
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
},
)
test.AssertNotError(t, err, "failed to create linter")
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
_, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
@ -629,13 +574,14 @@ func TestIssueMustStaple(t *testing.T) {
func TestIssueBadLint(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
lint, err := linter.New(issuerCert.Certificate, issuerSigner, []string{})
test.AssertNotError(t, err, "failed to create linter")
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), lint, fc)
noSkipLintsProfile, err := NewProfile(defaultProfileConfig(), []string{})
test.AssertNotError(t, err, "NewProfile failed")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
_, _, err = signer.Prepare(&IssuanceRequest{
_, _, err = signer.Prepare(noSkipLintsProfile, &IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
@ -651,9 +597,9 @@ func TestIssueBadLint(t *testing.T) {
func TestIssuanceToken(t *testing.T) {
fc := clock.NewFake()
linter, err := linter.New(issuerCert.Certificate, issuerSigner, []string{})
test.AssertNotError(t, err, "failed to create linter")
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
fc.Set(time.Now())
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
_, err = signer.Issue(&issuanceToken{})
@ -664,7 +610,7 @@ func TestIssuanceToken(t *testing.T) {
pk, err := rsa.GenerateKey(rand.Reader, 2048)
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
_, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
@ -681,7 +627,7 @@ func TestIssuanceToken(t *testing.T) {
test.AssertError(t, err, "expected second issuance with the same issuance token to fail")
test.AssertContains(t, err.Error(), "issuance token already redeemed")
_, issuanceToken, err = signer.Prepare(&IssuanceRequest{
_, issuanceToken, err = signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
@ -692,7 +638,7 @@ func TestIssuanceToken(t *testing.T) {
})
test.AssertNotError(t, err, "expected Prepare to succeed")
signer2, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
signer2, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
_, err = signer2.Issue(issuanceToken)
@ -703,19 +649,15 @@ func TestIssuanceToken(t *testing.T) {
func TestInvalidProfile(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
err := loglist.InitLintList("../test/ct-test-srv/log_list.json")
test.AssertNotError(t, err, "failed to load log list")
linter, err := linter.New(
issuerCert.Certificate,
issuerSigner,
[]string{},
)
test.AssertNotError(t, err, "failed to create linter")
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
_, _, err = signer.Prepare(&IssuanceRequest{
_, _, err = signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
@ -727,7 +669,7 @@ func TestInvalidProfile(t *testing.T) {
})
test.AssertError(t, err, "Invalid IssuanceRequest")
_, _, err = signer.Prepare(&IssuanceRequest{
_, _, err = signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
@ -753,18 +695,20 @@ func TestMismatchedProfiles(t *testing.T) {
fc.Set(time.Now())
err := loglist.InitLintList("../test/ct-test-srv/log_list.json")
test.AssertNotError(t, err, "failed to load log list")
linter, err := linter.New(
issuerCert.Certificate,
issuerSigner,
[]string{"w_subject_common_name_included"},
)
test.AssertNotError(t, err, "failed to create linter")
issuer1, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
issuer1, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
cnProfile, err := NewProfile(defaultProfileConfig(), []string{
"w_subject_common_name_included",
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
})
test.AssertNotError(t, err, "NewProfile failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := issuer1.Prepare(&IssuanceRequest{
_, issuanceToken, err := issuer1.Prepare(cnProfile, &IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
@ -782,9 +726,12 @@ func TestMismatchedProfiles(t *testing.T) {
// Create a new profile that differs slightly (no common name)
profileConfig := defaultProfileConfig()
profileConfig.AllowCommonName = false
p, err := NewProfile(profileConfig, defaultIssuerConfig())
noCNProfile, err := NewProfile(profileConfig, []string{
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
})
test.AssertNotError(t, err, "NewProfile failed")
issuer2, err := NewIssuer(issuerCert, issuerSigner, p, linter, fc)
issuer2, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
sctList := []ct.SignedCertificateTimestamp{
@ -805,7 +752,7 @@ func TestMismatchedProfiles(t *testing.T) {
test.AssertNotError(t, err, "RequestFromPrecert")
request2.CommonName = ""
_, _, err = issuer2.Prepare(request2)
_, _, err = issuer2.Prepare(noCNProfile, request2)
test.AssertError(t, err, "preparing final cert issuance")
test.AssertContains(t, err.Error(), "precert does not correspond to linted final cert")
}

View File

@ -97,7 +97,7 @@ func NewCertificate(ic *x509.Certificate) (*Certificate, error) {
func LoadCertificate(path string) (*Certificate, error) {
cert, err := core.LoadCert(path)
if err != nil {
return nil, err
return nil, fmt.Errorf("loading issuer certificate: %w", err)
}
return NewCertificate(cert)
}
@ -178,51 +178,83 @@ type IssuerLoc struct {
NumSessions int
}
// Issuer is capable of issuing new certificates
// TODO(#5086): make Cert and Signer private when they're no longer needed by ca.internalIssuer
// Issuer is capable of issuing new certificates.
type Issuer struct {
Cert *Certificate
Signer crypto.Signer
Profile *Profile
Linter *linter.Linter
Clk clock.Clock
// TODO(#7159): make Cert, Signer, and Linter private when all signing ops
// are handled through this package (e.g. the CA doesn't need direct access
// while signing CRLs anymore).
Cert *Certificate
Signer crypto.Signer
Linter *linter.Linter
sigAlg x509.SignatureAlgorithm
useForRSALeaves bool
useForECDSALeaves bool
issuerURL string
ocspURL string
crlURL string
// TODO(#7159): Make Clk private by giving ca_test.go a better way to build
// in-memory Issuers.
Clk clock.Clock
}
// NewIssuer constructs an Issuer on the heap, verifying that the profile
// is well-formed.
func NewIssuer(cert *Certificate, signer crypto.Signer, profile *Profile, linter *linter.Linter, clk clock.Clock) (*Issuer, error) {
// 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 sigAlg x509.SignatureAlgorithm
switch k := cert.PublicKey.(type) {
case *rsa.PublicKey:
profile.sigAlg = x509.SHA256WithRSA
sigAlg = x509.SHA256WithRSA
case *ecdsa.PublicKey:
switch k.Curve {
case elliptic.P256():
profile.sigAlg = x509.ECDSAWithSHA256
sigAlg = x509.ECDSAWithSHA256
case elliptic.P384():
profile.sigAlg = x509.ECDSAWithSHA384
sigAlg = x509.ECDSAWithSHA384
default:
return nil, fmt.Errorf("unsupported ECDSA curve: %s", k.Curve.Params().Name)
return nil, fmt.Errorf("unsupported ECDSA curve: %q", k.Curve.Params().Name)
}
default:
return nil, errors.New("unsupported issuer key type")
}
if profile.useForRSALeaves || profile.useForECDSALeaves {
if cert.KeyUsage&x509.KeyUsageCertSign == 0 {
return nil, errors.New("end-entity signing cert does not have keyUsage certSign")
}
if config.IssuerURL == "" {
return nil, errors.New("Issuer URL is required")
}
if config.OCSPURL == "" {
return nil, errors.New("OCSP URL is required")
}
// We require that all of our issuers be capable of both issuing certs and
// providing revocation information.
if cert.KeyUsage&x509.KeyUsageCertSign == 0 {
return nil, errors.New("end-entity signing cert does not have keyUsage certSign")
}
if cert.KeyUsage&x509.KeyUsageCRLSign == 0 {
return nil, errors.New("end-entity signing cert does not have keyUsage crlSign")
}
// TODO(#5086): Only do this check for ocsp-issuing issuers.
if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
return nil, errors.New("end-entity ocsp signing cert does not have keyUsage digitalSignature")
return nil, errors.New("end-entity signing cert does not have keyUsage digitalSignature")
}
lintSigner, err := linter.New(cert.Certificate, signer)
if err != nil {
return nil, fmt.Errorf("creating fake lint signer: %w", err)
}
i := &Issuer{
Cert: cert,
Signer: signer,
Profile: profile,
Linter: linter,
Clk: clk,
Cert: cert,
Signer: signer,
Linter: lintSigner,
sigAlg: sigAlg,
useForRSALeaves: config.UseForRSALeaves,
useForECDSALeaves: config.UseForECDSALeaves,
issuerURL: config.IssuerURL,
ocspURL: config.OCSPURL,
crlURL: config.CRLURL,
Clk: clk,
}
return i, nil
}
@ -232,10 +264,10 @@ func NewIssuer(cert *Certificate, signer crypto.Signer, profile *Profile, linter
// public key algorithm or signature algorithm in this issuer's own cert.
func (i *Issuer) Algs() []x509.PublicKeyAlgorithm {
var algs []x509.PublicKeyAlgorithm
if i.Profile.useForRSALeaves {
if i.useForRSALeaves {
algs = append(algs, x509.RSA)
}
if i.Profile.useForECDSALeaves {
if i.useForECDSALeaves {
algs = append(algs, x509.ECDSA)
}
return algs
@ -251,25 +283,32 @@ func (i *Issuer) NameID() NameID {
return i.Cert.NameID()
}
// LoadIssuer loads a signer (private key) and certificate from the locations specified.
func LoadIssuer(location IssuerLoc) (*Certificate, crypto.Signer, error) {
issuerCert, err := LoadCertificate(location.CertFile)
// LoadIssuer constructs a new Issuer, loading its certificate from disk and its
// private key material from the indicated location. It also verifies that the
// issuer metadata (such as AIA URLs) is well-formed.
func LoadIssuer(config IssuerConfig, clk clock.Clock) (*Issuer, error) {
issuerCert, err := LoadCertificate(config.Location.CertFile)
if err != nil {
return nil, nil, err
return nil, err
}
signer, err := loadSigner(location, issuerCert)
signer, err := loadSigner(config.Location, issuerCert.PublicKey)
if err != nil {
return nil, nil, err
return nil, err
}
if !core.KeyDigestEquals(signer.Public(), issuerCert.PublicKey) {
return nil, nil, fmt.Errorf("Issuer key did not match issuer cert %s", location.CertFile)
return nil, fmt.Errorf("issuer key did not match issuer cert %q", config.Location.CertFile)
}
return issuerCert, signer, err
return newIssuer(config, issuerCert, signer, clk)
}
func loadSigner(location IssuerLoc, cert *Certificate) (crypto.Signer, error) {
func loadSigner(location IssuerLoc, pubkey crypto.PublicKey) (crypto.Signer, error) {
if location.File == "" && location.ConfigFile == "" && location.PKCS11 == nil {
return nil, errors.New("must supply File, ConfigFile, or PKCS11")
}
if location.File != "" {
signer, _, err := privatekey.Load(location.File)
if err != nil {
@ -305,5 +344,5 @@ func loadSigner(location IssuerLoc, cert *Certificate) (crypto.Signer, error) {
}
return pkcs11key.NewPool(numSessions, pkcs11Config.Module,
pkcs11Config.TokenLabel, pkcs11Config.PIN, cert.PublicKey)
pkcs11Config.TokenLabel, pkcs11Config.PIN, pubkey)
}

View File

@ -19,7 +19,6 @@ import (
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/config"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/linter"
"github.com/letsencrypt/boulder/test"
)
@ -57,7 +56,7 @@ func TestMain(m *testing.M) {
Subject: pkix.Name{
CommonName: "big ca",
},
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
KeyUsage: x509.KeyUsageCRLSign | x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
}
issuer, err := x509.CreateCertificate(rand.Reader, template, template, tk.Public(), tk)
cmd.FailOnError(err, "failed to generate test issuer")
@ -67,90 +66,150 @@ func TestMain(m *testing.M) {
os.Exit(m.Run())
}
func TestNewIssuer(t *testing.T) {
_, err := NewIssuer(
func TestLoadCertificate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
path string
wantErr string
}{
{"invalid cert file", "../test/hierarchy/int-e1.crl.pem", "loading issuer certificate"},
{"non-CA cert file", "../test/hierarchy/ee-e1.cert.pem", "not a CA certificate"},
{"happy path", "../test/hierarchy/int-e1.cert.pem", ""},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := LoadCertificate(tc.path)
if err != nil {
if tc.wantErr != "" {
test.AssertContains(t, err.Error(), tc.wantErr)
} else {
t.Errorf("expected no error but got %v", err)
}
} else {
if tc.wantErr != "" {
t.Errorf("expected error %q but got none", tc.wantErr)
}
}
})
}
}
func TestLoadSigner(t *testing.T) {
t.Parallel()
// We're using this for its pubkey. This definitely doesn't match the private
// key loaded in any of the tests below, but that's okay because it still gets
// us through all the logic in loadSigner.
fakeKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
test.AssertNotError(t, err, "generating test key")
tests := []struct {
name string
loc IssuerLoc
wantErr string
}{
{"empty IssuerLoc", IssuerLoc{}, "must supply"},
{"invalid key file", IssuerLoc{File: "../test/hierarchy/int-e1.crl.pem"}, "unable to parse"},
{"ECDSA key file", IssuerLoc{File: "../test/hierarchy/int-e1.key.pem"}, ""},
{"RSA key file", IssuerLoc{File: "../test/hierarchy/int-r3.key.pem"}, ""},
{"invalid config file", IssuerLoc{ConfigFile: "../test/example-weak-keys.json"}, "json: cannot unmarshal"},
// Note that we don't have a test for "valid config file" because it would
// always fail -- in CI, the softhsm hasn't been initialized, so there's no
// key to look up; locally even if the softhsm has been initialized, the
// keys in it don't match the fakeKey we generated above.
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := loadSigner(tc.loc, fakeKey.Public())
if err != nil {
if tc.wantErr != "" {
test.AssertContains(t, err.Error(), tc.wantErr)
} else {
t.Errorf("expected no error but got %v", err)
}
} else {
if tc.wantErr != "" {
t.Errorf("expected error %q but got none", tc.wantErr)
}
}
})
}
}
func TestLoadIssuer(t *testing.T) {
_, err := newIssuer(
defaultIssuerConfig(),
issuerCert,
issuerSigner,
defaultProfile(),
&linter.Linter{},
clock.NewFake(),
)
test.AssertNotError(t, err, "NewIssuer failed")
test.AssertNotError(t, err, "newIssuer failed")
}
func TestNewIssuerUnsupportedKeyType(t *testing.T) {
_, err := NewIssuer(
_, err := newIssuer(
defaultIssuerConfig(),
&Certificate{
Certificate: &x509.Certificate{
PublicKey: &ed25519.PublicKey{},
},
},
&ed25519.PrivateKey{},
defaultProfile(),
&linter.Linter{},
clock.NewFake(),
)
test.AssertError(t, err, "NewIssuer didn't fail")
test.AssertError(t, err, "newIssuer didn't fail")
test.AssertEquals(t, err.Error(), "unsupported issuer key type")
}
func TestNewIssuerNoCertSign(t *testing.T) {
_, err := NewIssuer(
&Certificate{
Certificate: &x509.Certificate{
PublicKey: &ecdsa.PublicKey{
Curve: elliptic.P256(),
},
KeyUsage: 0,
},
},
issuerSigner,
defaultProfile(),
&linter.Linter{},
clock.NewFake(),
)
test.AssertError(t, err, "NewIssuer didn't fail")
test.AssertEquals(t, err.Error(), "end-entity signing cert does not have keyUsage certSign")
}
func TestNewIssuerKeyUsage(t *testing.T) {
t.Parallel()
func TestNewIssuerNoDigitalSignature(t *testing.T) {
_, err := NewIssuer(
&Certificate{
Certificate: &x509.Certificate{
PublicKey: &ecdsa.PublicKey{
Curve: elliptic.P256(),
tests := []struct {
name string
ku x509.KeyUsage
wantErr string
}{
{"missing certSign", x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature, "does not have keyUsage certSign"},
{"missing crlSign", x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, "does not have keyUsage crlSign"},
{"missing digitalSignature", x509.KeyUsageCertSign | x509.KeyUsageCRLSign, "does not have keyUsage digitalSignature"},
{"all three", x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature, ""},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := newIssuer(
defaultIssuerConfig(),
&Certificate{
Certificate: &x509.Certificate{
SerialNumber: big.NewInt(123),
PublicKey: &ecdsa.PublicKey{
Curve: elliptic.P256(),
},
KeyUsage: tc.ku,
},
},
KeyUsage: x509.KeyUsageCertSign,
},
},
issuerSigner,
defaultProfile(),
&linter.Linter{},
clock.NewFake(),
)
test.AssertError(t, err, "NewIssuer didn't fail")
test.AssertEquals(t, err.Error(), "end-entity ocsp signing cert does not have keyUsage digitalSignature")
}
func TestNewIssuerOCSPOnly(t *testing.T) {
p := defaultProfile()
p.useForRSALeaves = false
p.useForECDSALeaves = false
_, err := NewIssuer(
&Certificate{
Certificate: &x509.Certificate{
PublicKey: &ecdsa.PublicKey{
Curve: elliptic.P256(),
},
KeyUsage: x509.KeyUsageDigitalSignature,
},
},
issuerSigner,
p,
&linter.Linter{},
clock.NewFake(),
)
test.AssertNotError(t, err, "NewIssuer failed")
issuerSigner,
clock.NewFake(),
)
if err != nil {
if tc.wantErr != "" {
test.AssertContains(t, err.Error(), tc.wantErr)
} else {
t.Errorf("expected no error but got %v", err)
}
} else {
if tc.wantErr != "" {
t.Errorf("expected error %q but got none", tc.wantErr)
}
}
})
}
}
func TestLoadChain_Valid(t *testing.T) {

View File

@ -32,12 +32,17 @@ var ErrLinting = fmt.Errorf("failed lint(s)")
// a new signer and a new lint registry are expensive operations which
// performance-sensitive clients may want to cache via linter.New().
func Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, realIssuer *x509.Certificate, realSigner crypto.Signer, skipLints []string) ([]byte, error) {
linter, err := New(realIssuer, realSigner, skipLints)
linter, err := New(realIssuer, realSigner)
if err != nil {
return nil, err
}
lintCertBytes, err := linter.Check(tbs, subjectPubKey)
reg, err := NewRegistry(skipLints)
if err != nil {
return nil, err
}
lintCertBytes, err := linter.Check(tbs, subjectPubKey, reg)
if err != nil {
return nil, err
}
@ -47,11 +52,17 @@ func Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, realIssuer *x5
// CheckCRL is like Check, but for CRLs.
func CheckCRL(tbs *x509.RevocationList, realIssuer *x509.Certificate, realSigner crypto.Signer, skipLints []string) error {
linter, err := New(realIssuer, realSigner, skipLints)
linter, err := New(realIssuer, realSigner)
if err != nil {
return err
}
return linter.CheckCRL(tbs)
reg, err := NewRegistry(skipLints)
if err != nil {
return err
}
return linter.CheckCRL(tbs, reg)
}
// Linter is capable of linting a to-be-signed (TBS) certificate. It does so by
@ -61,7 +72,6 @@ func CheckCRL(tbs *x509.RevocationList, realIssuer *x509.Certificate, realSigner
type Linter struct {
issuer *x509.Certificate
signer crypto.Signer
registry lint.Registry
realPubKey crypto.PublicKey
}
@ -70,7 +80,7 @@ type Linter struct {
// be used to sign the lint certificate. It uses the provided list of lint names
// to skip to filter the zlint global registry to only those lints which should
// be run.
func New(realIssuer *x509.Certificate, realSigner crypto.Signer, skipLints []string) (*Linter, error) {
func New(realIssuer *x509.Certificate, realSigner crypto.Signer) (*Linter, error) {
lintSigner, err := makeSigner(realSigner)
if err != nil {
return nil, err
@ -79,21 +89,17 @@ func New(realIssuer *x509.Certificate, realSigner crypto.Signer, skipLints []str
if err != nil {
return nil, err
}
reg, err := makeRegistry(skipLints)
if err != nil {
return nil, err
}
return &Linter{lintIssuer, lintSigner, reg, realSigner.Public()}, nil
return &Linter{lintIssuer, lintSigner, realSigner.Public()}, nil
}
// Check signs the given TBS certificate using the Linter's fake issuer cert and
// private key, then runs the resulting certificate through all non-filtered
// lints. If the subjectPubKey is identical to the public key of the real signer
// private key, then runs the resulting certificate through all lints in reg.
// If the subjectPubKey is identical to the public key of the real signer
// used to create this linter, then the throwaway cert will have its pubkey
// replaced with the linter's pubkey so that it appears self-signed. It returns
// an error if any lint fails. On success it also returns the DER bytes of the
// linting certificate.
func (l Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey) ([]byte, error) {
func (l Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, reg lint.Registry) ([]byte, error) {
lintPubKey := subjectPubKey
selfSigned, err := core.PublicKeysEqual(subjectPubKey, l.realPubKey)
if err != nil {
@ -108,7 +114,7 @@ func (l Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey) ([]
return nil, err
}
lintRes := zlint.LintCertificateEx(cert, l.registry)
lintRes := zlint.LintCertificateEx(cert, reg)
err = ProcessResultSet(lintRes)
if err != nil {
return nil, err
@ -118,14 +124,14 @@ func (l Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey) ([]
}
// CheckCRL signs the given RevocationList template using the Linter's fake
// issuer cert and private key, then runs the resulting CRL through our suite
// of CRL checks. It returns an error if any check fails.
func (l Linter) CheckCRL(tbs *x509.RevocationList) error {
// issuer cert and private key, then runs the resulting CRL through all CRL
// lints in the registry. It returns an error if any check fails.
func (l Linter) CheckCRL(tbs *x509.RevocationList, reg lint.Registry) error {
crl, err := makeLintCRL(tbs, l.issuer, l.signer)
if err != nil {
return err
}
lintRes := zlint.LintRevocationListEx(crl, l.registry)
lintRes := zlint.LintRevocationListEx(crl, reg)
return ProcessResultSet(lintRes)
}
@ -205,7 +211,9 @@ func makeIssuer(realIssuer *x509.Certificate, lintSigner crypto.Signer) (*x509.C
return lintIssuer, nil
}
func makeRegistry(skipLints []string) (lint.Registry, error) {
// NewRegistry returns a zlint Registry with irrelevant (ETSI, EV) lints
// excluded. This registry also includes all custom lints defined in Boulder.
func NewRegistry(skipLints []string) (lint.Registry, error) {
reg, err := lint.GlobalRegistry().Filter(lint.FilterOptions{
ExcludeNames: skipLints,
ExcludeSources: []lint.LintSource{