Refactor CA configs for more modularity (#5087)

The CA is the only service which still defines its json config format
in the package itself, rather than in its corresponding boulder-ca cmd
package. This has allowed the CA's constructor interface to hide
arbitrary complexity inside its first argument, the whole config blob.

This change moves the CA's config to boulder-ca/main.go, to match
the other Boulder components. In the process, it makes a host of
other improvements:

It refactors the issuance package to have a cleaner configuration
interface. It also separates the config into a high-level profile (which
applies equally to all issuers), and issuer-level profiles (which apply
only to a single issuer). This does involve some code duplication,
but that will be removed when CFSSL goes away.

It adds helper functions to the issuance package to make it easier
to construct a new issuer, and takes advantage of these in the
boulder-ca package. As a result, the CA now receives fully-formed
Issuers at construction time, rather than constructing them from
nearly-complete configs during its own initialization.

It adds a Linter struct to the lint package, so that an issuer can
simply carry around a Linter, rather than a separate lint signing
key and registry of lints to run.

It makes CFSSL-specific code more clearly marked as such,
making future removal easier and cleaner.

Fixes #5070
Fixes #5076
This commit is contained in:
Aaron Gable 2020-09-14 18:38:12 -07:00 committed by GitHub
parent f0b54a8968
commit 2d10cce1a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 850 additions and 676 deletions

138
ca/ca.go
View File

@ -31,7 +31,6 @@ import (
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/crypto/ocsp"
ca_config "github.com/letsencrypt/boulder/ca/config"
capb "github.com/letsencrypt/boulder/ca/proto"
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
@ -127,25 +126,25 @@ type issuerMaps struct {
// CertificateAuthorityImpl represents a CA that signs certificates, CRLs, and
// OCSP responses.
type CertificateAuthorityImpl struct {
rsaProfile string
ecdsaProfile string
issuers issuerMaps
sa certificateStorage
pa core.PolicyAuthority
keyPolicy goodkey.KeyPolicy
clk clock.Clock
log blog.Logger
issuers issuerMaps
cfsslRSAProfile string
cfsslECDSAProfile string
prefix int // Prepended to the serial number
validityPeriod time.Duration
backdate time.Duration
maxNames int
ocspLifetime time.Duration
keyPolicy goodkey.KeyPolicy
orphanQueue *goque.Queue
clk clock.Clock
log blog.Logger
signatureCount *prometheus.CounterVec
csrExtensionCount *prometheus.CounterVec
orphanCount *prometheus.CounterVec
adoptedOrphanCount *prometheus.CounterVec
signErrorCounter *prometheus.CounterVec
orphanQueue *goque.Queue
ocspLifetime time.Duration
}
// Issuer represents a single issuer certificate, along with its key.
@ -163,6 +162,7 @@ type localSigner interface {
// internalIssuer represents the fully initialized internal state for a single
// issuer, including the cfssl signer and OCSP signer objects.
// TODO(#5086): Remove the ocsp-specific pieces of this as we factor OCSP out.
type internalIssuer struct {
cert *x509.Certificate
ocspSigner crypto.Signer
@ -172,37 +172,27 @@ type internalIssuer struct {
boulderIssuer *issuance.Issuer
}
func makeInternalIssuers(configs []issuance.IssuerConfig, lifespanOCSP time.Duration) (issuerMaps, error) {
func makeInternalIssuers(issuers []*issuance.Issuer, lifespanOCSP time.Duration) (issuerMaps, error) {
issuersByAlg := make(map[x509.PublicKeyAlgorithm]*internalIssuer, 2)
issuersByName := make(map[string]*internalIssuer, len(configs))
issuersByID := make(map[int64]*internalIssuer, len(configs))
for _, config := range configs {
issuer, err := issuance.New(config)
if err != nil {
return issuerMaps{}, err
}
issuersByName := make(map[string]*internalIssuer, len(issuers))
issuersByID := make(map[int64]*internalIssuer, len(issuers))
for _, issuer := range issuers {
ii := &internalIssuer{
cert: config.Cert,
ocspSigner: config.Signer,
cert: issuer.Cert,
ocspSigner: issuer.Signer,
boulderIssuer: issuer,
}
if config.Profile.UseForRSALeaves {
if issuersByAlg[x509.RSA] != nil {
return issuerMaps{}, errors.New("Multiple issuer certs for RSA are not allowed")
for _, alg := range issuer.Algs() {
if issuersByAlg[alg] != nil {
return issuerMaps{}, fmt.Errorf("Multiple issuer certs for %s are not allowed", alg)
}
issuersByAlg[x509.RSA] = ii
issuersByAlg[alg] = ii
}
if config.Profile.UseForECDSALeaves {
if issuersByAlg[x509.ECDSA] != nil {
return issuerMaps{}, errors.New("Multiple issuer certs for ECDSA are not allowed")
}
issuersByAlg[x509.ECDSA] = ii
}
if issuersByName[config.Cert.Subject.CommonName] != nil {
if issuersByName[issuer.Name()] != nil {
return issuerMaps{}, errors.New("Multiple issuer certs with the same CommonName are not supported")
}
issuersByName[config.Cert.Subject.CommonName] = ii
issuersByID[idForCert(config.Cert)] = ii
issuersByName[issuer.Name()] = ii
issuersByID[issuer.ID()] = ii
}
return issuerMaps{issuersByAlg, issuersByName, issuersByID}, nil
}
@ -262,38 +252,48 @@ func idForCert(cert *x509.Certificate) int64 {
// from a single issuer (the first first in the issuers slice), and can sign OCSP
// for any of the issuer certificates provided.
func NewCertificateAuthorityImpl(
config ca_config.CAConfig,
sa certificateStorage,
pa core.PolicyAuthority,
clk clock.Clock,
stats prometheus.Registerer,
cfsslProfiles cfsslConfig.Config,
cfsslRSAProfile string,
cfsslECDSAProfile string,
cfsslIssuers []Issuer,
boulderIssuers []issuance.IssuerConfig,
boulderIssuers []*issuance.Issuer,
certExpiry time.Duration,
certBackdate time.Duration,
serialPrefix int,
maxNames int,
ocspLifetime time.Duration,
keyPolicy goodkey.KeyPolicy,
logger blog.Logger,
orphanQueue *goque.Queue,
logger blog.Logger,
stats prometheus.Registerer,
clk clock.Clock,
) (*CertificateAuthorityImpl, error) {
var ca *CertificateAuthorityImpl
var err error
if config.SerialPrefix <= 0 || config.SerialPrefix >= 256 {
// TODO(briansmith): Make the backdate setting mandatory after the
// production ca.json has been updated to include it. Until then, manually
// default to 1h, which is the backdating duration we currently use.
if certBackdate == 0 {
certBackdate = time.Hour
}
if serialPrefix <= 0 || serialPrefix >= 256 {
err = errors.New("Must have a positive non-zero serial prefix less than 256 for CA.")
return nil, err
}
var issuers issuerMaps
// rsaProfile and ecdsaProfile are unused when using the boulder signer
// instead of the CFSSL signer
var rsaProfile, ecdsaProfile string
if features.Enabled(features.NonCFSSLSigner) {
issuers, err = makeInternalIssuers(boulderIssuers, config.LifespanOCSP.Duration)
issuers, err = makeInternalIssuers(boulderIssuers, ocspLifetime)
if err != nil {
return nil, err
}
} else {
// CFSSL requires processing JSON configs through its own LoadConfig, so we
// serialize and then deserialize.
cfsslJSON, err := json.Marshal(config.CFSSL)
cfsslJSON, err := json.Marshal(cfsslProfiles)
if err != nil {
return nil, err
}
@ -302,7 +302,7 @@ func NewCertificateAuthorityImpl(
return nil, err
}
if config.LifespanOCSP.Duration == 0 {
if ocspLifetime == 0 {
return nil, errors.New("Config must specify an OCSP lifespan period.")
}
@ -312,17 +312,12 @@ func NewCertificateAuthorityImpl(
}
}
issuers, err = makeCFSSLInternalIssuers(
cfsslIssuers,
cfsslConfigObj.Signing,
config.LifespanOCSP.Duration)
issuers, err = makeCFSSLInternalIssuers(cfsslIssuers, cfsslConfigObj.Signing, ocspLifetime)
if err != nil {
return nil, err
}
rsaProfile, ecdsaProfile = config.RSAProfile, config.ECDSAProfile
if rsaProfile == "" || ecdsaProfile == "" {
if cfsslRSAProfile == "" || cfsslECDSAProfile == "" {
return nil, errors.New("must specify rsaProfile and ecdsaProfile")
}
}
@ -369,39 +364,24 @@ func NewCertificateAuthorityImpl(
sa: sa,
pa: pa,
issuers: issuers,
rsaProfile: rsaProfile,
ecdsaProfile: ecdsaProfile,
prefix: config.SerialPrefix,
clk: clk,
log: logger,
cfsslRSAProfile: cfsslRSAProfile,
cfsslECDSAProfile: cfsslECDSAProfile,
validityPeriod: certExpiry,
backdate: certBackdate,
prefix: serialPrefix,
maxNames: maxNames,
ocspLifetime: ocspLifetime,
keyPolicy: keyPolicy,
orphanQueue: orphanQueue,
log: logger,
signatureCount: signatureCount,
csrExtensionCount: csrExtensionCount,
orphanCount: orphanCount,
adoptedOrphanCount: adoptedOrphanCount,
orphanQueue: orphanQueue,
ocspLifetime: config.LifespanOCSP.Duration,
signErrorCounter: signErrorCounter,
clk: clk,
}
if config.Expiry == "" {
return nil, errors.New("Config must specify an expiry period.")
}
ca.validityPeriod, err = time.ParseDuration(config.Expiry)
if err != nil {
return nil, err
}
// TODO(briansmith): Make the backdate setting mandatory after the
// production ca.json has been updated to include it. Until then, manually
// default to 1h, which is the backdating duration we currently use.
ca.backdate = config.Backdate.Duration
if ca.backdate == 0 {
ca.backdate = time.Hour
}
ca.maxNames = config.MaxNames
return ca, nil
}
@ -821,9 +801,9 @@ func (ca *CertificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
var profile string
switch csr.PublicKey.(type) {
case *rsa.PublicKey:
profile = ca.rsaProfile
profile = ca.cfsslRSAProfile
case *ecdsa.PublicKey:
profile = ca.ecdsaProfile
profile = ca.cfsslECDSAProfile
default:
err = berrors.InternalServerError("unsupported key type %T", csr.PublicKey)
ca.log.AuditErr(err.Error())

View File

@ -27,10 +27,9 @@ import (
cttls "github.com/google/certificate-transparency-go/tls"
"github.com/jmhodges/clock"
"github.com/prometheus/client_golang/prometheus"
"github.com/zmap/zlint/v2/lint"
zlint "github.com/zmap/zlint/v2/lint"
"golang.org/x/crypto/ocsp"
ca_config "github.com/letsencrypt/boulder/ca/config"
capb "github.com/letsencrypt/boulder/ca/proto"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
@ -39,6 +38,7 @@ import (
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey"
"github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/lint"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/policy"
@ -132,14 +132,21 @@ func mustRead(path string) []byte {
}
type testCtx struct {
caConfig ca_config.CAConfig
pa core.PolicyAuthority
issuers []Issuer
issuerConfigs []issuance.IssuerConfig
keyPolicy goodkey.KeyPolicy
fc clock.FakeClock
stats prometheus.Registerer
logger *blog.Mock
pa core.PolicyAuthority
certExpiry time.Duration
certBackdate time.Duration
serialPrefix int
maxNames int
ocspLifetime time.Duration
cfsslProfiles cfsslConfig.Config
cfsslRSAProfile string
cfsslECDSAProfile string
cfsslIssuers []Issuer
boulderIssuers []*issuance.Issuer
keyPolicy goodkey.KeyPolicy
fc clock.FakeClock
stats prometheus.Registerer
logger *blog.Mock
}
type mockSA struct {
@ -194,94 +201,87 @@ func setup(t *testing.T) *testCtx {
cfsslConfig.OID(OIDExtensionCTPoison),
}
// Create a CA
caConfig := ca_config.CAConfig{
RSAProfile: rsaProfileName,
ECDSAProfile: ecdsaProfileName,
SerialPrefix: 17,
Expiry: "8760h",
// TODO(briansmith): When the defaulting of Backdate is removed, this
// will need to be uncommented. Leave it commented for now to test the
// defaulting logic.
// Backdate: cmd.ConfigDuration{Duration: time.Hour},
LifespanOCSP: cmd.ConfigDuration{Duration: 45 * time.Minute},
MaxNames: 2,
CFSSL: cfsslConfig.Config{
Signing: &cfsslConfig.Signing{
Profiles: map[string]*cfsslConfig.SigningProfile{
rsaProfileName: {
Usage: []string{"digital signature", "key encipherment", "server auth"},
IssuerURL: []string{"http://not-example.com/issuer-url"},
OCSP: "http://not-example.com/ocsp",
CRL: "http://not-example.com/crl",
cfsslProfiles := cfsslConfig.Config{
Signing: &cfsslConfig.Signing{
Profiles: map[string]*cfsslConfig.SigningProfile{
rsaProfileName: {
Usage: []string{"digital signature", "key encipherment", "server auth"},
IssuerURL: []string{"http://not-example.com/issuer-url"},
OCSP: "http://not-example.com/ocsp",
CRL: "http://not-example.com/crl",
Policies: []cfsslConfig.CertificatePolicy{
{
ID: cfsslConfig.OID(asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}),
},
Policies: []cfsslConfig.CertificatePolicy{
{
ID: cfsslConfig.OID(asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}),
},
ExpiryString: "8760h",
Backdate: time.Hour,
CSRWhitelist: &cfsslConfig.CSRWhitelist{
PublicKeyAlgorithm: true,
PublicKey: true,
SignatureAlgorithm: true,
},
ClientProvidesSerialNumbers: true,
AllowedExtensions: allowedExtensions,
},
ecdsaProfileName: {
Usage: []string{"digital signature", "server auth"},
IssuerURL: []string{"http://not-example.com/issuer-url"},
OCSP: "http://not-example.com/ocsp",
CRL: "http://not-example.com/crl",
Policies: []cfsslConfig.CertificatePolicy{
{
ID: cfsslConfig.OID(asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}),
},
},
ExpiryString: "8760h",
Backdate: time.Hour,
CSRWhitelist: &cfsslConfig.CSRWhitelist{
PublicKeyAlgorithm: true,
PublicKey: true,
SignatureAlgorithm: true,
},
ClientProvidesSerialNumbers: true,
AllowedExtensions: allowedExtensions,
},
},
Default: &cfsslConfig.SigningProfile{
ExpiryString: "8760h",
Backdate: time.Hour,
CSRWhitelist: &cfsslConfig.CSRWhitelist{
PublicKeyAlgorithm: true,
PublicKey: true,
SignatureAlgorithm: true,
},
ClientProvidesSerialNumbers: true,
AllowedExtensions: allowedExtensions,
},
ecdsaProfileName: {
Usage: []string{"digital signature", "server auth"},
IssuerURL: []string{"http://not-example.com/issuer-url"},
OCSP: "http://not-example.com/ocsp",
CRL: "http://not-example.com/crl",
Policies: []cfsslConfig.CertificatePolicy{
{
ID: cfsslConfig.OID(asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}),
},
},
ExpiryString: "8760h",
Backdate: time.Hour,
CSRWhitelist: &cfsslConfig.CSRWhitelist{
PublicKeyAlgorithm: true,
PublicKey: true,
SignatureAlgorithm: true,
},
ClientProvidesSerialNumbers: true,
AllowedExtensions: allowedExtensions,
},
},
Default: &cfsslConfig.SigningProfile{
ExpiryString: "8760h",
},
},
}
cfsslIssuers := []Issuer{{caKey, caCert}}
issuers := []Issuer{{caKey, caCert}}
issuerConfigs := []issuance.IssuerConfig{
{
Cert: caCert,
Signer: caKey,
Clk: fc,
Profile: issuance.ProfileConfig{
UseForECDSALeaves: true,
UseForRSALeaves: true,
AllowMustStaple: true,
AllowCTPoison: true,
AllowSCTList: true,
AllowCommonName: true,
IssuerURL: "http://not-example.com/issuer-url",
OCSPURL: "http://not-example.com/ocsp",
CRLURL: "http://not-example.com/crl",
Policies: []issuance.PolicyInformation{
{OID: "2.23.140.1.2.1"},
},
MaxValidityPeriod: cmd.ConfigDuration{Duration: time.Hour * 8760},
MaxValidityBackdate: cmd.ConfigDuration{Duration: time.Hour},
boulderProfile, _ := issuance.NewProfile(
issuance.ProfileConfig{
AllowMustStaple: true,
AllowCTPoison: true,
AllowSCTList: true,
AllowCommonName: true,
Policies: []issuance.PolicyInformation{
{OID: "2.23.140.1.2.1"},
},
MaxValidityPeriod: cmd.ConfigDuration{Duration: time.Hour * 8760},
MaxValidityBackdate: cmd.ConfigDuration{Duration: time.Hour},
},
issuance.IssuerConfig{
UseForECDSALeaves: true,
UseForRSALeaves: true,
IssuerURL: "http://not-example.com/issuer-url",
OCSPURL: "http://not-example.com/ocsp",
CRLURL: "http://not-example.com/crl",
},
)
boulderLinter, _ := lint.NewLinter(caKey, nil)
boulderIssuers := []*issuance.Issuer{
{
Cert: caCert,
Signer: caKey,
Profile: boulderProfile,
Linter: boulderLinter,
Clk: fc,
},
}
@ -291,35 +291,46 @@ func setup(t *testing.T) *testCtx {
AllowECDSANISTP384: true,
}
logger := blog.NewMock()
return &testCtx{
caConfig,
pa,
issuers,
issuerConfigs,
keyPolicy,
fc,
metrics.NoopRegisterer,
logger,
pa: pa,
certExpiry: 8760 * time.Hour,
certBackdate: time.Hour,
serialPrefix: 17,
maxNames: 2,
ocspLifetime: time.Hour,
cfsslProfiles: cfsslProfiles,
cfsslRSAProfile: rsaProfileName,
cfsslECDSAProfile: ecdsaProfileName,
cfsslIssuers: cfsslIssuers,
boulderIssuers: boulderIssuers,
keyPolicy: keyPolicy,
fc: fc,
stats: metrics.NoopRegisterer,
logger: blog.NewMock(),
}
}
func TestFailNoSerial(t *testing.T) {
func TestFailNoSerialPrefix(t *testing.T) {
testCtx := setup(t)
testCtx.caConfig.SerialPrefix = 0
_, err := NewCertificateAuthorityImpl(
testCtx.caConfig,
nil,
nil,
testCtx.fc,
testCtx.stats,
testCtx.issuers,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
testCtx.cfsslIssuers,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
0,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
nil,
testCtx.logger,
nil)
testCtx.stats,
testCtx.fc)
test.AssertError(t, err, "CA should have failed with no SerialPrefix")
}
@ -404,25 +415,32 @@ func TestIssuePrecertificate(t *testing.T) {
func issueCertificateSubTestSetup(t *testing.T, boulderIssuer bool) (*CertificateAuthorityImpl, *mockSA) {
testCtx := setup(t)
sa := &mockSA{}
var issuers []Issuer
var issuerConfigs []issuance.IssuerConfig
var cfsslIssuers []Issuer
var boulderIssuers []*issuance.Issuer
if boulderIssuer {
issuerConfigs = testCtx.issuerConfigs
boulderIssuers = testCtx.boulderIssuers
_ = features.Set(map[string]bool{"NonCFSSLSigner": true})
} else {
issuers = testCtx.issuers
cfsslIssuers = testCtx.cfsslIssuers
}
ca, err := NewCertificateAuthorityImpl(
testCtx.caConfig,
sa,
testCtx.pa,
testCtx.fc,
testCtx.stats,
issuers,
issuerConfigs,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
cfsslIssuers,
boulderIssuers,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
nil,
testCtx.logger,
nil)
testCtx.stats,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
return ca, sa
@ -468,16 +486,23 @@ func TestMultipleIssuers(t *testing.T) {
}
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
testCtx.caConfig,
sa,
testCtx.pa,
testCtx.fc,
testCtx.stats,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
newIssuers,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
nil,
testCtx.logger,
nil)
testCtx.stats,
testCtx.fc)
test.AssertNotError(t, err, "Failed to remake CA")
issuedCert, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID})
@ -494,16 +519,23 @@ func TestOCSP(t *testing.T) {
testCtx := setup(t)
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
testCtx.caConfig,
sa,
testCtx.pa,
testCtx.fc,
testCtx.stats,
testCtx.issuers,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
testCtx.cfsslIssuers,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
nil,
testCtx.logger,
nil)
testCtx.stats,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID}
@ -546,16 +578,23 @@ func TestOCSP(t *testing.T) {
},
}
ca, err = NewCertificateAuthorityImpl(
testCtx.caConfig,
sa,
testCtx.pa,
testCtx.fc,
testCtx.stats,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
newIssuers,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
nil,
testCtx.logger,
nil)
testCtx.stats,
testCtx.fc)
test.AssertNotError(t, err, "Failed to remake CA")
// Now issue a new precert, signed by newIssuerCert
@ -644,16 +683,23 @@ func TestInvalidCSRs(t *testing.T) {
testCtx := setup(t)
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
testCtx.caConfig,
sa,
testCtx.pa,
testCtx.fc,
testCtx.stats,
testCtx.issuers,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
testCtx.cfsslIssuers,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
nil,
testCtx.logger,
nil)
testCtx.stats,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
t.Run(testCase.name, func(t *testing.T) {
@ -676,16 +722,23 @@ func TestRejectValidityTooLong(t *testing.T) {
testCtx := setup(t)
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
testCtx.caConfig,
sa,
testCtx.pa,
testCtx.fc,
testCtx.stats,
testCtx.issuers,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
testCtx.cfsslIssuers,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
nil,
testCtx.logger,
nil)
testCtx.stats,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
// This time is a few minutes before the notAfter in testdata/ca_cert.pem
@ -704,44 +757,47 @@ func TestSingleAIAEnforcement(t *testing.T) {
test.AssertNotError(t, err, "Couldn't create PA")
_, err = NewCertificateAuthorityImpl(
ca_config.CAConfig{
SerialPrefix: 1,
LifespanOCSP: cmd.ConfigDuration{Duration: time.Second},
CFSSL: cfsslConfig.Config{
Signing: &cfsslConfig.Signing{
Profiles: map[string]*cfsslConfig.SigningProfile{
rsaProfileName: {
IssuerURL: []string{"http://not-example.com/issuer-url", "bad"},
Usage: []string{"digital signature", "key encipherment", "server auth"},
OCSP: "http://not-example.com/ocsp",
CRL: "http://not-example.com/crl",
Policies: []cfsslConfig.CertificatePolicy{
{
ID: cfsslConfig.OID(asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}),
},
&mockSA{},
pa,
cfsslConfig.Config{
Signing: &cfsslConfig.Signing{
Profiles: map[string]*cfsslConfig.SigningProfile{
rsaProfileName: {
IssuerURL: []string{"http://not-example.com/issuer-url", "bad"},
Usage: []string{"digital signature", "key encipherment", "server auth"},
OCSP: "http://not-example.com/ocsp",
CRL: "http://not-example.com/crl",
Policies: []cfsslConfig.CertificatePolicy{
{
ID: cfsslConfig.OID(asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}),
},
ExpiryString: "8760h",
Backdate: time.Hour,
CSRWhitelist: &cfsslConfig.CSRWhitelist{
PublicKeyAlgorithm: true,
PublicKey: true,
SignatureAlgorithm: true,
},
ClientProvidesSerialNumbers: true,
},
ExpiryString: "8760h",
Backdate: time.Hour,
CSRWhitelist: &cfsslConfig.CSRWhitelist{
PublicKeyAlgorithm: true,
PublicKey: true,
SignatureAlgorithm: true,
},
ClientProvidesSerialNumbers: true,
},
},
},
},
&mockSA{},
pa,
clock.New(),
metrics.NoopRegisterer,
rsaProfileName,
ecdsaProfileName,
nil,
nil,
8760*time.Hour,
time.Hour,
1,
1,
time.Second,
goodkey.KeyPolicy{},
&blog.Mock{},
nil,
&blog.Mock{},
metrics.NoopRegisterer,
clock.New(),
)
test.AssertError(t, err, "NewCertificateAuthorityImpl allowed a profile with multiple issuer_urls")
test.AssertEquals(t, err.Error(), "only one issuer_url supported")
@ -841,16 +897,23 @@ func TestIssueCertificateForPrecertificate(t *testing.T) {
for _, nonCFSSL := range []bool{true, false} {
_ = features.Set(map[string]bool{"NonCFSSLSigner": nonCFSSL})
ca, err := NewCertificateAuthorityImpl(
testCtx.caConfig,
sa,
testCtx.pa,
testCtx.fc,
testCtx.stats,
testCtx.issuers,
testCtx.issuerConfigs,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
testCtx.cfsslIssuers,
testCtx.boulderIssuers,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
nil,
testCtx.logger,
nil)
testCtx.stats,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, OrderID: 0}
@ -924,16 +987,23 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
testCtx := setup(t)
sa := &dupeSA{}
ca, err := NewCertificateAuthorityImpl(
testCtx.caConfig,
sa,
testCtx.pa,
testCtx.fc,
testCtx.stats,
testCtx.issuers,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
testCtx.cfsslIssuers,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
nil,
testCtx.logger,
nil)
testCtx.stats,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
sctBytes, err := makeSCTs()
@ -961,16 +1031,23 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
// for the duplicate.
errorsa := &getCertErrorSA{}
errorca, err := NewCertificateAuthorityImpl(
testCtx.caConfig,
errorsa,
testCtx.pa,
testCtx.fc,
testCtx.stats,
testCtx.issuers,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
testCtx.cfsslIssuers,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
nil,
testCtx.logger,
nil)
testCtx.stats,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
_, err = errorca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
@ -1035,16 +1112,23 @@ func TestPrecertOrphanQueue(t *testing.T) {
fakeNow := time.Date(2019, 9, 20, 0, 0, 0, 0, time.UTC)
testCtx.fc.Set(fakeNow)
ca, err := NewCertificateAuthorityImpl(
testCtx.caConfig,
qsa,
testCtx.pa,
testCtx.fc,
testCtx.stats,
testCtx.issuers,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
testCtx.cfsslIssuers,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
orphanQueue,
testCtx.logger,
orphanQueue)
testCtx.stats,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
err = ca.integrateOrphan()
@ -1098,16 +1182,23 @@ func TestOrphanQueue(t *testing.T) {
}
testCtx.fc.Set(fakeNow)
ca, err := NewCertificateAuthorityImpl(
testCtx.caConfig,
qsa,
testCtx.pa,
testCtx.fc,
testCtx.stats,
testCtx.issuers,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
testCtx.cfsslIssuers,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
orphanQueue,
testCtx.logger,
orphanQueue)
testCtx.stats,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
err = ca.integrateOrphan()
@ -1211,16 +1302,23 @@ func TestIssuePrecertificateLinting(t *testing.T) {
testCtx := setup(t)
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
testCtx.caConfig,
sa,
testCtx.pa,
testCtx.fc,
testCtx.stats,
testCtx.issuers,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
testCtx.cfsslIssuers,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
nil,
testCtx.logger,
nil)
testCtx.stats,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
// Reconfigure the CA's cfsslSigner to be a linttrapSigner that always returns
@ -1228,13 +1326,13 @@ func TestIssuePrecertificateLinting(t *testing.T) {
rsaIssuer := ca.issuers.byAlg[x509.RSA]
rsaIssuer.cfsslSigner = &linttrapSigner{
lintErr: &local.LintError{
ErrorResults: map[string]lint.LintResult{
ErrorResults: map[string]zlint.LintResult{
"foobar": {
Status: lint.Error,
Status: zlint.Error,
Details: "foobar is error",
},
"foobar2": {
Status: lint.Warn,
Status: zlint.Warn,
Details: "foobar2 is warning",
},
},
@ -1268,16 +1366,23 @@ func TestGenerateOCSPWithIssuerID(t *testing.T) {
sa := &mockSA{}
_ = features.Set(map[string]bool{"StoreIssuerInfo": true})
ca, err := NewCertificateAuthorityImpl(
testCtx.caConfig,
sa,
testCtx.pa,
testCtx.fc,
testCtx.stats,
testCtx.issuers,
testCtx.cfsslProfiles,
testCtx.cfsslRSAProfile,
testCtx.cfsslECDSAProfile,
testCtx.cfsslIssuers,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.ocspLifetime,
testCtx.keyPolicy,
nil,
testCtx.logger,
nil)
testCtx.stats,
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
// GenerateOCSP with feature enabled + req contains bad IssuerID

View File

@ -1,75 +0,0 @@
package ca_config
import (
cfsslConfig "github.com/cloudflare/cfssl/config"
"github.com/letsencrypt/pkcs11key/v4"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/issuance"
)
// CAConfig structs have configuration information for the certificate
// authority, including database parameters as well as controls for
// issued certificates.
type CAConfig struct {
cmd.ServiceConfig
cmd.DBConfig
cmd.HostnamePolicyConfig
GRPCCA *cmd.GRPCServerConfig
GRPCOCSPGenerator *cmd.GRPCServerConfig
RSAProfile string
ECDSAProfile string
SerialPrefix int
// Issuers contains configuration information for each issuer cert and key
// this CA knows about. The first in the list is used as the default.
Issuers []IssuerConfig
// SignerProfile contains the signer issuance profile, if using the boulder
// signer rather than the CFSSL signer.
SignerProfile issuance.ProfileConfig
// LifespanOCSP is how long OCSP responses are valid for; It should be longer
// than the minTimeToExpiry field for the OCSP Updater.
LifespanOCSP cmd.ConfigDuration
// How long issued certificates are valid for, should match expiry field
// in cfssl config.
Expiry string
// How far back certificates should be backdated, should match backdate
// field in cfssl config.
Backdate cmd.ConfigDuration
// The maximum number of subjectAltNames in a single certificate
MaxNames int
CFSSL cfsslConfig.Config
IgnoredLints []string
// WeakKeyFile is the path to a JSON file containing truncated RSA modulus
// hashes of known easily enumerable keys.
WeakKeyFile string
// BlockedKeyFile is the path to a YAML file containing Base64 encoded
// SHA256 hashes of SubjectPublicKeyInfo's that should be considered
// administratively blocked.
BlockedKeyFile string
SAService *cmd.GRPCClientConfig
// Path to directory holding orphan queue files, if not provided an orphan queue
// is not used.
OrphanQueueDir string
Features map[string]bool
}
// IssuerConfig contains info about an issuer: private key and issuer cert.
// It should contain either a File path to a PEM-format private key,
// or a PKCS11Config defining how to load a module for an HSM.
type IssuerConfig struct {
// A file from which a pkcs11key.Config will be read and parsed, if present
ConfigFile string
File string
PKCS11 *pkcs11key.Config
CertFile string
// Number of sessions to open with the HSM. For maximum performance,
// this should be equal to the number of cores in the HSM. Defaults to 1.
NumSessions int
}

View File

@ -11,11 +11,11 @@ import (
"github.com/beeker1121/goque"
cfsslConfig "github.com/cloudflare/cfssl/config"
"github.com/cloudflare/cfssl/helpers"
pkcs11key "github.com/letsencrypt/pkcs11key/v4"
"github.com/letsencrypt/boulder/ca"
ca_config "github.com/letsencrypt/boulder/ca/config"
capb "github.com/letsencrypt/boulder/ca/proto"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
@ -23,56 +23,113 @@ import (
"github.com/letsencrypt/boulder/goodkey"
bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/lint"
"github.com/letsencrypt/boulder/policy"
sapb "github.com/letsencrypt/boulder/sa/proto"
)
type config struct {
CA ca_config.CAConfig
CA struct {
cmd.ServiceConfig
cmd.DBConfig
cmd.HostnamePolicyConfig
GRPCCA *cmd.GRPCServerConfig
GRPCOCSPGenerator *cmd.GRPCServerConfig
SAService *cmd.GRPCClientConfig
// CFSSL contains CFSSL-specific configs as specified by that library.
CFSSL cfsslConfig.Config
// RSAProfile and ECDSAProfile name which of the profiles specified in the
// CFSSL config should be used when issuing RSA and ECDSA certs, respectively.
RSAProfile string
ECDSAProfile string
// Issuers contains configuration information for each issuer cert and key
// this CA knows about. The first in the list is used as the default.
// Only used by CFSSL.
Issuers []IssuerConfig
// Issuance contains all information necessary to load and initialize non-CFSSL issuers.
Issuance struct {
Profile issuance.ProfileConfig
Issuers []issuance.IssuerConfig
IgnoredLints []string
}
// How long issued certificates are valid for, should match expiry field
// in cfssl config.
Expiry cmd.ConfigDuration
// How far back certificates should be backdated, should match backdate
// field in cfssl config.
Backdate cmd.ConfigDuration
// What digits we should prepend to serials after randomly generating them.
SerialPrefix int
// The maximum number of subjectAltNames in a single certificate
MaxNames int
// LifespanOCSP is how long OCSP responses are valid for; It should be longer
// than the minTimeToExpiry field for the OCSP Updater.
LifespanOCSP cmd.ConfigDuration
// WeakKeyFile is the path to a JSON file containing truncated RSA modulus
// hashes of known easily enumerable keys.
WeakKeyFile string
// BlockedKeyFile is the path to a YAML file containing Base64 encoded
// SHA256 hashes of SubjectPublicKeyInfo's that should be considered
// administratively blocked.
BlockedKeyFile string
// Path to directory holding orphan queue files, if not provided an orphan queue
// is not used.
OrphanQueueDir string
Features map[string]bool
}
PA cmd.PAConfig
Syslog cmd.SyslogConfig
}
func loadCFSSLIssuers(configs []ca_config.IssuerConfig) ([]ca.Issuer, error) {
// IssuerConfig contains info about an issuer: private key and issuer cert.
// It should contain either a File path to a PEM-format private key,
// or a PKCS11Config defining how to load a module for an HSM. Used by CFSSL.
type IssuerConfig struct {
// A file from which a pkcs11key.Config will be read and parsed, if present
ConfigFile string
File string
PKCS11 *pkcs11key.Config
CertFile string
// Number of sessions to open with the HSM. For maximum performance,
// this should be equal to the number of cores in the HSM. Defaults to 1.
NumSessions int
}
func loadCFSSLIssuers(configs []IssuerConfig) ([]ca.Issuer, error) {
var issuers []ca.Issuer
for _, issuerConfig := range configs {
priv, cert, err := loadIssuer(issuerConfig)
signer, cert, err := loadCFSSLIssuer(issuerConfig)
cmd.FailOnError(err, "Couldn't load private key")
issuers = append(issuers, ca.Issuer{
Signer: priv,
Signer: signer,
Cert: cert,
})
}
return issuers, nil
}
func loadBoulderIssuers(configs []ca_config.IssuerConfig, profile issuance.ProfileConfig, ignoredLints []string) ([]issuance.IssuerConfig, error) {
boulderIssuerConfigs := make([]issuance.IssuerConfig, 0, len(configs))
for _, issuerConfig := range configs {
signer, issuer, err := loadIssuer(issuerConfig)
if err != nil {
return nil, err
}
boulderIssuerConfigs = append(boulderIssuerConfigs, issuance.IssuerConfig{
Cert: issuer,
Signer: signer,
IgnoredLints: ignoredLints,
Clk: cmd.Clock(),
Profile: profile,
})
}
return boulderIssuerConfigs, nil
}
func loadIssuer(issuerConfig ca_config.IssuerConfig) (crypto.Signer, *x509.Certificate, error) {
func loadCFSSLIssuer(issuerConfig IssuerConfig) (crypto.Signer, *x509.Certificate, error) {
cert, err := core.LoadCert(issuerConfig.CertFile)
if err != nil {
return nil, nil, err
}
signer, err := loadSigner(issuerConfig, cert)
signer, err := loadCFSSLSigner(issuerConfig, cert)
if err != nil {
return nil, nil, err
}
@ -83,7 +140,7 @@ func loadIssuer(issuerConfig ca_config.IssuerConfig) (crypto.Signer, *x509.Certi
return signer, cert, err
}
func loadSigner(issuerConfig ca_config.IssuerConfig, cert *x509.Certificate) (crypto.Signer, error) {
func loadCFSSLSigner(issuerConfig IssuerConfig, cert *x509.Certificate) (crypto.Signer, error) {
if issuerConfig.File != "" {
keyBytes, err := ioutil.ReadFile(issuerConfig.File)
if err != nil {
@ -124,6 +181,34 @@ func loadSigner(issuerConfig ca_config.IssuerConfig, cert *x509.Certificate) (cr
pkcs11Config.TokenLabel, pkcs11Config.PIN, cert.PublicKey)
}
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 := lint.NewLinter(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() {
caAddr := flag.String("ca-addr", "", "CA gRPC listen address override")
ocspAddr := flag.String("ocsp-addr", "", "OCSP gRPC listen address override")
@ -172,9 +257,9 @@ func main() {
cmd.FailOnError(err, "Couldn't load hostname policy file")
var cfsslIssuers []ca.Issuer
var boulderIssuerConfigs []issuance.IssuerConfig
var boulderIssuers []*issuance.Issuer
if features.Enabled(features.NonCFSSLSigner) {
boulderIssuerConfigs, err = loadBoulderIssuers(c.CA.Issuers, c.CA.SignerProfile, c.CA.IgnoredLints)
boulderIssuers, err = loadBoulderIssuers(c.CA.Issuance.Profile, c.CA.Issuance.Issuers, c.CA.Issuance.IgnoredLints)
cmd.FailOnError(err, "Couldn't load issuers")
} else {
cfsslIssuers, err = loadCFSSLIssuers(c.CA.Issuers)
@ -202,16 +287,23 @@ func main() {
}
cai, err := ca.NewCertificateAuthorityImpl(
c.CA,
sa,
pa,
clk,
scope,
c.CA.CFSSL,
c.CA.RSAProfile,
c.CA.ECDSAProfile,
cfsslIssuers,
boulderIssuerConfigs,
boulderIssuers,
c.CA.Expiry.Duration,
c.CA.Backdate.Duration,
c.CA.SerialPrefix,
c.CA.MaxNames,
c.CA.LifespanOCSP.Duration,
kp,
orphanQueue,
logger,
orphanQueue)
scope,
clk)
cmd.FailOnError(err, "Failed to create CA impl")
if orphanQueue != nil {

View File

@ -2,12 +2,10 @@ package main
import (
"testing"
ca_config "github.com/letsencrypt/boulder/ca/config"
)
func TestLoadIssuerSuccess(t *testing.T) {
signer, cert, err := loadIssuer(ca_config.IssuerConfig{
signer, cert, err := loadCFSSLIssuer(IssuerConfig{
File: "../../test/test-ca.key",
CertFile: "../../test/test-ca2.pem",
})
@ -23,7 +21,7 @@ func TestLoadIssuerSuccess(t *testing.T) {
}
func TestLoadIssuerBadKey(t *testing.T) {
_, _, err := loadIssuer(ca_config.IssuerConfig{
_, _, err := loadCFSSLIssuer(IssuerConfig{
File: "/dev/null",
CertFile: "../../test/test-ca2.pem",
})
@ -33,7 +31,7 @@ func TestLoadIssuerBadKey(t *testing.T) {
}
func TestLoadIssuerBadCert(t *testing.T) {
_, _, err := loadIssuer(ca_config.IssuerConfig{
_, _, err := loadCFSSLIssuer(IssuerConfig{
File: "../../test/test-ca.key",
CertFile: "/dev/null",
})

View File

@ -8,47 +8,41 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"math/big"
"strconv"
"strings"
"time"
"github.com/cloudflare/cfssl/helpers"
ct "github.com/google/certificate-transparency-go"
cttls "github.com/google/certificate-transparency-go/tls"
ctx509 "github.com/google/certificate-transparency-go/x509"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/lint"
"github.com/letsencrypt/boulder/policyasn1"
zlint "github.com/zmap/zlint/v2/lint"
"github.com/letsencrypt/pkcs11key/v4"
)
// IssuanceRequest describes a certificate issuance request
type IssuanceRequest struct {
PublicKey crypto.PublicKey
// ProfileConfig describes the certificate issuance constraints for all issuers.
type ProfileConfig struct {
AllowMustStaple bool
AllowCTPoison bool
AllowSCTList bool
AllowCommonName bool
Serial []byte
NotBefore time.Time
NotAfter time.Time
CommonName string
DNSNames []string
IncludeMustStaple bool
IncludeCTPoison bool
SCTList []ct.SignedCertificateTimestamp
}
// PolicyQualifier describes a policy qualifier
type PolicyQualifier struct {
Type string
Value string
Policies []PolicyInformation
MaxValidityPeriod cmd.ConfigDuration
MaxValidityBackdate cmd.ConfigDuration
}
// PolicyInformation describes a policy
@ -57,26 +51,105 @@ type PolicyInformation struct {
Qualifiers []PolicyQualifier
}
// ProfileConfig describes the certificate issuance constraints
type ProfileConfig struct {
// PolicyQualifier describes a policy qualifier
type PolicyQualifier struct {
Type string
Value string
}
// IssuerConfig describes the constraints on and URLs used by a single issuer.
type IssuerConfig struct {
UseForRSALeaves bool
UseForECDSALeaves bool
AllowMustStaple bool
AllowCTPoison bool
AllowSCTList bool
AllowCommonName bool
IssuerURL string
OCSPURL string
CRLURL string
IssuerURL string
OCSPURL string
CRLURL string
Policies []PolicyInformation
MaxValidityPeriod cmd.ConfigDuration
MaxValidityBackdate cmd.ConfigDuration
Location IssuerLoc
}
// The internal structure created by reading in ProfileConfigs
type issuanceProfile struct {
// IssuerLoc describes the on-disk location and parameters that an issuer
// should use to retrieve its certificate and private key.
// Only one of File, ConfigFile, or PKCS11 should be set.
type IssuerLoc struct {
// A file from which a private key will be read and parsed.
File string
// A file from which a pkcs11key.Config will be read and parsed, if File is not set.
ConfigFile string
// An in-memory pkcs11key.Config, which will be used if ConfigFile is not set.
PKCS11 *pkcs11key.Config
// A file from which a certificate will be read and parsed.
CertFile string
// Number of sessions to open with the HSM. For maximum performance,
// this should be equal to the number of cores in the HSM. Defaults to 1.
NumSessions int
}
// LoadIssuer loads a signer (private key) and certificate from the locations specified.
func LoadIssuer(location IssuerLoc) (*x509.Certificate, crypto.Signer, error) {
cert, err := core.LoadCert(location.CertFile)
if err != nil {
return nil, nil, err
}
signer, err := loadSigner(location, cert)
if err != nil {
return nil, nil, err
}
if !core.KeyDigestEquals(signer.Public(), cert.PublicKey) {
return nil, nil, fmt.Errorf("Issuer key did not match issuer cert %s", location.CertFile)
}
return cert, signer, err
}
func loadSigner(location IssuerLoc, cert *x509.Certificate) (crypto.Signer, error) {
if location.File != "" {
keyBytes, err := ioutil.ReadFile(location.File)
if err != nil {
return nil, fmt.Errorf("Could not read key file %s", location.File)
}
signer, err := helpers.ParsePrivateKeyPEM(keyBytes)
if err != nil {
return nil, err
}
return signer, nil
}
var pkcs11Config *pkcs11key.Config
if location.ConfigFile != "" {
contents, err := ioutil.ReadFile(location.ConfigFile)
if err != nil {
return nil, err
}
pkcs11Config = new(pkcs11key.Config)
err = json.Unmarshal(contents, pkcs11Config)
if err != nil {
return nil, err
}
} else {
pkcs11Config = location.PKCS11
}
if pkcs11Config.Module == "" ||
pkcs11Config.TokenLabel == "" ||
pkcs11Config.PIN == "" {
return nil, fmt.Errorf("Missing a field in pkcs11Config %#v", pkcs11Config)
}
numSessions := location.NumSessions
if numSessions <= 0 {
numSessions = 1
}
return pkcs11key.NewPool(numSessions, pkcs11Config.Module,
pkcs11Config.TokenLabel, pkcs11Config.PIN, cert.PublicKey)
}
// Profile is the validated structure created by reading in ProfileConfigs and IssuerConfigs
type Profile struct {
useForRSALeaves bool
useForECDSALeaves bool
@ -114,29 +187,31 @@ var stringToQualifierType = map[string]asn1.ObjectIdentifier{
"id-qt-cps": policyasn1.CPSQualifierOID,
}
func newProfile(config ProfileConfig) (*issuanceProfile, error) {
if config.IssuerURL == "" {
// 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 config.OCSPURL == "" {
if issuerConfig.OCSPURL == "" {
return nil, errors.New("OCSP URL is required")
}
sp := &issuanceProfile{
useForRSALeaves: config.UseForRSALeaves,
useForECDSALeaves: config.UseForECDSALeaves,
allowMustStaple: config.AllowMustStaple,
allowCTPoison: config.AllowCTPoison,
allowSCTList: config.AllowSCTList,
allowCommonName: config.AllowCommonName,
issuerURL: config.IssuerURL,
crlURL: config.CRLURL,
ocspURL: config.OCSPURL,
maxBackdate: config.MaxValidityBackdate.Duration,
maxValidity: config.MaxValidityPeriod.Duration,
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,
}
if len(config.Policies) > 0 {
if len(profileConfig.Policies) > 0 {
var policies []policyasn1.PolicyInformation
for _, policyConfig := range config.Policies {
for _, policyConfig := range profileConfig.Policies {
id, err := parseOID(policyConfig.OID)
if err != nil {
return nil, fmt.Errorf("failed parsing policy OID %q: %s", policyConfig.OID, err)
@ -169,7 +244,7 @@ func newProfile(config ProfileConfig) (*issuanceProfile, error) {
// requestValid verifies the passed IssuanceRequest against the profile. If the
// request doesn't match the signing profile an error is returned.
func (p *issuanceProfile) requestValid(clk clock.Clock, req *IssuanceRequest) error {
func (p *Profile) requestValid(clk clock.Clock, req *IssuanceRequest) error {
switch req.PublicKey.(type) {
case *rsa.PublicKey:
if !p.useForRSALeaves {
@ -230,7 +305,7 @@ var defaultEKU = []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
}
func (p *issuanceProfile) generateTemplate(clk clock.Clock) *x509.Certificate {
func (p *Profile) generateTemplate(clk clock.Clock) *x509.Certificate {
template := &x509.Certificate{
SignatureAlgorithm: p.sigAlg,
ExtKeyUsage: defaultEKU,
@ -251,31 +326,19 @@ func (p *issuanceProfile) generateTemplate(clk clock.Clock) *x509.Certificate {
}
// Issuer is capable of issuing new certificates
// TODO(#5086): make Cert and Signer private when they're no longer needed by ca.internalIssuer
type Issuer struct {
cert *x509.Certificate
signer crypto.Signer
profile *issuanceProfile
lintKey crypto.Signer
lints zlint.Registry
clk clock.Clock
Cert *x509.Certificate
Signer crypto.Signer
Profile *Profile
Linter *lint.Linter
Clk clock.Clock
}
// IssuerConfig contains the information necessary to construct an Issuer
type IssuerConfig struct {
Cert *x509.Certificate
Signer crypto.Signer
Profile ProfileConfig
IgnoredLints []string
Clk clock.Clock
}
// New constructs an Issuer from the provided IssuerConfig
func New(config IssuerConfig) (*Issuer, error) {
profile, err := newProfile(config.Profile)
if err != nil {
return nil, err
}
switch k := config.Cert.PublicKey.(type) {
// NewIssuer constructs an Issuer on the heap, verifying that the profile
// is well-formed.
func NewIssuer(cert *x509.Certificate, signer crypto.Signer, profile *Profile, linter *lint.Linter, clk clock.Clock) (*Issuer, error) {
switch k := cert.PublicKey.(type) {
case *rsa.PublicKey:
profile.sigAlg = x509.SHA256WithRSA
case *ecdsa.PublicKey:
@ -290,34 +353,42 @@ func New(config IssuerConfig) (*Issuer, error) {
default:
return nil, errors.New("unsupported issuer key type")
}
lintKey, err := lint.MakeSigner(config.Signer)
if err != nil {
return nil, err
}
lints, err := zlint.GlobalRegistry().Filter(zlint.FilterOptions{
ExcludeNames: config.IgnoredLints,
ExcludeSources: []zlint.LintSource{
// We ignore the ETSI and EVG lints since they do not
// apply to the certificates we issue, and not attempting
// to apply them will save some cycles.
zlint.CABFEVGuidelines,
zlint.EtsiEsi,
},
})
if err != nil {
return nil, err
}
i := &Issuer{
cert: config.Cert,
signer: config.Signer,
clk: config.Clk,
lints: lints,
lintKey: lintKey,
profile: profile,
Cert: cert,
Signer: signer,
Profile: profile,
Linter: linter,
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.Profile.useForRSALeaves {
algs = append(algs, x509.RSA)
}
if i.Profile.useForECDSALeaves {
algs = append(algs, x509.ECDSA)
}
return algs
}
// Name provides the Common Name specified in the issuer's certificate.
func (i *Issuer) Name() string {
return i.Cert.Subject.CommonName
}
// ID provides a stable ID for an issuer's certificate. This is used for
// identifying which issuer issued a certificate in the certificateStatus table.
func (i *Issuer) ID() int64 {
h := sha256.Sum256(i.Cert.Raw)
return big.NewInt(0).SetBytes(h[:4]).Int64()
}
var ctPoisonExt = pkix.Extension{
// OID for CT poison, RFC 6962 (was never assigned a proper id-pe- name)
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3},
@ -377,6 +448,23 @@ func generateSKID(pk crypto.PublicKey) ([]byte, error) {
return skid[:], nil
}
// IssuanceRequest describes a certificate issuance request
type IssuanceRequest struct {
PublicKey crypto.PublicKey
Serial []byte
NotBefore time.Time
NotAfter time.Time
CommonName string
DNSNames []string
IncludeMustStaple bool
IncludeCTPoison bool
SCTList []ct.SignedCertificateTimestamp
}
// Issue generates a certificate from the provided issuance request and
// signs it. Before signing the certificate with the issuer's private
// key, it is signed using a throwaway key so that it can be linted using
@ -384,12 +472,12 @@ func generateSKID(pk crypto.PublicKey) ([]byte, error) {
// is not signed using the issuer's key.
func (i *Issuer) Issue(req *IssuanceRequest) ([]byte, error) {
// check request is valid according to the issuance profile
if err := i.profile.requestValid(i.clk, req); err != nil {
if err := i.Profile.requestValid(i.Clk, req); err != nil {
return nil, err
}
// generate template from the issuance profile
template := i.profile.generateTemplate(i.clk)
template := i.Profile.generateTemplate(i.Clk)
// populate template from the issuance request
template.NotBefore, template.NotAfter = req.NotBefore, req.NotAfter
@ -398,7 +486,7 @@ func (i *Issuer) Issue(req *IssuanceRequest) ([]byte, error) {
template.Subject.CommonName = req.CommonName
}
template.DNSNames = req.DNSNames
template.AuthorityKeyId = i.cert.SubjectKeyId
template.AuthorityKeyId = i.Cert.SubjectKeyId
skid, err := generateSKID(req.PublicKey)
if err != nil {
return nil, err
@ -427,16 +515,12 @@ func (i *Issuer) Issue(req *IssuanceRequest) ([]byte, error) {
// check that the tbsCertificate is properly formed by signing it
// with a throwaway key and then linting it using zlint
lintCert, err := lint.MakeLintCert(template, i.cert, req.PublicKey, i.lintKey)
if err != nil {
return nil, err
}
err = lint.LintCert(lintCert, i.lints)
err = i.Linter.LintTBS(template, i.Cert, req.PublicKey)
if err != nil {
return nil, fmt.Errorf("tbsCertificate linting failed: %w", err)
}
return x509.CreateCertificate(rand.Reader, template, i.cert, req.PublicKey, i.signer)
return x509.CreateCertificate(rand.Reader, template, i.Cert, req.PublicKey, i.Signer)
}
func ContainsMustStaple(extensions []pkix.Extension) bool {

View File

@ -19,20 +19,17 @@ import (
ct "github.com/google/certificate-transparency-go"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/lint"
"github.com/letsencrypt/boulder/policyasn1"
"github.com/letsencrypt/boulder/test"
)
func defaultProfileConfig() ProfileConfig {
return ProfileConfig{
UseForECDSALeaves: true,
UseForRSALeaves: true,
AllowCommonName: true,
AllowCTPoison: true,
AllowSCTList: true,
AllowMustStaple: true,
IssuerURL: "http://issuer-url",
OCSPURL: "http://ocsp-url",
AllowCommonName: true,
AllowCTPoison: true,
AllowSCTList: true,
AllowMustStaple: true,
Policies: []PolicyInformation{
{OID: "1.2.3"},
},
@ -41,6 +38,45 @@ func defaultProfileConfig() ProfileConfig {
}
}
func defaultIssuerConfig() IssuerConfig {
return IssuerConfig{
UseForECDSALeaves: true,
UseForRSALeaves: true,
IssuerURL: "http://issuer-url",
OCSPURL: "http://ocsp-url",
}
}
func defaultProfile() *Profile {
p, _ := NewProfile(defaultProfileConfig(), defaultIssuerConfig())
return p
}
var issuerCert *x509.Certificate
var issuerSigner *ecdsa.PrivateKey
func TestMain(m *testing.M) {
tk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
cmd.FailOnError(err, "failed to generate test key")
issuerSigner = tk
template := &x509.Certificate{
SerialNumber: big.NewInt(123),
PublicKey: tk.Public(),
BasicConstraintsValid: true,
IsCA: true,
Subject: pkix.Name{
CommonName: "big ca",
},
KeyUsage: x509.KeyUsageCertSign,
SubjectKeyId: []byte{1, 2, 3, 4, 5, 6, 7, 8},
}
issuer, err := x509.CreateCertificate(rand.Reader, template, template, tk.Public(), tk)
cmd.FailOnError(err, "failed to generate test issuer")
issuerCert, err = x509.ParseCertificate(issuer)
cmd.FailOnError(err, "failed to parse test issuer")
os.Exit(m.Run())
}
func TestNewProfilePolicies(t *testing.T) {
config := defaultProfileConfig()
config.Policies = append(config.Policies, PolicyInformation{
@ -52,9 +88,9 @@ func TestNewProfilePolicies(t *testing.T) {
},
},
})
profile, err := newProfile(config)
test.AssertNotError(t, err, "newProfile failed")
test.AssertDeepEquals(t, *profile, issuanceProfile{
profile, err := NewProfile(config, defaultIssuerConfig())
test.AssertNotError(t, err, "NewProfile failed")
test.AssertDeepEquals(t, *profile, Profile{
useForRSALeaves: true,
useForECDSALeaves: true,
allowMustStaple: true,
@ -87,33 +123,29 @@ func TestNewProfilePolicies(t *testing.T) {
}
func TestNewProfileNoIssuerURL(t *testing.T) {
_, err := newProfile(ProfileConfig{})
test.AssertError(t, err, "newProfile didn't fail with no issuer URL")
_, 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{IssuerURL: "issuer-url"})
test.AssertError(t, err, "newProfile didn't fail with no OCSP URL")
_, 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 TestNewProfileInvalidOID(t *testing.T) {
_, err := newProfile(ProfileConfig{
IssuerURL: "issuer-url",
OCSPURL: "ocsp-url",
_, err := NewProfile(ProfileConfig{
Policies: []PolicyInformation{{
OID: "a.b.c",
}},
})
test.AssertError(t, err, "newProfile didn't fail with unknown policy qualifier type")
}, defaultIssuerConfig())
test.AssertError(t, err, "NewProfile didn't fail with unknown policy qualifier type")
test.AssertEquals(t, err.Error(), "failed parsing policy OID \"a.b.c\": strconv.Atoi: parsing \"a\": invalid syntax")
}
func TestNewProfileUnknownQualifierType(t *testing.T) {
_, err := newProfile(ProfileConfig{
IssuerURL: "issuer-url",
OCSPURL: "ocsp-url",
_, err := NewProfile(ProfileConfig{
Policies: []PolicyInformation{{
OID: "1.2.3",
Qualifiers: []PolicyQualifier{{
@ -121,8 +153,8 @@ func TestNewProfileUnknownQualifierType(t *testing.T) {
Value: "bad",
}},
}},
})
test.AssertError(t, err, "newProfile didn't fail with unknown policy qualifier type")
}, defaultIssuerConfig())
test.AssertError(t, err, "NewProfile didn't fail with unknown policy qualifier type")
test.AssertEquals(t, err.Error(), "unknown qualifier type: asd")
}
@ -131,31 +163,31 @@ func TestRequestValid(t *testing.T) {
fc.Add(time.Hour * 24)
tests := []struct {
name string
profile *issuanceProfile
profile *Profile
request *IssuanceRequest
expectedError string
}{
{
name: "unsupported key type",
profile: &issuanceProfile{},
profile: &Profile{},
request: &IssuanceRequest{PublicKey: &dsa.PublicKey{}},
expectedError: "unsupported public key type",
},
{
name: "cannot sign rsa",
profile: &issuanceProfile{},
profile: &Profile{},
request: &IssuanceRequest{PublicKey: &rsa.PublicKey{}},
expectedError: "cannot sign RSA public keys",
},
{
name: "cannot sign ecdsa",
profile: &issuanceProfile{},
profile: &Profile{},
request: &IssuanceRequest{PublicKey: &ecdsa.PublicKey{}},
expectedError: "cannot sign ECDSA public keys",
},
{
name: "must staple not allowed",
profile: &issuanceProfile{
profile: &Profile{
useForECDSALeaves: true,
},
request: &IssuanceRequest{
@ -166,7 +198,7 @@ func TestRequestValid(t *testing.T) {
},
{
name: "ct poison not allowed",
profile: &issuanceProfile{
profile: &Profile{
useForECDSALeaves: true,
},
request: &IssuanceRequest{
@ -177,7 +209,7 @@ func TestRequestValid(t *testing.T) {
},
{
name: "sct list not allowed",
profile: &issuanceProfile{
profile: &Profile{
useForECDSALeaves: true,
},
request: &IssuanceRequest{
@ -188,7 +220,7 @@ func TestRequestValid(t *testing.T) {
},
{
name: "sct list and ct poison not allowed",
profile: &issuanceProfile{
profile: &Profile{
useForECDSALeaves: true,
allowCTPoison: true,
allowSCTList: true,
@ -202,7 +234,7 @@ func TestRequestValid(t *testing.T) {
},
{
name: "common name not allowed",
profile: &issuanceProfile{
profile: &Profile{
useForECDSALeaves: true,
},
request: &IssuanceRequest{
@ -213,7 +245,7 @@ func TestRequestValid(t *testing.T) {
},
{
name: "negative validity",
profile: &issuanceProfile{
profile: &Profile{
useForECDSALeaves: true,
},
request: &IssuanceRequest{
@ -225,7 +257,7 @@ func TestRequestValid(t *testing.T) {
},
{
name: "validity larger than max",
profile: &issuanceProfile{
profile: &Profile{
useForECDSALeaves: true,
maxValidity: time.Minute,
},
@ -238,7 +270,7 @@ func TestRequestValid(t *testing.T) {
},
{
name: "validity backdated more than max",
profile: &issuanceProfile{
profile: &Profile{
useForECDSALeaves: true,
maxValidity: time.Hour * 2,
maxBackdate: time.Hour,
@ -252,7 +284,7 @@ func TestRequestValid(t *testing.T) {
},
{
name: "validity is forward dated",
profile: &issuanceProfile{
profile: &Profile{
useForECDSALeaves: true,
maxValidity: time.Hour * 2,
maxBackdate: time.Hour,
@ -266,7 +298,7 @@ func TestRequestValid(t *testing.T) {
},
{
name: "serial too short",
profile: &issuanceProfile{
profile: &Profile{
useForECDSALeaves: true,
maxValidity: time.Hour * 2,
},
@ -279,7 +311,7 @@ func TestRequestValid(t *testing.T) {
},
{
name: "serial too long",
profile: &issuanceProfile{
profile: &Profile{
useForECDSALeaves: true,
maxValidity: time.Hour * 2,
},
@ -293,7 +325,7 @@ func TestRequestValid(t *testing.T) {
},
{
name: "good",
profile: &issuanceProfile{
profile: &Profile{
useForECDSALeaves: true,
maxValidity: time.Hour * 2,
},
@ -325,12 +357,12 @@ func TestRequestValid(t *testing.T) {
func TestGenerateTemplate(t *testing.T) {
tests := []struct {
name string
profile *issuanceProfile
profile *Profile
expectedTemplate *x509.Certificate
}{
{
name: "crl url",
profile: &issuanceProfile{
profile: &Profile{
crlURL: "crl-url",
sigAlg: x509.SHA256WithRSA,
},
@ -345,7 +377,7 @@ func TestGenerateTemplate(t *testing.T) {
},
{
name: "include policies",
profile: &issuanceProfile{
profile: &Profile{
sigAlg: x509.SHA256WithRSA,
policies: &pkix.Extension{
Id: asn1.ObjectIdentifier{1, 2, 3},
@ -378,82 +410,19 @@ func TestGenerateTemplate(t *testing.T) {
}
func TestNewSignerUnsupportedKeyType(t *testing.T) {
_, err := New(IssuerConfig{
Profile: defaultProfileConfig(),
Cert: &x509.Certificate{
_, err := NewIssuer(
&x509.Certificate{
PublicKey: &ed25519.PublicKey{},
},
Signer: &ed25519.PrivateKey{},
})
test.AssertError(t, err, "issuance.New didn't fail")
&ed25519.PrivateKey{},
defaultProfile(),
&lint.Linter{},
clock.NewFake(),
)
test.AssertError(t, err, "NewIssuer didn't fail")
test.AssertEquals(t, err.Error(), "unsupported issuer key type")
}
func TestNewSignerRSAKey(t *testing.T) {
mod, ok := big.NewInt(0).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16)
test.Assert(t, ok, "failed to set mod")
signer, err := New(IssuerConfig{
Profile: defaultProfileConfig(),
Cert: &x509.Certificate{
PublicKey: &rsa.PublicKey{
N: mod,
},
},
Signer: &rsa.PrivateKey{
PublicKey: rsa.PublicKey{
N: mod,
},
},
})
test.AssertNotError(t, err, "issuance.New failed")
_, ok = signer.lintKey.(*rsa.PrivateKey)
test.Assert(t, ok, "lint key is not RSA")
}
func TestNewSignerECDSAKey(t *testing.T) {
signer, err := New(IssuerConfig{
Profile: defaultProfileConfig(),
Cert: &x509.Certificate{
PublicKey: &ecdsa.PublicKey{
Curve: elliptic.P256(),
},
},
Signer: &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: elliptic.P256(),
},
},
})
test.AssertNotError(t, err, "issuance.New failed")
_, ok := signer.lintKey.(*ecdsa.PrivateKey)
test.Assert(t, ok, "lint key is not ECDSA")
}
var issuerCert *x509.Certificate
var issuerSigner *ecdsa.PrivateKey
func TestMain(m *testing.M) {
tk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
cmd.FailOnError(err, "failed to generate test key")
issuerSigner = tk
template := &x509.Certificate{
SerialNumber: big.NewInt(123),
PublicKey: tk.Public(),
BasicConstraintsValid: true,
IsCA: true,
Subject: pkix.Name{
CommonName: "big ca",
},
KeyUsage: x509.KeyUsageCertSign,
SubjectKeyId: []byte{1, 2, 3, 4, 5, 6, 7, 8},
}
issuer, err := x509.CreateCertificate(rand.Reader, template, template, tk.Public(), tk)
cmd.FailOnError(err, "failed to generate test issuer")
issuerCert, err = x509.ParseCertificate(issuer)
cmd.FailOnError(err, "failed to parse test issuer")
os.Exit(m.Run())
}
func TestIssue(t *testing.T) {
for _, tc := range []struct {
name string
@ -478,14 +447,12 @@ func TestIssue(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
signer, err := New(IssuerConfig{
Cert: issuerCert,
Signer: issuerSigner,
Clk: fc,
Profile: defaultProfileConfig(),
IgnoredLints: []string{"w_ct_sct_policy_count_unsatisfied", "n_subject_common_name_included"},
})
test.AssertNotError(t, err, "issuance.New failed")
linter, _ := lint.NewLinter(
issuerSigner,
[]string{"w_ct_sct_policy_count_unsatisfied", "n_subject_common_name_included"},
)
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := tc.generateFunc()
test.AssertNotError(t, err, "failed to generate test key")
certBytes, err := signer.Issue(&IssuanceRequest{
@ -514,14 +481,12 @@ func TestIssue(t *testing.T) {
func TestIssueRSA(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
signer, err := New(IssuerConfig{
Cert: issuerCert,
Signer: issuerSigner,
Clk: fc,
Profile: defaultProfileConfig(),
IgnoredLints: []string{"w_ct_sct_policy_count_unsatisfied"},
})
test.AssertNotError(t, err, "issuance.New failed")
linter, _ := lint.NewLinter(
issuerSigner,
[]string{"w_ct_sct_policy_count_unsatisfied"},
)
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")
certBytes, err := signer.Issue(&IssuanceRequest{
@ -545,14 +510,12 @@ func TestIssueRSA(t *testing.T) {
func TestIssueCTPoison(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
signer, err := New(IssuerConfig{
Cert: issuerCert,
Signer: issuerSigner,
Clk: fc,
Profile: defaultProfileConfig(),
IgnoredLints: []string{"w_ct_sct_policy_count_unsatisfied"},
})
test.AssertNotError(t, err, "issuance.New failed")
linter, _ := lint.NewLinter(
issuerSigner,
[]string{"w_ct_sct_policy_count_unsatisfied"},
)
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
certBytes, err := signer.Issue(&IssuanceRequest{
@ -577,14 +540,12 @@ func TestIssueCTPoison(t *testing.T) {
func TestIssueSCTList(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
signer, err := New(IssuerConfig{
Cert: issuerCert,
Signer: issuerSigner,
Clk: fc,
Profile: defaultProfileConfig(),
IgnoredLints: []string{"w_ct_sct_policy_count_unsatisfied"},
})
test.AssertNotError(t, err, "issuance.New failed")
linter, _ := lint.NewLinter(
issuerSigner,
[]string{"w_ct_sct_policy_count_unsatisfied"},
)
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
certBytes, err := signer.Issue(&IssuanceRequest{
@ -614,14 +575,12 @@ func TestIssueSCTList(t *testing.T) {
func TestIssueMustStaple(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
signer, err := New(IssuerConfig{
Cert: issuerCert,
Signer: issuerSigner,
Clk: fc,
Profile: defaultProfileConfig(),
IgnoredLints: []string{"w_ct_sct_policy_count_unsatisfied"},
})
test.AssertNotError(t, err, "issuance.New failed")
linter, _ := lint.NewLinter(
issuerSigner,
[]string{"w_ct_sct_policy_count_unsatisfied"},
)
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
certBytes, err := signer.Issue(&IssuanceRequest{
@ -646,13 +605,9 @@ func TestIssueMustStaple(t *testing.T) {
func TestIssueBadLint(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
signer, err := New(IssuerConfig{
Cert: issuerCert,
Signer: issuerSigner,
Clk: fc,
Profile: defaultProfileConfig(),
})
test.AssertNotError(t, err, "issuance.New failed")
linter, _ := lint.NewLinter(issuerSigner, []string{})
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, 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.Issue(&IssuanceRequest{

View File

@ -29,17 +29,11 @@ func Check(tbs, issuer *x509.Certificate, subjectPubKey crypto.PublicKey, realSi
if err != nil {
return err
}
lints, err := lint.GlobalRegistry().Filter(lint.FilterOptions{
ExcludeNames: skipLints,
ExcludeSources: []lint.LintSource{
lint.CABFEVGuidelines,
lint.EtsiEsi,
},
})
reg, err := MakeRegistry(skipLints)
if err != nil {
return fmt.Errorf("failed to create lint registry: %w", err)
return err
}
return LintCert(lintCert, lints)
return LintCert(lintCert, reg)
}
// MakeSigner creates a throwaway crypto.Signer with the same key algorithm
@ -65,6 +59,22 @@ func MakeSigner(realSigner crypto.Signer) (crypto.Signer, error) {
return lintSigner, nil
}
// MakeRegistry creates a zlint Registry of lints to run, filtering out the
// EV- and ETSI-specific lints, as well as any others specified.
func MakeRegistry(skipLints []string) (lint.Registry, error) {
reg, err := lint.GlobalRegistry().Filter(lint.FilterOptions{
ExcludeNames: skipLints,
ExcludeSources: []lint.LintSource{
lint.CABFEVGuidelines,
lint.EtsiEsi,
},
})
if err != nil {
return nil, fmt.Errorf("failed to create lint registry: %w", err)
}
return reg, nil
}
// MakeLintCert creates a throwaway x509.Certificate which can be linted.
// Only use the result from MakeSigner as the final argument.
func MakeLintCert(tbs, issuer *x509.Certificate, subjectPubKey crypto.PublicKey, lintSigner crypto.Signer) (*zlintx509.Certificate, error) {
@ -94,3 +104,28 @@ func LintCert(lintCert *zlintx509.Certificate, lints lint.Registry) error {
}
return nil
}
type Linter struct {
signer crypto.Signer
registry lint.Registry
}
func NewLinter(realSigner crypto.Signer, skipLints []string) (*Linter, error) {
signer, err := MakeSigner(realSigner)
if err != nil {
return nil, err
}
reg, err := MakeRegistry(skipLints)
if err != nil {
return nil, err
}
return &Linter{signer, reg}, nil
}
func (l Linter) LintTBS(tbs, issuer *x509.Certificate, subjectPubKey crypto.PublicKey) error {
cert, err := MakeLintCert(tbs, issuer, subjectPubKey, l.signer)
if err != nil {
return err
}
return LintCert(cert, l.registry)
}