ca: implement our own certificate issuance lib (#5007)
Adds a replacement issuance library that replaces CFSSL. Usage of the new library is gated by a feature, meaning until we fully deploy the new signer we need to support both the new one and CFSSL, which makes a few things a bit complicated. One Big follow-up change is that once CFSSL is completely gone we'll be able to stop using CSRs as the internal representation of issuance requests (i.e. instead of passing a CSR all the way through from the WFE -> CA and then converting it to the new signer.IssuanceRequest, we can just construct a signer.IssuanceRequest at the WFE (or RA) and pass that through the backend instead, making things a lot less opaque). Fixes #4906.
This commit is contained in:
parent
8556d8a801
commit
85851a6f2e
301
ca/ca.go
301
ca/ca.go
|
@ -41,6 +41,7 @@ import (
|
|||
"github.com/letsencrypt/boulder/goodkey"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
sapb "github.com/letsencrypt/boulder/sa/proto"
|
||||
bsigner "github.com/letsencrypt/boulder/signer"
|
||||
)
|
||||
|
||||
// Miscellaneous PKIX OIDs that we need to refer to
|
||||
|
@ -156,11 +157,33 @@ type localSigner interface {
|
|||
// issuer, including the cfssl signer and OCSP signer objects.
|
||||
type internalIssuer struct {
|
||||
cert *x509.Certificate
|
||||
eeSigner localSigner
|
||||
ocspSigner crypto.Signer
|
||||
|
||||
// Only one of cfsslSigner and boulderSigner will be non-nill
|
||||
cfsslSigner localSigner
|
||||
boulderSigner *bsigner.Signer
|
||||
}
|
||||
|
||||
func makeInternalIssuers(
|
||||
func makeInternalIssuers(issuers []bsigner.Config, lifespanOCSP time.Duration) (map[string]*internalIssuer, error) {
|
||||
internalIssuers := make(map[string]*internalIssuer, len(issuers))
|
||||
for _, issuer := range issuers {
|
||||
signer, err := bsigner.NewSigner(issuer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if internalIssuers[issuer.Issuer.Subject.CommonName] != nil {
|
||||
return nil, errors.New("Multiple issuer certs with the same CommonName are not supported")
|
||||
}
|
||||
internalIssuers[issuer.Issuer.Subject.CommonName] = &internalIssuer{
|
||||
cert: issuer.Issuer,
|
||||
ocspSigner: issuer.Signer,
|
||||
boulderSigner: signer,
|
||||
}
|
||||
}
|
||||
return internalIssuers, nil
|
||||
}
|
||||
|
||||
func makeCFSSLInternalIssuers(
|
||||
issuers []Issuer,
|
||||
policy *cfsslConfig.Signing,
|
||||
lifespanOCSP time.Duration,
|
||||
|
@ -173,7 +196,7 @@ func makeInternalIssuers(
|
|||
if iss.Cert == nil || iss.Signer == nil {
|
||||
return nil, errors.New("Issuer with nil cert or signer specified.")
|
||||
}
|
||||
eeSigner, err := local.NewSigner(iss.Signer, iss.Cert, x509.SHA256WithRSA, policy)
|
||||
cfsslSigner, err := local.NewSigner(iss.Signer, iss.Cert, x509.SHA256WithRSA, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -183,9 +206,9 @@ func makeInternalIssuers(
|
|||
return nil, errors.New("Multiple issuer certs with the same CommonName are not supported")
|
||||
}
|
||||
internalIssuers[cn] = &internalIssuer{
|
||||
cert: iss.Cert,
|
||||
eeSigner: eeSigner,
|
||||
ocspSigner: iss.Signer,
|
||||
cert: iss.Cert,
|
||||
cfsslSigner: cfsslSigner,
|
||||
ocspSigner: iss.Signer,
|
||||
}
|
||||
}
|
||||
return internalIssuers, nil
|
||||
|
@ -208,7 +231,8 @@ func NewCertificateAuthorityImpl(
|
|||
pa core.PolicyAuthority,
|
||||
clk clock.Clock,
|
||||
stats prometheus.Registerer,
|
||||
issuers []Issuer,
|
||||
cfsslIssuers []Issuer,
|
||||
boulderIssuers []bsigner.Config,
|
||||
keyPolicy goodkey.KeyPolicy,
|
||||
logger blog.Logger,
|
||||
orphanQueue *goque.Queue,
|
||||
|
@ -221,41 +245,53 @@ func NewCertificateAuthorityImpl(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// CFSSL requires processing JSON configs through its own LoadConfig, so we
|
||||
// serialize and then deserialize.
|
||||
cfsslJSON, err := json.Marshal(config.CFSSL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfsslConfigObj, err := cfsslConfig.LoadConfig(cfsslJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.LifespanOCSP.Duration == 0 {
|
||||
return nil, errors.New("Config must specify an OCSP lifespan period.")
|
||||
}
|
||||
|
||||
for _, profile := range cfsslConfigObj.Signing.Profiles {
|
||||
if len(profile.IssuerURL) > 1 {
|
||||
return nil, errors.New("only one issuer_url supported")
|
||||
var internalIssuers map[string]*internalIssuer
|
||||
var defaultIssuer *internalIssuer
|
||||
// rsaProfile and ecdsaProfile are unused when using the boulder signer
|
||||
// instead of the CFSSL signer
|
||||
var rsaProfile, ecdsaProfile string
|
||||
if features.Enabled(features.NonCFSSLSigner) {
|
||||
internalIssuers, err = makeInternalIssuers(boulderIssuers, config.LifespanOCSP.Duration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defaultIssuer = internalIssuers[boulderIssuers[0].Issuer.Subject.CommonName]
|
||||
} else {
|
||||
// CFSSL requires processing JSON configs through its own LoadConfig, so we
|
||||
// serialize and then deserialize.
|
||||
cfsslJSON, err := json.Marshal(config.CFSSL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfsslConfigObj, err := cfsslConfig.LoadConfig(cfsslJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
internalIssuers, err := makeInternalIssuers(
|
||||
issuers,
|
||||
cfsslConfigObj.Signing,
|
||||
config.LifespanOCSP.Duration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defaultIssuer := internalIssuers[issuers[0].Cert.Subject.CommonName]
|
||||
if config.LifespanOCSP.Duration == 0 {
|
||||
return nil, errors.New("Config must specify an OCSP lifespan period.")
|
||||
}
|
||||
|
||||
rsaProfile := config.RSAProfile
|
||||
ecdsaProfile := config.ECDSAProfile
|
||||
for _, profile := range cfsslConfigObj.Signing.Profiles {
|
||||
if len(profile.IssuerURL) > 1 {
|
||||
return nil, errors.New("only one issuer_url supported")
|
||||
}
|
||||
}
|
||||
|
||||
if rsaProfile == "" || ecdsaProfile == "" {
|
||||
return nil, errors.New("must specify rsaProfile and ecdsaProfile")
|
||||
internalIssuers, err = makeCFSSLInternalIssuers(
|
||||
cfsslIssuers,
|
||||
cfsslConfigObj.Signing,
|
||||
config.LifespanOCSP.Duration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rsaProfile, ecdsaProfile = config.RSAProfile, config.ECDSAProfile
|
||||
|
||||
if rsaProfile == "" || ecdsaProfile == "" {
|
||||
return nil, errors.New("must specify rsaProfile and ecdsaProfile")
|
||||
}
|
||||
defaultIssuer = internalIssuers[cfsslIssuers[0].Cert.Subject.CommonName]
|
||||
}
|
||||
|
||||
csrExtensionCount := prometheus.NewCounterVec(
|
||||
|
@ -619,19 +655,32 @@ func (ca *CertificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
|
|||
}
|
||||
scts = append(scts, sct)
|
||||
}
|
||||
certPEM, err := ca.defaultIssuer.eeSigner.SignFromPrecert(precert, scts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
var certDER []byte
|
||||
if features.Enabled(features.NonCFSSLSigner) {
|
||||
issuanceReq, err := bsigner.RequestFromPrecert(precert, scts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certDER, err = ca.defaultIssuer.boulderSigner.Issue(issuanceReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
certPEM, err := ca.defaultIssuer.cfsslSigner.SignFromPrecert(precert, scts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ca.signatureCount.WithLabelValues(string(certType)).Inc()
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
err = berrors.InternalServerError("invalid certificate value returned")
|
||||
ca.log.AuditErrf("PEM decode error, aborting: serial=[%s] pem=[%s] err=[%v]", serialHex, certPEM, err)
|
||||
return nil, err
|
||||
}
|
||||
certDER = block.Bytes
|
||||
}
|
||||
ca.signatureCount.WithLabelValues(string(certType)).Inc()
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
err = berrors.InternalServerError("invalid certificate value returned")
|
||||
ca.log.AuditErrf("PEM decode error, aborting: serial=[%s] pem=[%s] err=[%v]", serialHex, certPEM, err)
|
||||
return nil, err
|
||||
}
|
||||
certDER := block.Bytes
|
||||
ca.log.AuditInfof("Signing success: serial=[%s] names=[%s] certificate=[%s]",
|
||||
ca.log.AuditInfof("Signing success: serial=[%s] names=[%s] csr=[%s] certificate=[%s]",
|
||||
serialHex, strings.Join(precert.DNSNames, ", "), hex.EncodeToString(req.DER),
|
||||
hex.EncodeToString(certDER))
|
||||
err = ca.storeCertificate(ctx, req.RegistrationID, req.OrderID, precert.SerialNumber, certDER)
|
||||
|
@ -713,79 +762,101 @@ func (ca *CertificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Convert the CSR to PEM
|
||||
csrPEM := string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csr.Raw,
|
||||
}))
|
||||
|
||||
var profile string
|
||||
switch csr.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
profile = ca.rsaProfile
|
||||
case *ecdsa.PublicKey:
|
||||
profile = ca.ecdsaProfile
|
||||
default:
|
||||
err = berrors.InternalServerError("unsupported key type %T", csr.PublicKey)
|
||||
ca.log.AuditErr(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Send the cert off for signing
|
||||
req := signer.SignRequest{
|
||||
Request: csrPEM,
|
||||
Profile: profile,
|
||||
Hosts: csr.DNSNames,
|
||||
Subject: &signer.Subject{
|
||||
CN: csr.Subject.CommonName,
|
||||
},
|
||||
Serial: serialBigInt,
|
||||
Extensions: extensions,
|
||||
NotBefore: validity.NotBefore,
|
||||
NotAfter: validity.NotAfter,
|
||||
ReturnPrecert: true,
|
||||
}
|
||||
|
||||
serialHex := core.SerialToString(serialBigInt)
|
||||
|
||||
ca.log.AuditInfof("Signing: serial=[%s] names=[%s] csr=[%s]",
|
||||
serialHex, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw))
|
||||
var certDER []byte
|
||||
if features.Enabled(features.NonCFSSLSigner) {
|
||||
ca.log.AuditInfof("Signing: serial=[%s] names=[%s] csr=[%s]",
|
||||
serialHex, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw))
|
||||
certDER, err = issuer.boulderSigner.Issue(&bsigner.IssuanceRequest{
|
||||
PublicKey: csr.PublicKey,
|
||||
Serial: serialBigInt.Bytes(),
|
||||
CommonName: csr.Subject.CommonName,
|
||||
DNSNames: csr.DNSNames,
|
||||
IncludeCTPoison: true,
|
||||
IncludeMustStaple: bsigner.ContainsMustStaple(csr.Extensions),
|
||||
NotBefore: validity.NotBefore,
|
||||
NotAfter: validity.NotAfter,
|
||||
})
|
||||
ca.noteSignError(err)
|
||||
if err != nil {
|
||||
err = berrors.InternalServerError("failed to sign certificate: %s", err)
|
||||
ca.log.AuditErrf("Signing failed: serial=[%s] err=[%v]", serialHex, err)
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Convert the CSR to PEM
|
||||
csrPEM := string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csr.Raw,
|
||||
}))
|
||||
|
||||
certPEM, err := issuer.eeSigner.Sign(req)
|
||||
ca.noteSignError(err)
|
||||
if err != nil {
|
||||
// If the Signing error was a pre-issuance lint error then marshal the
|
||||
// linting errors to include in the audit err msg.
|
||||
if lErr, ok := err.(*local.LintError); ok {
|
||||
// NOTE(@cpu): We throw away the JSON marshal error here. If marshaling
|
||||
// fails for some reason it's acceptable to log an empty string for the
|
||||
// JSON component.
|
||||
lintErrsJSON, _ := json.Marshal(lErr.ErrorResults)
|
||||
ca.log.AuditErrf("Signing failed: serial=[%s] err=[%v] lintErrors=%s",
|
||||
serialHex, err, string(lintErrsJSON))
|
||||
return nil, berrors.InternalServerError("failed to sign certificate: %s", err)
|
||||
var profile string
|
||||
switch csr.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
profile = ca.rsaProfile
|
||||
case *ecdsa.PublicKey:
|
||||
profile = ca.ecdsaProfile
|
||||
default:
|
||||
err = berrors.InternalServerError("unsupported key type %T", csr.PublicKey)
|
||||
ca.log.AuditErr(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = berrors.InternalServerError("failed to sign certificate: %s", err)
|
||||
ca.log.AuditErrf("Signing failed: serial=[%s] err=[%v]", serialHex, err)
|
||||
return nil, err
|
||||
// Send the cert off for signing
|
||||
req := signer.SignRequest{
|
||||
Request: csrPEM,
|
||||
Profile: profile,
|
||||
Hosts: csr.DNSNames,
|
||||
Subject: &signer.Subject{
|
||||
CN: csr.Subject.CommonName,
|
||||
},
|
||||
Serial: serialBigInt,
|
||||
Extensions: extensions,
|
||||
NotBefore: validity.NotBefore,
|
||||
NotAfter: validity.NotAfter,
|
||||
ReturnPrecert: true,
|
||||
}
|
||||
|
||||
ca.log.AuditInfof("Signing: serial=[%s] names=[%s] csr=[%s]",
|
||||
serialHex, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw))
|
||||
|
||||
certPEM, err := issuer.cfsslSigner.Sign(req)
|
||||
ca.noteSignError(err)
|
||||
if err != nil {
|
||||
// If the Signing error was a pre-issuance lint error then marshal the
|
||||
// linting errors to include in the audit err msg.
|
||||
if lErr, ok := err.(*local.LintError); ok {
|
||||
// NOTE(@cpu): We throw away the JSON marshal error here. If marshaling
|
||||
// fails for some reason it's acceptable to log an empty string for the
|
||||
// JSON component.
|
||||
lintErrsJSON, _ := json.Marshal(lErr.ErrorResults)
|
||||
ca.log.AuditErrf("Signing failed: serial=[%s] err=[%v] lintErrors=%s",
|
||||
serialHex, err, string(lintErrsJSON))
|
||||
return nil, berrors.InternalServerError("failed to sign certificate: %s", err)
|
||||
}
|
||||
|
||||
err = berrors.InternalServerError("failed to sign certificate: %s", err)
|
||||
ca.log.AuditErrf("Signing failed: serial=[%s] err=[%v]", serialHex, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(certPEM) == 0 {
|
||||
err = berrors.InternalServerError("no certificate returned by server")
|
||||
ca.log.AuditErrf("PEM empty from Signer: serial=[%s] err=[%v]", serialHex, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
err = berrors.InternalServerError("invalid certificate value returned")
|
||||
ca.log.AuditErrf("PEM decode error, aborting: serial=[%s] pem=[%s] err=[%v]", serialHex, certPEM, err)
|
||||
return nil, err
|
||||
}
|
||||
certDER = block.Bytes
|
||||
}
|
||||
ca.signatureCount.WithLabelValues(string(precertType)).Inc()
|
||||
|
||||
if len(certPEM) == 0 {
|
||||
err = berrors.InternalServerError("no certificate returned by server")
|
||||
ca.log.AuditErrf("PEM empty from Signer: serial=[%s] err=[%v]", serialHex, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
err = berrors.InternalServerError("invalid certificate value returned")
|
||||
ca.log.AuditErrf("PEM decode error, aborting: serial=[%s] pem=[%s] err=[%v]", serialHex, certPEM, err)
|
||||
return nil, err
|
||||
}
|
||||
certDER := block.Bytes
|
||||
|
||||
ca.log.AuditInfof("Signing success: serial=[%s] names=[%s] csr=[%s] precertificate=[%s]",
|
||||
serialHex, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw),
|
||||
hex.EncodeToString(certDER))
|
||||
|
|
275
ca/ca_test.go
275
ca/ca_test.go
|
@ -42,6 +42,7 @@ import (
|
|||
"github.com/letsencrypt/boulder/metrics"
|
||||
"github.com/letsencrypt/boulder/policy"
|
||||
sapb "github.com/letsencrypt/boulder/sa/proto"
|
||||
bsigner "github.com/letsencrypt/boulder/signer"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
)
|
||||
|
||||
|
@ -131,13 +132,14 @@ func mustRead(path string) []byte {
|
|||
}
|
||||
|
||||
type testCtx struct {
|
||||
caConfig ca_config.CAConfig
|
||||
pa core.PolicyAuthority
|
||||
issuers []Issuer
|
||||
keyPolicy goodkey.KeyPolicy
|
||||
fc clock.FakeClock
|
||||
stats prometheus.Registerer
|
||||
logger *blog.Mock
|
||||
caConfig ca_config.CAConfig
|
||||
pa core.PolicyAuthority
|
||||
issuers []Issuer
|
||||
signerConfigs []bsigner.Config
|
||||
keyPolicy goodkey.KeyPolicy
|
||||
fc clock.FakeClock
|
||||
stats prometheus.Registerer
|
||||
logger *blog.Mock
|
||||
}
|
||||
|
||||
type mockSA struct {
|
||||
|
@ -259,6 +261,30 @@ func setup(t *testing.T) *testCtx {
|
|||
|
||||
issuers := []Issuer{{caKey, caCert}}
|
||||
|
||||
signerConfigs := []bsigner.Config{
|
||||
{
|
||||
Issuer: caCert,
|
||||
Signer: caKey,
|
||||
Clk: fc,
|
||||
Profile: bsigner.ProfileConfig{
|
||||
AllowECDSAKeys: true,
|
||||
AllowRSAKeys: 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: []bsigner.PolicyInformation{
|
||||
{OID: "2.23.140.1.2.1"},
|
||||
},
|
||||
MaxValidityPeriod: cmd.ConfigDuration{Duration: time.Hour * 8760},
|
||||
MaxValidityBackdate: cmd.ConfigDuration{Duration: time.Hour},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
keyPolicy := goodkey.KeyPolicy{
|
||||
AllowRSA: true,
|
||||
AllowECDSANISTP256: true,
|
||||
|
@ -271,6 +297,7 @@ func setup(t *testing.T) *testCtx {
|
|||
caConfig,
|
||||
pa,
|
||||
issuers,
|
||||
signerConfigs,
|
||||
keyPolicy,
|
||||
fc,
|
||||
metrics.NoopRegisterer,
|
||||
|
@ -289,6 +316,7 @@ func TestFailNoSerial(t *testing.T) {
|
|||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
testCtx.issuers,
|
||||
nil,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
nil)
|
||||
|
@ -310,77 +338,88 @@ type IssuanceMode struct {
|
|||
}
|
||||
|
||||
func TestIssuePrecertificate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
csr []byte
|
||||
subTest func(t *testing.T, i *TestCertificateIssuance)
|
||||
}{
|
||||
{"IssuePrecertificate", CNandSANCSR, issueCertificateSubTestIssuePrecertificate},
|
||||
{"ValidityUsesCAClock", CNandSANCSR, issueCertificateSubTestValidityUsesCAClock},
|
||||
{"ProfileSelectionRSA", CNandSANCSR, issueCertificateSubTestProfileSelectionRSA},
|
||||
{"ProfileSelectionECDSA", ECDSACSR, issueCertificateSubTestProfileSelectionECDSA},
|
||||
{"MustStaple", MustStapleCSR, issueCertificateSubTestMustStaple},
|
||||
{"MustStapleDuplicate", DuplicateMustStapleCSR, issueCertificateSubTestMustStaple},
|
||||
{"UnknownExtension", UnsupportedExtensionCSR, issueCertificateSubTestUnknownExtension},
|
||||
{"CTPoisonExtension", CTPoisonExtensionCSR, issueCertificateSubTestCTPoisonExtension},
|
||||
{"CTPoisonExtensionEmpty", CTPoisonExtensionEmptyCSR, issueCertificateSubTestCTPoisonExtension},
|
||||
}
|
||||
for _, nonCFSSL := range []bool{true, false} {
|
||||
testCases := []struct {
|
||||
name string
|
||||
csr []byte
|
||||
subTest func(t *testing.T, i *TestCertificateIssuance)
|
||||
}{
|
||||
{"IssuePrecertificate", CNandSANCSR, issueCertificateSubTestIssuePrecertificate},
|
||||
{"ValidityUsesCAClock", CNandSANCSR, issueCertificateSubTestValidityUsesCAClock},
|
||||
{"ProfileSelectionRSA", CNandSANCSR, issueCertificateSubTestProfileSelectionRSA},
|
||||
{"ProfileSelectionECDSA", ECDSACSR, issueCertificateSubTestProfileSelectionECDSA},
|
||||
{"MustStaple", MustStapleCSR, issueCertificateSubTestMustStaple},
|
||||
{"MustStapleDuplicate", DuplicateMustStapleCSR, issueCertificateSubTestMustStaple},
|
||||
{"UnknownExtension", UnsupportedExtensionCSR, issueCertificateSubTestUnknownExtension},
|
||||
{"CTPoisonExtension", CTPoisonExtensionCSR, issueCertificateSubTestCTPoisonExtension},
|
||||
{"CTPoisonExtensionEmpty", CTPoisonExtensionEmptyCSR, issueCertificateSubTestCTPoisonExtension},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
// The loop through |issuanceModes| must be inside the loop through
|
||||
// |testCases| because the "certificate-for-precertificate" tests use
|
||||
// the precertificates previously generated from the preceding
|
||||
// "precertificate" test. See also the comment above |issuanceModes|.
|
||||
for _, mode := range issuanceModes {
|
||||
ca, sa := issueCertificateSubTestSetup(t)
|
||||
for _, testCase := range testCases {
|
||||
// The loop through |issuanceModes| must be inside the loop through
|
||||
// |testCases| because the "certificate-for-precertificate" tests use
|
||||
// the precertificates previously generated from the preceding
|
||||
// "precertificate" test. See also the comment above |issuanceModes|.
|
||||
for _, mode := range issuanceModes {
|
||||
ca, sa := issueCertificateSubTestSetup(t, nonCFSSL)
|
||||
|
||||
t.Run(mode.name+"-"+testCase.name, func(t *testing.T) {
|
||||
req, err := x509.ParseCertificateRequest(testCase.csr)
|
||||
test.AssertNotError(t, err, "Certificate request failed to parse")
|
||||
t.Run(fmt.Sprintf("%s - %s (using boulder signer: %t)", mode.name, testCase.name, nonCFSSL), func(t *testing.T) {
|
||||
req, err := x509.ParseCertificateRequest(testCase.csr)
|
||||
test.AssertNotError(t, err, "Certificate request failed to parse")
|
||||
|
||||
issueReq := &capb.IssueCertificateRequest{Csr: testCase.csr, RegistrationID: arbitraryRegID}
|
||||
issueReq := &capb.IssueCertificateRequest{Csr: testCase.csr, RegistrationID: arbitraryRegID}
|
||||
|
||||
var certDER []byte
|
||||
response, err := ca.IssuePrecertificate(ctx, issueReq)
|
||||
var certDER []byte
|
||||
response, err := ca.IssuePrecertificate(ctx, issueReq)
|
||||
|
||||
test.AssertNotError(t, err, "Failed to issue precertificate")
|
||||
certDER = response.DER
|
||||
test.AssertNotError(t, err, "Failed to issue precertificate")
|
||||
certDER = response.DER
|
||||
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
test.AssertNotError(t, err, "Certificate failed to parse")
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
test.AssertNotError(t, err, "Certificate failed to parse")
|
||||
|
||||
poisonExtension := findExtension(cert.Extensions, OIDExtensionCTPoison)
|
||||
test.AssertEquals(t, true, poisonExtension != nil)
|
||||
if poisonExtension != nil {
|
||||
test.AssertEquals(t, poisonExtension.Critical, true)
|
||||
test.AssertDeepEquals(t, poisonExtension.Value, []byte{0x05, 0x00}) // ASN.1 DER NULL
|
||||
}
|
||||
poisonExtension := findExtension(cert.Extensions, OIDExtensionCTPoison)
|
||||
test.AssertEquals(t, true, poisonExtension != nil)
|
||||
if poisonExtension != nil {
|
||||
test.AssertEquals(t, poisonExtension.Critical, true)
|
||||
test.AssertDeepEquals(t, poisonExtension.Value, []byte{0x05, 0x00}) // ASN.1 DER NULL
|
||||
}
|
||||
|
||||
i := TestCertificateIssuance{
|
||||
ca: ca,
|
||||
sa: sa,
|
||||
req: req,
|
||||
mode: mode,
|
||||
certDER: certDER,
|
||||
cert: cert,
|
||||
}
|
||||
i := TestCertificateIssuance{
|
||||
ca: ca,
|
||||
sa: sa,
|
||||
req: req,
|
||||
mode: mode,
|
||||
certDER: certDER,
|
||||
cert: cert,
|
||||
}
|
||||
|
||||
testCase.subTest(t, &i)
|
||||
})
|
||||
testCase.subTest(t, &i)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func issueCertificateSubTestSetup(t *testing.T) (*CertificateAuthorityImpl, *mockSA) {
|
||||
func issueCertificateSubTestSetup(t *testing.T, boulderSigner bool) (*CertificateAuthorityImpl, *mockSA) {
|
||||
testCtx := setup(t)
|
||||
sa := &mockSA{}
|
||||
var issuers []Issuer
|
||||
var signerConfigs []bsigner.Config
|
||||
if boulderSigner {
|
||||
signerConfigs = testCtx.signerConfigs
|
||||
_ = features.Set(map[string]bool{"NonCFSSLSigner": true})
|
||||
} else {
|
||||
issuers = testCtx.issuers
|
||||
}
|
||||
ca, err := NewCertificateAuthorityImpl(
|
||||
testCtx.caConfig,
|
||||
sa,
|
||||
testCtx.pa,
|
||||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
testCtx.issuers,
|
||||
issuers,
|
||||
signerConfigs,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
nil)
|
||||
|
@ -435,6 +474,7 @@ func TestMultipleIssuers(t *testing.T) {
|
|||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
newIssuers,
|
||||
nil,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
nil)
|
||||
|
@ -460,6 +500,7 @@ func TestOCSP(t *testing.T) {
|
|||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
testCtx.issuers,
|
||||
nil,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
nil)
|
||||
|
@ -511,6 +552,7 @@ func TestOCSP(t *testing.T) {
|
|||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
newIssuers,
|
||||
nil,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
nil)
|
||||
|
@ -608,6 +650,7 @@ func TestInvalidCSRs(t *testing.T) {
|
|||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
testCtx.issuers,
|
||||
nil,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
nil)
|
||||
|
@ -639,6 +682,7 @@ func TestRejectValidityTooLong(t *testing.T) {
|
|||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
testCtx.issuers,
|
||||
nil,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
nil)
|
||||
|
@ -694,6 +738,7 @@ func TestSingleAIAEnforcement(t *testing.T) {
|
|||
clock.New(),
|
||||
metrics.NoopRegisterer,
|
||||
nil,
|
||||
nil,
|
||||
goodkey.KeyPolicy{},
|
||||
&blog.Mock{},
|
||||
nil,
|
||||
|
@ -793,63 +838,67 @@ func makeSCTs() ([][]byte, error) {
|
|||
func TestIssueCertificateForPrecertificate(t *testing.T) {
|
||||
testCtx := setup(t)
|
||||
sa := &mockSA{}
|
||||
ca, err := NewCertificateAuthorityImpl(
|
||||
testCtx.caConfig,
|
||||
sa,
|
||||
testCtx.pa,
|
||||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
testCtx.issuers,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
nil)
|
||||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
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.signerConfigs,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
nil)
|
||||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
|
||||
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, OrderID: 0}
|
||||
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
|
||||
test.AssertNotError(t, err, "Failed to issue precert")
|
||||
parsedPrecert, err := x509.ParseCertificate(precert.DER)
|
||||
test.AssertNotError(t, err, "Failed to parse precert")
|
||||
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, OrderID: 0}
|
||||
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
|
||||
test.AssertNotError(t, err, "Failed to issue precert")
|
||||
parsedPrecert, err := x509.ParseCertificate(precert.DER)
|
||||
test.AssertNotError(t, err, "Failed to parse precert")
|
||||
|
||||
// Check for poison extension
|
||||
poisoned := false
|
||||
for _, ext := range parsedPrecert.Extensions {
|
||||
if ext.Id.Equal(signer.CTPoisonOID) && ext.Critical {
|
||||
poisoned = true
|
||||
// Check for poison extension
|
||||
poisoned := false
|
||||
for _, ext := range parsedPrecert.Extensions {
|
||||
if ext.Id.Equal(signer.CTPoisonOID) && ext.Critical {
|
||||
poisoned = true
|
||||
}
|
||||
}
|
||||
}
|
||||
test.Assert(t, poisoned, "returned precert not poisoned")
|
||||
test.Assert(t, poisoned, "returned precert not poisoned")
|
||||
|
||||
sctBytes, err := makeSCTs()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
test.AssertNotError(t, err, "Failed to marshal SCT")
|
||||
cert, err := ca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
|
||||
DER: precert.DER,
|
||||
SCTs: sctBytes,
|
||||
RegistrationID: arbitraryRegID,
|
||||
OrderID: 0,
|
||||
})
|
||||
test.AssertNotError(t, err, "Failed to issue cert from precert")
|
||||
parsedCert, err := x509.ParseCertificate(cert.Der)
|
||||
test.AssertNotError(t, err, "Failed to parse cert")
|
||||
|
||||
// Check for SCT list extension
|
||||
list := false
|
||||
for _, ext := range parsedCert.Extensions {
|
||||
if ext.Id.Equal(signer.SCTListOID) && !ext.Critical {
|
||||
list = true
|
||||
var rawValue []byte
|
||||
_, err = asn1.Unmarshal(ext.Value, &rawValue)
|
||||
test.AssertNotError(t, err, "Failed to unmarshal extension value")
|
||||
sctList, err := helpers.DeserializeSCTList(rawValue)
|
||||
test.AssertNotError(t, err, "Failed to deserialize SCT list")
|
||||
test.Assert(t, len(sctList) == 1, fmt.Sprintf("Wrong number of SCTs, wanted: 1, got: %d", len(sctList)))
|
||||
sctBytes, err := makeSCTs()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
test.AssertNotError(t, err, "Failed to marshal SCT")
|
||||
cert, err := ca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
|
||||
DER: precert.DER,
|
||||
SCTs: sctBytes,
|
||||
RegistrationID: arbitraryRegID,
|
||||
OrderID: 0,
|
||||
})
|
||||
test.AssertNotError(t, err, "Failed to issue cert from precert")
|
||||
parsedCert, err := x509.ParseCertificate(cert.Der)
|
||||
test.AssertNotError(t, err, "Failed to parse cert")
|
||||
|
||||
// Check for SCT list extension
|
||||
list := false
|
||||
for _, ext := range parsedCert.Extensions {
|
||||
if ext.Id.Equal(signer.SCTListOID) && !ext.Critical {
|
||||
list = true
|
||||
var rawValue []byte
|
||||
_, err = asn1.Unmarshal(ext.Value, &rawValue)
|
||||
test.AssertNotError(t, err, "Failed to unmarshal extension value")
|
||||
sctList, err := helpers.DeserializeSCTList(rawValue)
|
||||
test.AssertNotError(t, err, "Failed to deserialize SCT list")
|
||||
test.Assert(t, len(sctList) == 1, fmt.Sprintf("Wrong number of SCTs, wanted: 1, got: %d", len(sctList)))
|
||||
}
|
||||
}
|
||||
test.Assert(t, list, "returned cert doesn't contain SCT list")
|
||||
}
|
||||
test.Assert(t, list, "returned cert doesn't contain SCT list")
|
||||
}
|
||||
|
||||
// dupeSA returns a non-error to GetCertificate in order to simulate a request
|
||||
|
@ -881,6 +930,7 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
|
|||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
testCtx.issuers,
|
||||
nil,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
nil)
|
||||
|
@ -917,6 +967,7 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
|
|||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
testCtx.issuers,
|
||||
nil,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
nil)
|
||||
|
@ -990,6 +1041,7 @@ func TestPrecertOrphanQueue(t *testing.T) {
|
|||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
testCtx.issuers,
|
||||
nil,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
orphanQueue)
|
||||
|
@ -1052,6 +1104,7 @@ func TestOrphanQueue(t *testing.T) {
|
|||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
testCtx.issuers,
|
||||
nil,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
orphanQueue)
|
||||
|
@ -1162,14 +1215,15 @@ func TestIssuePrecertificateLinting(t *testing.T) {
|
|||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
testCtx.issuers,
|
||||
nil,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
nil)
|
||||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
|
||||
// Reconfigure the CA's eeSigner to be a linttrapSigner that always returns
|
||||
// Reconfigure the CA's cfsslSigner to be a linttrapSigner that always returns
|
||||
// two LintResults.
|
||||
ca.defaultIssuer.eeSigner = &linttrapSigner{
|
||||
ca.defaultIssuer.cfsslSigner = &linttrapSigner{
|
||||
lintErr: &local.LintError{
|
||||
ErrorResults: map[string]lint.LintResult{
|
||||
"foobar": {
|
||||
|
@ -1217,6 +1271,7 @@ func TestGenerateOCSPWithIssuerID(t *testing.T) {
|
|||
testCtx.fc,
|
||||
testCtx.stats,
|
||||
testCtx.issuers,
|
||||
nil,
|
||||
testCtx.keyPolicy,
|
||||
testCtx.logger,
|
||||
nil)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"github.com/letsencrypt/pkcs11key/v4"
|
||||
|
||||
"github.com/letsencrypt/boulder/cmd"
|
||||
"github.com/letsencrypt/boulder/signer"
|
||||
)
|
||||
|
||||
// CAConfig structs have configuration information for the certificate
|
||||
|
@ -24,6 +25,9 @@ type CAConfig struct {
|
|||
// 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 signer.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
|
||||
|
@ -34,8 +38,9 @@ type CAConfig struct {
|
|||
// field in cfssl config.
|
||||
Backdate cmd.ConfigDuration
|
||||
// The maximum number of subjectAltNames in a single certificate
|
||||
MaxNames int
|
||||
CFSSL cfsslConfig.Config
|
||||
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.
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
bgrpc "github.com/letsencrypt/boulder/grpc"
|
||||
"github.com/letsencrypt/boulder/policy"
|
||||
sapb "github.com/letsencrypt/boulder/sa/proto"
|
||||
bsigner "github.com/letsencrypt/boulder/signer"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
|
@ -34,7 +35,7 @@ type config struct {
|
|||
Syslog cmd.SyslogConfig
|
||||
}
|
||||
|
||||
func loadIssuers(c config) ([]ca.Issuer, error) {
|
||||
func loadCFSSLIssuers(c config) ([]ca.Issuer, error) {
|
||||
var issuers []ca.Issuer
|
||||
for _, issuerConfig := range c.CA.Issuers {
|
||||
priv, cert, err := loadIssuer(issuerConfig)
|
||||
|
@ -47,6 +48,24 @@ func loadIssuers(c config) ([]ca.Issuer, error) {
|
|||
return issuers, nil
|
||||
}
|
||||
|
||||
func loadBoulderIssuers(configs []ca_config.IssuerConfig, profile bsigner.ProfileConfig, ignoredLints []string) ([]bsigner.Config, error) {
|
||||
boulderIssuerConfigs := make([]bsigner.Config, 0, len(configs))
|
||||
for _, issuerConfig := range configs {
|
||||
signer, issuer, err := loadIssuer(issuerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
boulderIssuerConfigs = append(boulderIssuerConfigs, bsigner.Config{
|
||||
Issuer: issuer,
|
||||
Signer: signer,
|
||||
IgnoredLints: ignoredLints,
|
||||
Clk: cmd.Clock(),
|
||||
Profile: profile,
|
||||
})
|
||||
}
|
||||
return boulderIssuerConfigs, nil
|
||||
}
|
||||
|
||||
func loadIssuer(issuerConfig ca_config.IssuerConfig) (crypto.Signer, *x509.Certificate, error) {
|
||||
cert, err := core.LoadCert(issuerConfig.CertFile)
|
||||
if err != nil {
|
||||
|
@ -152,8 +171,15 @@ func main() {
|
|||
err = pa.SetHostnamePolicyFile(c.CA.HostnamePolicyFile)
|
||||
cmd.FailOnError(err, "Couldn't load hostname policy file")
|
||||
|
||||
issuers, err := loadIssuers(c)
|
||||
cmd.FailOnError(err, "Couldn't load issuers")
|
||||
var cfsslIssuers []ca.Issuer
|
||||
var boulderIssuerConfigs []bsigner.Config
|
||||
if features.Enabled(features.NonCFSSLSigner) {
|
||||
boulderIssuerConfigs, err = loadBoulderIssuers(c.CA.Issuers, c.CA.SignerProfile, c.CA.IgnoredLints)
|
||||
cmd.FailOnError(err, "Couldn't load issuers")
|
||||
} else {
|
||||
cfsslIssuers, err = loadCFSSLIssuers(c)
|
||||
cmd.FailOnError(err, "Couldn't load issuers")
|
||||
}
|
||||
|
||||
tlsConfig, err := c.CA.TLS.Load()
|
||||
cmd.FailOnError(err, "TLS config")
|
||||
|
@ -181,7 +207,8 @@ func main() {
|
|||
pa,
|
||||
clk,
|
||||
scope,
|
||||
issuers,
|
||||
cfsslIssuers,
|
||||
boulderIssuerConfigs,
|
||||
kp,
|
||||
logger,
|
||||
orphanQueue)
|
||||
|
|
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/letsencrypt/boulder/ca/config"
|
||||
ca_config "github.com/letsencrypt/boulder/ca/config"
|
||||
)
|
||||
|
||||
func TestLoadIssuerSuccess(t *testing.T) {
|
||||
|
|
|
@ -12,6 +12,8 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/letsencrypt/boulder/policyasn1"
|
||||
)
|
||||
|
||||
type policyInfoConfig struct {
|
||||
|
@ -54,7 +56,7 @@ type certProfile struct {
|
|||
|
||||
// PolicyOIDs should contain any OIDs to be inserted in a certificate
|
||||
// policies extension. If the CPSURI field of a policyInfoConfig element
|
||||
// is set it will result in a policyInformation structure containing a
|
||||
// is set it will result in a PolicyInformation structure containing a
|
||||
// single id-qt-cps type qualifier indicating the CPS URI.
|
||||
Policies []policyInfoConfig `yaml:"policies"`
|
||||
|
||||
|
@ -142,34 +144,23 @@ var stringToKeyUsage = map[string]x509.KeyUsage{
|
|||
"Cert Sign": x509.KeyUsageCertSign,
|
||||
}
|
||||
|
||||
type policyQualifier struct {
|
||||
Id asn1.ObjectIdentifier
|
||||
Value string `asn1:"tag:optional,ia5"`
|
||||
}
|
||||
|
||||
type policyInformation struct {
|
||||
Policy asn1.ObjectIdentifier
|
||||
Qualifiers []policyQualifier `asn1:"tag:optional,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
oidExtensionCertificatePolicies = asn1.ObjectIdentifier{2, 5, 29, 32}
|
||||
oidCPSQualifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 2, 1}
|
||||
|
||||
oidOCSPNoCheck = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 48, 1, 5}
|
||||
)
|
||||
|
||||
func buildPolicies(policies []policyInfoConfig) (pkix.Extension, error) {
|
||||
policyExt := pkix.Extension{Id: oidExtensionCertificatePolicies}
|
||||
var policyInfo []policyInformation
|
||||
var policyInfo []policyasn1.PolicyInformation
|
||||
for _, p := range policies {
|
||||
oid, err := parseOID(p.OID)
|
||||
if err != nil {
|
||||
return pkix.Extension{}, err
|
||||
}
|
||||
pi := policyInformation{Policy: oid}
|
||||
pi := policyasn1.PolicyInformation{Policy: oid}
|
||||
if p.CPSURI != "" {
|
||||
pi.Qualifiers = []policyQualifier{{Id: oidCPSQualifier, Value: p.CPSURI}}
|
||||
pi.Qualifiers = []policyasn1.PolicyQualifier{{OID: policyasn1.CPSQualifierOID, Value: p.CPSURI}}
|
||||
}
|
||||
policyInfo = append(policyInfo, pi)
|
||||
}
|
||||
|
|
|
@ -30,11 +30,12 @@ func _() {
|
|||
_ = x[StoreRevokerInfo-19]
|
||||
_ = x[RestrictRSAKeySizes-20]
|
||||
_ = x[FasterNewOrdersRateLimit-21]
|
||||
_ = x[NonCFSSLSigner-22]
|
||||
}
|
||||
|
||||
const _FeatureFlag_name = "unusedWriteIssuedNamesPrecertHeadNonceStatusOKRemoveWFE2AccountIDCheckRenewalFirstParallelCheckFailedValidationDeleteUnusedChallengesBlockedKeyTableStoreKeyHashesCAAValidationMethodsCAAAccountURIEnforceMultiVAMultiVAFullResultsMandatoryPOSTAsGETAllowV1RegistrationV1DisableNewValidationsPrecertificateRevocationStripDefaultSchemePortStoreIssuerInfoStoreRevokerInfoRestrictRSAKeySizesFasterNewOrdersRateLimit"
|
||||
const _FeatureFlag_name = "unusedWriteIssuedNamesPrecertHeadNonceStatusOKRemoveWFE2AccountIDCheckRenewalFirstParallelCheckFailedValidationDeleteUnusedChallengesBlockedKeyTableStoreKeyHashesCAAValidationMethodsCAAAccountURIEnforceMultiVAMultiVAFullResultsMandatoryPOSTAsGETAllowV1RegistrationV1DisableNewValidationsPrecertificateRevocationStripDefaultSchemePortStoreIssuerInfoStoreRevokerInfoRestrictRSAKeySizesFasterNewOrdersRateLimitNonCFSSLSigner"
|
||||
|
||||
var _FeatureFlag_index = [...]uint16{0, 6, 29, 46, 65, 82, 111, 133, 148, 162, 182, 195, 209, 227, 245, 264, 287, 311, 333, 348, 364, 383, 407}
|
||||
var _FeatureFlag_index = [...]uint16{0, 6, 29, 46, 65, 82, 111, 133, 148, 162, 182, 195, 209, 227, 245, 264, 287, 311, 333, 348, 364, 383, 407, 421}
|
||||
|
||||
func (i FeatureFlag) String() string {
|
||||
if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) {
|
||||
|
|
|
@ -58,6 +58,9 @@ const (
|
|||
// FasterNewOrdersRateLimit enables use of a separate table for counting the
|
||||
// new orders rate limit.
|
||||
FasterNewOrdersRateLimit
|
||||
// NonCFSSLSigner enables usage of our own certificate signer instead of the
|
||||
// CFSSL signer.
|
||||
NonCFSSLSigner
|
||||
)
|
||||
|
||||
// List of features and their default value, protected by fMu
|
||||
|
@ -84,6 +87,7 @@ var features = map[FeatureFlag]bool{
|
|||
RestrictRSAKeySizes: false,
|
||||
FasterNewOrdersRateLimit: false,
|
||||
BlockedKeyTable: false,
|
||||
NonCFSSLSigner: false,
|
||||
}
|
||||
|
||||
var fMu = new(sync.RWMutex)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
// policyasn1 contains structures required to encode the RFC 5280
|
||||
// PolicyInformation ASN.1 structures.
|
||||
package policyasn1
|
||||
|
||||
import "encoding/asn1"
|
||||
|
||||
// CPSQualifierOID contains the id-qt-cps OID that is used to indicate the
|
||||
// CPS policy qualifier type
|
||||
var CPSQualifierOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 2, 1}
|
||||
|
||||
// PolicyQualifier represents the PolicyQualifierInfo ASN.1 structure
|
||||
type PolicyQualifier struct {
|
||||
OID asn1.ObjectIdentifier
|
||||
Value string `asn1:"optional,ia5"`
|
||||
}
|
||||
|
||||
// PolicyInformation represents the PolicyInformation ASN.1 structure
|
||||
type PolicyInformation struct {
|
||||
Policy asn1.ObjectIdentifier
|
||||
Qualifiers []PolicyQualifier `asn1:"optional"`
|
||||
}
|
|
@ -0,0 +1,491 @@
|
|||
package signer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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/policyasn1"
|
||||
zlintx509 "github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v2"
|
||||
"github.com/zmap/zlint/v2/lint"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
type signingProfile struct {
|
||||
allowRSAKeys bool
|
||||
allowECDSAKeys bool
|
||||
|
||||
allowMustStaple bool
|
||||
allowCTPoison bool
|
||||
allowSCTList bool
|
||||
allowCommonName bool
|
||||
|
||||
sigAlg x509.SignatureAlgorithm
|
||||
ocspURL string
|
||||
crlURL string
|
||||
issuerURL string
|
||||
policies *pkix.Extension
|
||||
|
||||
maxBackdate time.Duration
|
||||
maxValidity time.Duration
|
||||
}
|
||||
|
||||
// PolicyQualifier describes a policy qualifier
|
||||
type PolicyQualifier struct {
|
||||
Type string
|
||||
Value string
|
||||
}
|
||||
|
||||
// PolicyInformation describes a policy
|
||||
type PolicyInformation struct {
|
||||
OID string
|
||||
Qualifiers []PolicyQualifier
|
||||
}
|
||||
|
||||
// ProfileConfig describes the certificate issuance constraints
|
||||
type ProfileConfig struct {
|
||||
AllowRSAKeys bool
|
||||
AllowECDSAKeys bool
|
||||
AllowMustStaple bool
|
||||
AllowCTPoison bool
|
||||
AllowSCTList bool
|
||||
AllowCommonName bool
|
||||
|
||||
IssuerURL string
|
||||
OCSPURL string
|
||||
CRLURL string
|
||||
Policies []PolicyInformation
|
||||
MaxValidityPeriod cmd.ConfigDuration
|
||||
MaxValidityBackdate cmd.ConfigDuration
|
||||
}
|
||||
|
||||
func parseOID(oidStr string) (asn1.ObjectIdentifier, error) {
|
||||
var oid asn1.ObjectIdentifier
|
||||
for _, a := range strings.Split(oidStr, ".") {
|
||||
i, err := strconv.Atoi(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if i <= 0 {
|
||||
return nil, errors.New("OID components must be >= 1")
|
||||
}
|
||||
oid = append(oid, i)
|
||||
}
|
||||
return oid, nil
|
||||
}
|
||||
|
||||
var stringToQualifierType = map[string]asn1.ObjectIdentifier{
|
||||
"id-qt-cps": policyasn1.CPSQualifierOID,
|
||||
}
|
||||
|
||||
func newProfile(config ProfileConfig) (*signingProfile, error) {
|
||||
sp := &signingProfile{
|
||||
allowRSAKeys: config.AllowRSAKeys,
|
||||
allowECDSAKeys: config.AllowECDSAKeys,
|
||||
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,
|
||||
}
|
||||
if config.IssuerURL == "" {
|
||||
return nil, errors.New("Issuer URL is required")
|
||||
}
|
||||
if config.OCSPURL == "" {
|
||||
return nil, errors.New("OCSP URL is required")
|
||||
}
|
||||
if len(config.Policies) > 0 {
|
||||
var policies []policyasn1.PolicyInformation
|
||||
for _, policyConfig := range config.Policies {
|
||||
id, err := parseOID(policyConfig.OID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed parsing policy OID %q: %s", policyConfig.OID, err)
|
||||
}
|
||||
pi := policyasn1.PolicyInformation{Policy: id}
|
||||
for _, qualifierConfig := range policyConfig.Qualifiers {
|
||||
qt, ok := stringToQualifierType[qualifierConfig.Type]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown qualifier type: %s", qualifierConfig.Type)
|
||||
}
|
||||
pq := policyasn1.PolicyQualifier{
|
||||
OID: qt,
|
||||
Value: qualifierConfig.Value,
|
||||
}
|
||||
pi.Qualifiers = append(pi.Qualifiers, pq)
|
||||
}
|
||||
policies = append(policies, pi)
|
||||
}
|
||||
policyExtBytes, err := asn1.Marshal(policies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sp.policies = &pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{2, 5, 29, 32},
|
||||
Value: policyExtBytes,
|
||||
}
|
||||
}
|
||||
return sp, nil
|
||||
}
|
||||
|
||||
// requestValid verifies the passed IssuanceRequest against the signingProfile. If the
|
||||
// request doesn't match the signing profile an error is returned.
|
||||
func (p *signingProfile) requestValid(clk clock.Clock, req *IssuanceRequest) error {
|
||||
switch req.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
if !p.allowRSAKeys {
|
||||
return errors.New("RSA keys not allowed")
|
||||
}
|
||||
case *ecdsa.PublicKey:
|
||||
if !p.allowECDSAKeys {
|
||||
return errors.New("ECDSA keys not allowed")
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported public key type")
|
||||
}
|
||||
|
||||
if !p.allowMustStaple && req.IncludeMustStaple {
|
||||
return errors.New("must-staple extension cannot be included")
|
||||
}
|
||||
|
||||
if !p.allowCTPoison && req.IncludeCTPoison {
|
||||
return errors.New("ct poison extension cannot be included")
|
||||
}
|
||||
|
||||
if !p.allowSCTList && req.SCTList != nil {
|
||||
return errors.New("sct list extension cannot be included")
|
||||
}
|
||||
|
||||
if req.IncludeCTPoison && req.SCTList != nil {
|
||||
return errors.New("cannot include both ct poison and sct list extensions")
|
||||
}
|
||||
|
||||
if !p.allowCommonName && req.CommonName != "" {
|
||||
return errors.New("common name cannot be included")
|
||||
}
|
||||
|
||||
validity := req.NotAfter.Sub(req.NotBefore)
|
||||
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)
|
||||
}
|
||||
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 < 0 {
|
||||
return errors.New("NotBefore is in the future")
|
||||
}
|
||||
|
||||
if len(req.Serial) > 20 || len(req.Serial) < 8 {
|
||||
return errors.New("serial must be between 8 and 20 bytes")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var defaultEKU = []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageServerAuth,
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
}
|
||||
|
||||
func (p *signingProfile) generateTemplate(clk clock.Clock) *x509.Certificate {
|
||||
template := &x509.Certificate{
|
||||
SignatureAlgorithm: p.sigAlg,
|
||||
ExtKeyUsage: defaultEKU,
|
||||
OCSPServer: []string{p.ocspURL},
|
||||
IssuingCertificateURL: []string{p.issuerURL},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
if p.crlURL != "" {
|
||||
template.CRLDistributionPoints = []string{p.crlURL}
|
||||
}
|
||||
|
||||
if p.policies != nil {
|
||||
template.ExtraExtensions = []pkix.Extension{*p.policies}
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
// Signer is a certificate signer
|
||||
type Signer struct {
|
||||
issuer *x509.Certificate
|
||||
signer crypto.Signer
|
||||
profile *signingProfile
|
||||
clk clock.Clock
|
||||
lintKey crypto.Signer
|
||||
lints lint.Registry
|
||||
}
|
||||
|
||||
// Config contains the information necessary to construct a Signer
|
||||
type Config struct {
|
||||
Issuer *x509.Certificate
|
||||
Signer crypto.Signer
|
||||
IgnoredLints []string
|
||||
Clk clock.Clock
|
||||
Profile ProfileConfig
|
||||
}
|
||||
|
||||
// NewSigner constructs a Signer from the provided Config
|
||||
func NewSigner(config Config) (*Signer, error) {
|
||||
profile, err := newProfile(config.Profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lints, err := lint.GlobalRegistry().Filter(lint.FilterOptions{
|
||||
ExcludeNames: config.IgnoredLints,
|
||||
ExcludeSources: []lint.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.
|
||||
lint.CABFEVGuidelines,
|
||||
lint.EtsiEsi,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var lk crypto.Signer
|
||||
switch k := config.Issuer.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
lk, err = rsa.GenerateKey(rand.Reader, k.Size()*8)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile.sigAlg = x509.SHA256WithRSA
|
||||
case *ecdsa.PublicKey:
|
||||
lk, err = ecdsa.GenerateKey(k.Curve, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch k.Curve {
|
||||
case elliptic.P256():
|
||||
profile.sigAlg = x509.ECDSAWithSHA256
|
||||
case elliptic.P384():
|
||||
profile.sigAlg = x509.ECDSAWithSHA384
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported ECDSA curve: %s", k.Curve.Params().Name)
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported issuer key type")
|
||||
}
|
||||
s := &Signer{
|
||||
issuer: config.Issuer,
|
||||
signer: config.Signer,
|
||||
clk: config.Clk,
|
||||
lints: lints,
|
||||
lintKey: lk,
|
||||
profile: profile,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
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},
|
||||
Value: asn1.NullBytes,
|
||||
Critical: true,
|
||||
}
|
||||
|
||||
// OID for SCT list, RFC 6962 (was never assigned a proper id-pe- name)
|
||||
var sctListOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}
|
||||
|
||||
func generateSCTListExt(scts []ct.SignedCertificateTimestamp) (pkix.Extension, error) {
|
||||
list := ctx509.SignedCertificateTimestampList{}
|
||||
for _, sct := range scts {
|
||||
sctBytes, err := cttls.Marshal(sct)
|
||||
if err != nil {
|
||||
return pkix.Extension{}, err
|
||||
}
|
||||
list.SCTList = append(list.SCTList, ctx509.SerializedSCT{Val: sctBytes})
|
||||
}
|
||||
listBytes, err := cttls.Marshal(list)
|
||||
if err != nil {
|
||||
return pkix.Extension{}, err
|
||||
}
|
||||
extBytes, err := asn1.Marshal(listBytes)
|
||||
if err != nil {
|
||||
return pkix.Extension{}, err
|
||||
}
|
||||
return pkix.Extension{
|
||||
Id: sctListOID,
|
||||
Value: extBytes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var mustStapleExt = pkix.Extension{
|
||||
// RFC 7633: id-pe-tlsfeature OBJECT IDENTIFIER ::= { id-pe 24 }
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24},
|
||||
// ASN.1 encoding of:
|
||||
// SEQUENCE
|
||||
// INTEGER 5
|
||||
// where "5" is the status_request feature (RFC 6066)
|
||||
Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05},
|
||||
}
|
||||
|
||||
func generateSKID(pk crypto.PublicKey) ([]byte, error) {
|
||||
pkBytes, err := x509.MarshalPKIXPublicKey(pk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pkixPublicKey struct {
|
||||
Algo pkix.AlgorithmIdentifier
|
||||
BitString asn1.BitString
|
||||
}
|
||||
if _, err := asn1.Unmarshal(pkBytes, &pkixPublicKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
skid := sha1.Sum(pkixPublicKey.BitString.Bytes)
|
||||
return skid[:], nil
|
||||
}
|
||||
|
||||
// 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
|
||||
// zlint. If the linting fails, an error is returned and the certificate
|
||||
// is not signed using the issuer's key.
|
||||
func (s *Signer) Issue(req *IssuanceRequest) ([]byte, error) {
|
||||
// check request is valid according to the issuance profile
|
||||
if err := s.profile.requestValid(s.clk, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// generate template from the issuance profile
|
||||
template := s.profile.generateTemplate(s.clk)
|
||||
|
||||
// populate template from the issuance request
|
||||
template.NotBefore, template.NotAfter = req.NotBefore, req.NotAfter
|
||||
template.SerialNumber = big.NewInt(0).SetBytes(req.Serial)
|
||||
if req.CommonName != "" {
|
||||
template.Subject.CommonName = req.CommonName
|
||||
}
|
||||
template.DNSNames = req.DNSNames
|
||||
template.AuthorityKeyId = s.issuer.SubjectKeyId
|
||||
skid, err := generateSKID(req.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
template.SubjectKeyId = skid
|
||||
switch req.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
|
||||
case *ecdsa.PublicKey:
|
||||
template.KeyUsage = x509.KeyUsageDigitalSignature
|
||||
}
|
||||
|
||||
if req.IncludeCTPoison {
|
||||
template.ExtraExtensions = append(template.ExtraExtensions, ctPoisonExt)
|
||||
} else if req.SCTList != nil {
|
||||
sctListExt, err := generateSCTListExt(req.SCTList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
template.ExtraExtensions = append(template.ExtraExtensions, sctListExt)
|
||||
}
|
||||
|
||||
if req.IncludeMustStaple {
|
||||
template.ExtraExtensions = append(template.ExtraExtensions, mustStapleExt)
|
||||
}
|
||||
|
||||
// check that the tbsCertificate is properly formed by signing it
|
||||
// with a throwaway key and then linting it using zlint
|
||||
lintCertBytes, err := x509.CreateCertificate(rand.Reader, template, s.issuer, req.PublicKey, s.lintKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lintCert, err := zlintx509.ParseCertificate(lintCertBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results := zlint.LintCertificateEx(lintCert, s.lints)
|
||||
if results.NoticesPresent || results.WarningsPresent || results.ErrorsPresent || results.FatalsPresent {
|
||||
var badLints []string
|
||||
for lintName, result := range results.Results {
|
||||
if result.Status > lint.Pass {
|
||||
badLints = append(badLints, lintName)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("tbsCertificate linting failed: %s", strings.Join(badLints, ", "))
|
||||
}
|
||||
|
||||
return x509.CreateCertificate(rand.Reader, template, s.issuer, req.PublicKey, s.signer)
|
||||
}
|
||||
|
||||
func ContainsMustStaple(extensions []pkix.Extension) bool {
|
||||
for _, ext := range extensions {
|
||||
if ext.Id.Equal(mustStapleExt.Id) && bytes.Equal(ext.Value, mustStapleExt.Value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsCTPoison(extensions []pkix.Extension) bool {
|
||||
for _, ext := range extensions {
|
||||
if ext.Id.Equal(ctPoisonExt.Id) && bytes.Equal(ext.Value, asn1.NullBytes) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RequestFromPrecert constructs a final certificate IssuanceRequest matching
|
||||
// the provided precertificate. It returns an error if the precertificate doesn't
|
||||
// contain the CT poison extension.
|
||||
func RequestFromPrecert(precert *x509.Certificate, scts []ct.SignedCertificateTimestamp) (*IssuanceRequest, error) {
|
||||
if !containsCTPoison(precert.Extensions) {
|
||||
return nil, errors.New("provided certificate doesn't contain the CT poison extension")
|
||||
}
|
||||
return &IssuanceRequest{
|
||||
PublicKey: precert.PublicKey,
|
||||
Serial: precert.SerialNumber.Bytes(),
|
||||
NotBefore: precert.NotBefore,
|
||||
NotAfter: precert.NotAfter,
|
||||
CommonName: precert.Subject.CommonName,
|
||||
DNSNames: precert.DNSNames,
|
||||
IncludeMustStaple: ContainsMustStaple(precert.Extensions),
|
||||
SCTList: scts,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,655 @@
|
|||
package signer
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"math/big"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ct "github.com/google/certificate-transparency-go"
|
||||
"github.com/jmhodges/clock"
|
||||
"github.com/letsencrypt/boulder/cmd"
|
||||
"github.com/letsencrypt/boulder/policyasn1"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
)
|
||||
|
||||
func defaultProfileConfig() ProfileConfig {
|
||||
return ProfileConfig{
|
||||
AllowECDSAKeys: true,
|
||||
AllowRSAKeys: true,
|
||||
AllowCommonName: true,
|
||||
AllowCTPoison: true,
|
||||
AllowSCTList: true,
|
||||
AllowMustStaple: true,
|
||||
IssuerURL: "http://issuer-url",
|
||||
OCSPURL: "http://ocsp-url",
|
||||
Policies: []PolicyInformation{
|
||||
{OID: "1.2.3"},
|
||||
},
|
||||
MaxValidityPeriod: cmd.ConfigDuration{Duration: time.Hour},
|
||||
MaxValidityBackdate: cmd.ConfigDuration{Duration: time.Hour},
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewProfilePolicies(t *testing.T) {
|
||||
config := defaultProfileConfig()
|
||||
config.Policies = append(config.Policies, PolicyInformation{
|
||||
OID: "1.2.3.4",
|
||||
Qualifiers: []PolicyQualifier{
|
||||
{
|
||||
Type: "id-qt-cps",
|
||||
Value: "cps-url",
|
||||
},
|
||||
},
|
||||
})
|
||||
profile, err := newProfile(config)
|
||||
test.AssertNotError(t, err, "newProfile failed")
|
||||
test.AssertDeepEquals(t, *profile, signingProfile{
|
||||
allowRSAKeys: true,
|
||||
allowECDSAKeys: true,
|
||||
allowMustStaple: true,
|
||||
allowCTPoison: true,
|
||||
allowSCTList: true,
|
||||
allowCommonName: true,
|
||||
issuerURL: "http://issuer-url",
|
||||
ocspURL: "http://ocsp-url",
|
||||
policies: &pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{2, 5, 29, 32},
|
||||
Value: []byte{48, 36, 48, 4, 6, 2, 42, 3, 48, 28, 6, 3, 42, 3, 4, 48, 21, 48, 19, 6, 8, 43, 6, 1, 5, 5, 7, 2, 1, 22, 7, 99, 112, 115, 45, 117, 114, 108},
|
||||
},
|
||||
maxBackdate: time.Hour,
|
||||
maxValidity: time.Hour,
|
||||
})
|
||||
var policies []policyasn1.PolicyInformation
|
||||
_, err = asn1.Unmarshal(profile.policies.Value, &policies)
|
||||
test.AssertNotError(t, err, "failed to parse policies extension")
|
||||
test.AssertEquals(t, len(policies), 2)
|
||||
test.AssertDeepEquals(t, policies[0], policyasn1.PolicyInformation{
|
||||
Policy: asn1.ObjectIdentifier{1, 2, 3},
|
||||
})
|
||||
test.AssertDeepEquals(t, policies[1], policyasn1.PolicyInformation{
|
||||
Policy: asn1.ObjectIdentifier{1, 2, 3, 4},
|
||||
Qualifiers: []policyasn1.PolicyQualifier{{
|
||||
OID: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 2, 1},
|
||||
Value: "cps-url",
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewProfileNoIssuerURL(t *testing.T) {
|
||||
_, err := newProfile(ProfileConfig{})
|
||||
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")
|
||||
test.AssertEquals(t, err.Error(), "OCSP URL is required")
|
||||
}
|
||||
|
||||
func TestNewProfileInvalidOID(t *testing.T) {
|
||||
_, err := newProfile(ProfileConfig{
|
||||
IssuerURL: "issuer-url",
|
||||
OCSPURL: "ocsp-url",
|
||||
Policies: []PolicyInformation{{
|
||||
OID: "a.b.c",
|
||||
}},
|
||||
})
|
||||
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",
|
||||
Policies: []PolicyInformation{{
|
||||
OID: "1.2.3",
|
||||
Qualifiers: []PolicyQualifier{{
|
||||
Type: "asd",
|
||||
Value: "bad",
|
||||
}},
|
||||
}},
|
||||
})
|
||||
test.AssertError(t, err, "newProfile didn't fail with unknown policy qualifier type")
|
||||
test.AssertEquals(t, err.Error(), "unknown qualifier type: asd")
|
||||
}
|
||||
|
||||
func TestRequestValid(t *testing.T) {
|
||||
fc := clock.NewFake()
|
||||
fc.Add(time.Hour * 24)
|
||||
tests := []struct {
|
||||
name string
|
||||
profile *signingProfile
|
||||
request *IssuanceRequest
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "unsupported key type",
|
||||
profile: &signingProfile{},
|
||||
request: &IssuanceRequest{PublicKey: &dsa.PublicKey{}},
|
||||
expectedError: "unsupported public key type",
|
||||
},
|
||||
{
|
||||
name: "rsa keys not allowed",
|
||||
profile: &signingProfile{},
|
||||
request: &IssuanceRequest{PublicKey: &rsa.PublicKey{}},
|
||||
expectedError: "RSA keys not allowed",
|
||||
},
|
||||
{
|
||||
name: "ecdsa keys not allowed",
|
||||
profile: &signingProfile{},
|
||||
request: &IssuanceRequest{PublicKey: &ecdsa.PublicKey{}},
|
||||
expectedError: "ECDSA keys not allowed",
|
||||
},
|
||||
{
|
||||
name: "must staple not allowed",
|
||||
profile: &signingProfile{
|
||||
allowECDSAKeys: true,
|
||||
},
|
||||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
IncludeMustStaple: true,
|
||||
},
|
||||
expectedError: "must-staple extension cannot be included",
|
||||
},
|
||||
{
|
||||
name: "ct poison not allowed",
|
||||
profile: &signingProfile{
|
||||
allowECDSAKeys: true,
|
||||
},
|
||||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
IncludeCTPoison: true,
|
||||
},
|
||||
expectedError: "ct poison extension cannot be included",
|
||||
},
|
||||
{
|
||||
name: "sct list not allowed",
|
||||
profile: &signingProfile{
|
||||
allowECDSAKeys: true,
|
||||
},
|
||||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
SCTList: []ct.SignedCertificateTimestamp{},
|
||||
},
|
||||
expectedError: "sct list extension cannot be included",
|
||||
},
|
||||
{
|
||||
name: "sct list and ct poison not allowed",
|
||||
profile: &signingProfile{
|
||||
allowECDSAKeys: true,
|
||||
allowCTPoison: true,
|
||||
allowSCTList: true,
|
||||
},
|
||||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
IncludeCTPoison: true,
|
||||
SCTList: []ct.SignedCertificateTimestamp{},
|
||||
},
|
||||
expectedError: "cannot include both ct poison and sct list extensions",
|
||||
},
|
||||
{
|
||||
name: "common name not allowed",
|
||||
profile: &signingProfile{
|
||||
allowECDSAKeys: true,
|
||||
},
|
||||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
CommonName: "cn",
|
||||
},
|
||||
expectedError: "common name cannot be included",
|
||||
},
|
||||
{
|
||||
name: "negative validity",
|
||||
profile: &signingProfile{
|
||||
allowECDSAKeys: true,
|
||||
},
|
||||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
NotBefore: fc.Now().Add(time.Hour),
|
||||
NotAfter: fc.Now(),
|
||||
},
|
||||
expectedError: "NotAfter must be after NotBefore",
|
||||
},
|
||||
{
|
||||
name: "validity larger than max",
|
||||
profile: &signingProfile{
|
||||
allowECDSAKeys: true,
|
||||
maxValidity: time.Minute,
|
||||
},
|
||||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour),
|
||||
},
|
||||
expectedError: "validity period is more than the maximum allowed period (1h0m0s>1m0s)",
|
||||
},
|
||||
{
|
||||
name: "validity backdated more than max",
|
||||
profile: &signingProfile{
|
||||
allowECDSAKeys: true,
|
||||
maxValidity: time.Hour * 2,
|
||||
maxBackdate: time.Hour,
|
||||
},
|
||||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
NotBefore: fc.Now().Add(-time.Hour * 2),
|
||||
NotAfter: fc.Now().Add(-time.Hour),
|
||||
},
|
||||
expectedError: "NotBefore is backdated more than the maximum allowed period (2h0m0s>1h0m0s)",
|
||||
},
|
||||
{
|
||||
name: "validity is forward dated",
|
||||
profile: &signingProfile{
|
||||
allowECDSAKeys: true,
|
||||
maxValidity: time.Hour * 2,
|
||||
maxBackdate: time.Hour,
|
||||
},
|
||||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
NotBefore: fc.Now().Add(time.Hour),
|
||||
NotAfter: fc.Now().Add(time.Hour * 2),
|
||||
},
|
||||
expectedError: "NotBefore is in the future",
|
||||
},
|
||||
{
|
||||
name: "serial too short",
|
||||
profile: &signingProfile{
|
||||
allowECDSAKeys: true,
|
||||
maxValidity: time.Hour * 2,
|
||||
},
|
||||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour),
|
||||
},
|
||||
expectedError: "serial must be between 8 and 20 bytes",
|
||||
},
|
||||
{
|
||||
name: "serial too long",
|
||||
profile: &signingProfile{
|
||||
allowECDSAKeys: true,
|
||||
maxValidity: time.Hour * 2,
|
||||
},
|
||||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 0},
|
||||
},
|
||||
expectedError: "serial must be between 8 and 20 bytes",
|
||||
},
|
||||
{
|
||||
name: "good",
|
||||
profile: &signingProfile{
|
||||
allowECDSAKeys: true,
|
||||
maxValidity: time.Hour * 2,
|
||||
},
|
||||
request: &IssuanceRequest{
|
||||
PublicKey: &ecdsa.PublicKey{},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.profile.requestValid(fc, tc.request)
|
||||
if err != nil {
|
||||
if tc.expectedError == "" {
|
||||
t.Errorf("failed with unexpected error: %s", err)
|
||||
} else if tc.expectedError != err.Error() {
|
||||
t.Errorf("failed with unexpected error, wanted: %q, got: %q", tc.expectedError, err.Error())
|
||||
}
|
||||
return
|
||||
} else if tc.expectedError != "" {
|
||||
t.Errorf("didn't fail, expected %q", tc.expectedError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateTemplate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
profile *signingProfile
|
||||
expectedTemplate *x509.Certificate
|
||||
}{
|
||||
{
|
||||
name: "crl url",
|
||||
profile: &signingProfile{
|
||||
crlURL: "crl-url",
|
||||
sigAlg: x509.SHA256WithRSA,
|
||||
},
|
||||
expectedTemplate: &x509.Certificate{
|
||||
BasicConstraintsValid: true,
|
||||
SignatureAlgorithm: x509.SHA256WithRSA,
|
||||
ExtKeyUsage: defaultEKU,
|
||||
IssuingCertificateURL: []string{""},
|
||||
OCSPServer: []string{""},
|
||||
CRLDistributionPoints: []string{"crl-url"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "include policies",
|
||||
profile: &signingProfile{
|
||||
sigAlg: x509.SHA256WithRSA,
|
||||
policies: &pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{1, 2, 3},
|
||||
Value: []byte{4, 5, 6},
|
||||
},
|
||||
},
|
||||
expectedTemplate: &x509.Certificate{
|
||||
BasicConstraintsValid: true,
|
||||
SignatureAlgorithm: x509.SHA256WithRSA,
|
||||
ExtKeyUsage: defaultEKU,
|
||||
IssuingCertificateURL: []string{""},
|
||||
OCSPServer: []string{""},
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 2, 3},
|
||||
Value: []byte{4, 5, 6},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
fc := clock.NewFake()
|
||||
fc.Set(time.Time{}.Add(time.Hour))
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
template := tc.profile.generateTemplate(fc)
|
||||
test.AssertDeepEquals(t, *template, *tc.expectedTemplate)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSignerUnsupportedKeyType(t *testing.T) {
|
||||
_, err := NewSigner(Config{
|
||||
Profile: defaultProfileConfig(),
|
||||
Issuer: &x509.Certificate{
|
||||
PublicKey: &dsa.PublicKey{},
|
||||
},
|
||||
})
|
||||
test.AssertError(t, err, "NewSigner 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 := NewSigner(Config{
|
||||
Profile: defaultProfileConfig(),
|
||||
Issuer: &x509.Certificate{
|
||||
PublicKey: &rsa.PublicKey{
|
||||
N: mod,
|
||||
},
|
||||
},
|
||||
})
|
||||
test.AssertNotError(t, err, "NewSigner failed")
|
||||
_, ok = signer.lintKey.(*rsa.PrivateKey)
|
||||
test.Assert(t, ok, "lint key is not RSA")
|
||||
}
|
||||
|
||||
func TestNewSignerECDSAKey(t *testing.T) {
|
||||
signer, err := NewSigner(Config{
|
||||
Profile: defaultProfileConfig(),
|
||||
Issuer: &x509.Certificate{
|
||||
PublicKey: &ecdsa.PublicKey{
|
||||
Curve: elliptic.P256(),
|
||||
},
|
||||
},
|
||||
})
|
||||
test.AssertNotError(t, err, "NewSigner 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
|
||||
generateFunc func() (crypto.Signer, error)
|
||||
ku x509.KeyUsage
|
||||
}{
|
||||
{
|
||||
name: "RSA",
|
||||
generateFunc: func() (crypto.Signer, error) {
|
||||
return rsa.GenerateKey(rand.Reader, 2048)
|
||||
},
|
||||
ku: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
},
|
||||
{
|
||||
name: "ECDSA",
|
||||
generateFunc: func() (crypto.Signer, error) {
|
||||
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
},
|
||||
ku: x509.KeyUsageDigitalSignature,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fc := clock.NewFake()
|
||||
fc.Set(time.Now())
|
||||
signer, err := NewSigner(Config{
|
||||
Issuer: issuerCert,
|
||||
Signer: issuerSigner,
|
||||
Clk: fc,
|
||||
Profile: defaultProfileConfig(),
|
||||
IgnoredLints: []string{"w_ct_sct_policy_count_unsatisfied", "n_subject_common_name_included"},
|
||||
})
|
||||
test.AssertNotError(t, err, "NewSigner failed")
|
||||
pk, err := tc.generateFunc()
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
certBytes, err := signer.Issue(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
CommonName: "example.com",
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour),
|
||||
})
|
||||
test.AssertNotError(t, err, "Issue failed")
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
test.AssertNotError(t, err, "failed to parse certificate")
|
||||
err = cert.CheckSignatureFrom(issuerCert)
|
||||
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})
|
||||
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
|
||||
test.AssertEquals(t, len(cert.Extensions), 8) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies
|
||||
test.AssertEquals(t, cert.KeyUsage, tc.ku)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueRSA(t *testing.T) {
|
||||
fc := clock.NewFake()
|
||||
fc.Set(time.Now())
|
||||
signer, err := NewSigner(Config{
|
||||
Issuer: issuerCert,
|
||||
Signer: issuerSigner,
|
||||
Clk: fc,
|
||||
Profile: defaultProfileConfig(),
|
||||
IgnoredLints: []string{"w_ct_sct_policy_count_unsatisfied"},
|
||||
})
|
||||
test.AssertNotError(t, err, "NewSigner failed")
|
||||
pk, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
certBytes, err := signer.Issue(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour),
|
||||
})
|
||||
test.AssertNotError(t, err, "Issue failed")
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
test.AssertNotError(t, err, "failed to parse certificate")
|
||||
err = cert.CheckSignatureFrom(issuerCert)
|
||||
test.AssertNotError(t, err, "signature validation failed")
|
||||
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8})
|
||||
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
|
||||
test.AssertEquals(t, len(cert.Extensions), 8) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies
|
||||
test.AssertEquals(t, cert.KeyUsage, x509.KeyUsageDigitalSignature|x509.KeyUsageKeyEncipherment)
|
||||
}
|
||||
|
||||
func TestIssueCTPoison(t *testing.T) {
|
||||
fc := clock.NewFake()
|
||||
fc.Set(time.Now())
|
||||
signer, err := NewSigner(Config{
|
||||
Issuer: issuerCert,
|
||||
Signer: issuerSigner,
|
||||
Clk: fc,
|
||||
Profile: defaultProfileConfig(),
|
||||
IgnoredLints: []string{"w_ct_sct_policy_count_unsatisfied"},
|
||||
})
|
||||
test.AssertNotError(t, err, "NewSigner failed")
|
||||
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
certBytes, err := signer.Issue(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
DNSNames: []string{"example.com"},
|
||||
IncludeCTPoison: true,
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour),
|
||||
})
|
||||
test.AssertNotError(t, err, "Issue failed")
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
test.AssertNotError(t, err, "failed to parse certificate")
|
||||
err = cert.CheckSignatureFrom(issuerCert)
|
||||
test.AssertNotError(t, err, "signature validation failed")
|
||||
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8})
|
||||
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
|
||||
test.AssertEquals(t, len(cert.Extensions), 9) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, CT Poison
|
||||
test.AssertDeepEquals(t, cert.Extensions[8], ctPoisonExt)
|
||||
}
|
||||
|
||||
func TestIssueSCTList(t *testing.T) {
|
||||
fc := clock.NewFake()
|
||||
fc.Set(time.Now())
|
||||
signer, err := NewSigner(Config{
|
||||
Issuer: issuerCert,
|
||||
Signer: issuerSigner,
|
||||
Clk: fc,
|
||||
Profile: defaultProfileConfig(),
|
||||
IgnoredLints: []string{"w_ct_sct_policy_count_unsatisfied"},
|
||||
})
|
||||
test.AssertNotError(t, err, "NewSigner failed")
|
||||
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
certBytes, err := signer.Issue(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
DNSNames: []string{"example.com"},
|
||||
SCTList: []ct.SignedCertificateTimestamp{
|
||||
{},
|
||||
},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour),
|
||||
})
|
||||
test.AssertNotError(t, err, "Issue failed")
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
test.AssertNotError(t, err, "failed to parse certificate")
|
||||
err = cert.CheckSignatureFrom(issuerCert)
|
||||
test.AssertNotError(t, err, "signature validation failed")
|
||||
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8})
|
||||
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
|
||||
test.AssertEquals(t, len(cert.Extensions), 9) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, SCT list
|
||||
test.AssertDeepEquals(t, cert.Extensions[8], pkix.Extension{
|
||||
Id: sctListOID,
|
||||
Value: []byte{4, 51, 0, 49, 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
})
|
||||
}
|
||||
|
||||
func TestIssueMustStaple(t *testing.T) {
|
||||
fc := clock.NewFake()
|
||||
fc.Set(time.Now())
|
||||
signer, err := NewSigner(Config{
|
||||
Issuer: issuerCert,
|
||||
Signer: issuerSigner,
|
||||
Clk: fc,
|
||||
Profile: defaultProfileConfig(),
|
||||
IgnoredLints: []string{"w_ct_sct_policy_count_unsatisfied"},
|
||||
})
|
||||
test.AssertNotError(t, err, "NewSigner failed")
|
||||
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
certBytes, err := signer.Issue(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
DNSNames: []string{"example.com"},
|
||||
IncludeMustStaple: true,
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour),
|
||||
})
|
||||
test.AssertNotError(t, err, "Issue failed")
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
test.AssertNotError(t, err, "failed to parse certificate")
|
||||
err = cert.CheckSignatureFrom(issuerCert)
|
||||
test.AssertNotError(t, err, "signature validation failed")
|
||||
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8})
|
||||
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
|
||||
test.AssertEquals(t, len(cert.Extensions), 9) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, Must-Staple
|
||||
test.AssertDeepEquals(t, cert.Extensions[8], mustStapleExt)
|
||||
}
|
||||
|
||||
func TestIssueBadLint(t *testing.T) {
|
||||
fc := clock.NewFake()
|
||||
fc.Set(time.Now())
|
||||
signer, err := NewSigner(Config{
|
||||
Issuer: issuerCert,
|
||||
Signer: issuerSigner,
|
||||
Clk: fc,
|
||||
Profile: defaultProfileConfig(),
|
||||
})
|
||||
test.AssertNotError(t, err, "NewSigner failed")
|
||||
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
test.AssertNotError(t, err, "failed to generate test key")
|
||||
_, err = signer.Issue(&IssuanceRequest{
|
||||
PublicKey: pk.Public(),
|
||||
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
DNSNames: []string{"example.com"},
|
||||
NotBefore: fc.Now(),
|
||||
NotAfter: fc.Now().Add(time.Hour),
|
||||
})
|
||||
test.AssertError(t, err, "Issue didn't fail")
|
||||
test.AssertEquals(t, err.Error(), "tbsCertificate linting failed: w_ct_sct_policy_count_unsatisfied")
|
||||
}
|
|
@ -37,106 +37,43 @@
|
|||
"CertFile": "/tmp/intermediate-cert-rsa-b.pem",
|
||||
"NumSessions": 2
|
||||
}],
|
||||
"SignerProfile": {
|
||||
"allowRSAKeys": true,
|
||||
"allowECDSAKeys": true,
|
||||
"allowMustStaple": true,
|
||||
"allowCTPoison": true,
|
||||
"allowSCTList": true,
|
||||
"allowCommonName": true,
|
||||
"issuerURL": "http://127.0.0.1:4000/acme/issuer-cert",
|
||||
"ocspURL": "http://127.0.0.1:4002/",
|
||||
"crlURL": "http://example.com/crl",
|
||||
"policies": [
|
||||
{
|
||||
"oid": "2.23.140.1.2.1"
|
||||
},
|
||||
{
|
||||
"oid": "1.2.3.4",
|
||||
"qualifiers": [
|
||||
{
|
||||
"type": "id-qt-cps",
|
||||
"value": "http://example.com/cps"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"maxValidityPeriod": "2160h",
|
||||
"maxValidityBackdate": "1h5m"
|
||||
},
|
||||
"expiry": "2160h",
|
||||
"backdate": "1h",
|
||||
"lifespanOCSP": "96h",
|
||||
"maxNames": 100,
|
||||
"hostnamePolicyFile": "test/hostname-policy.yaml",
|
||||
"cfssl": {
|
||||
"signing": {
|
||||
"profiles": {
|
||||
"rsaEE": {
|
||||
"usages": [
|
||||
"digital signature",
|
||||
"key encipherment",
|
||||
"server auth",
|
||||
"client auth"
|
||||
],
|
||||
"backdate": "1h",
|
||||
"ca_constraint": { "is_ca": false },
|
||||
"issuer_urls": [
|
||||
"http://boulder:4430/acme/issuer-cert"
|
||||
],
|
||||
"ocsp_url": "http://127.0.0.1:4002/",
|
||||
"crl_url": "http://example.com/crl",
|
||||
"policies": [
|
||||
{
|
||||
"ID": "2.23.140.1.2.1"
|
||||
},
|
||||
{
|
||||
"ID": "1.2.3.4",
|
||||
"Qualifiers": [ {
|
||||
"type": "id-qt-cps",
|
||||
"value": "http://example.com/cps"
|
||||
} ]
|
||||
}
|
||||
],
|
||||
"expiry": "2160h",
|
||||
"CSRWhitelist": {
|
||||
"PublicKeyAlgorithm": true,
|
||||
"PublicKey": true,
|
||||
"SignatureAlgorithm": true
|
||||
},
|
||||
"ClientProvidesSerialNumbers": true,
|
||||
"allowed_extensions": [ "1.3.6.1.5.5.7.1.24" ],
|
||||
"lint_error_level": "pass",
|
||||
"ignored_lints": [
|
||||
"n_subject_common_name_included"
|
||||
]
|
||||
},
|
||||
"ecdsaEE": {
|
||||
"usages": [
|
||||
"digital signature",
|
||||
"server auth",
|
||||
"client auth"
|
||||
],
|
||||
"backdate": "1h",
|
||||
"is_ca": false,
|
||||
"issuer_urls": [
|
||||
"http://127.0.0.1:4000/acme/issuer-cert"
|
||||
],
|
||||
"ocsp_url": "http://127.0.0.1:4002/",
|
||||
"crl_url": "http://example.com/crl",
|
||||
"policies": [
|
||||
{
|
||||
"ID": "2.23.140.1.2.1"
|
||||
},
|
||||
{
|
||||
"ID": "1.2.3.4",
|
||||
"Qualifiers": [ {
|
||||
"type": "id-qt-cps",
|
||||
"value": "http://example.com/cps"
|
||||
}, {
|
||||
"type": "id-qt-unotice",
|
||||
"value": "Do What Thou Wilt"
|
||||
} ]
|
||||
}
|
||||
],
|
||||
"expiry": "2160h",
|
||||
"CSRWhitelist": {
|
||||
"PublicKeyAlgorithm": true,
|
||||
"PublicKey": true,
|
||||
"SignatureAlgorithm": true
|
||||
},
|
||||
"ClientProvidesSerialNumbers": true,
|
||||
"allowed_extensions": [ "1.3.6.1.5.5.7.1.24" ],
|
||||
"lint_error_level": "pass",
|
||||
"ignored_lints": [
|
||||
"n_subject_common_name_included"
|
||||
]
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"usages": [
|
||||
"digital signature"
|
||||
],
|
||||
"expiry": "8760h"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ignoredLints": ["n_subject_common_name_included"],
|
||||
"orphanQueueDir": "/tmp/orphaned-certificates-a",
|
||||
"features": {
|
||||
"StoreIssuerInfo": true
|
||||
"StoreIssuerInfo": true,
|
||||
"NonCFSSLSigner": true
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -37,106 +37,43 @@
|
|||
"CertFile": "/tmp/intermediate-cert-rsa-b.pem",
|
||||
"NumSessions": 2
|
||||
}],
|
||||
"SignerProfile": {
|
||||
"allowRSAKeys": true,
|
||||
"allowECDSAKeys": true,
|
||||
"allowMustStaple": true,
|
||||
"allowCTPoison": true,
|
||||
"allowSCTList": true,
|
||||
"allowCommonName": true,
|
||||
"issuerURL": "http://127.0.0.1:4000/acme/issuer-cert",
|
||||
"ocspURL": "http://127.0.0.1:4002/",
|
||||
"crlURL": "http://example.com/crl",
|
||||
"policies": [
|
||||
{
|
||||
"oid": "2.23.140.1.2.1"
|
||||
},
|
||||
{
|
||||
"oid": "1.2.3.4",
|
||||
"qualifiers": [
|
||||
{
|
||||
"type": "id-qt-cps",
|
||||
"value": "http://example.com/cps"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"maxValidityPeriod": "2160h",
|
||||
"maxValidityBackdate": "1h5m"
|
||||
},
|
||||
"expiry": "2160h",
|
||||
"backdate": "1h",
|
||||
"lifespanOCSP": "96h",
|
||||
"maxNames": 100,
|
||||
"hostnamePolicyFile": "test/hostname-policy.yaml",
|
||||
"cfssl": {
|
||||
"signing": {
|
||||
"profiles": {
|
||||
"rsaEE": {
|
||||
"usages": [
|
||||
"digital signature",
|
||||
"key encipherment",
|
||||
"server auth",
|
||||
"client auth"
|
||||
],
|
||||
"backdate": "1h",
|
||||
"ca_constraint": { "is_ca": false },
|
||||
"issuer_urls": [
|
||||
"http://boulder:4430/acme/issuer-cert"
|
||||
],
|
||||
"ocsp_url": "http://127.0.0.1:4002/",
|
||||
"crl_url": "http://example.com/crl",
|
||||
"policies": [
|
||||
{
|
||||
"ID": "2.23.140.1.2.1"
|
||||
},
|
||||
{
|
||||
"ID": "1.2.3.4",
|
||||
"Qualifiers": [ {
|
||||
"type": "id-qt-cps",
|
||||
"value": "http://example.com/cps"
|
||||
} ]
|
||||
}
|
||||
],
|
||||
"expiry": "2160h",
|
||||
"CSRWhitelist": {
|
||||
"PublicKeyAlgorithm": true,
|
||||
"PublicKey": true,
|
||||
"SignatureAlgorithm": true
|
||||
},
|
||||
"ClientProvidesSerialNumbers": true,
|
||||
"allowed_extensions": [ "1.3.6.1.5.5.7.1.24" ],
|
||||
"lint_error_level": "pass",
|
||||
"ignored_lints": [
|
||||
"n_subject_common_name_included"
|
||||
]
|
||||
},
|
||||
"ecdsaEE": {
|
||||
"usages": [
|
||||
"digital signature",
|
||||
"server auth",
|
||||
"client auth"
|
||||
],
|
||||
"backdate": "1h",
|
||||
"is_ca": false,
|
||||
"issuer_urls": [
|
||||
"http://127.0.0.1:4000/acme/issuer-cert"
|
||||
],
|
||||
"ocsp_url": "http://127.0.0.1:4002/",
|
||||
"crl_url": "http://example.com/crl",
|
||||
"policies": [
|
||||
{
|
||||
"ID": "2.23.140.1.2.1"
|
||||
},
|
||||
{
|
||||
"ID": "1.2.3.4",
|
||||
"Qualifiers": [ {
|
||||
"type": "id-qt-cps",
|
||||
"value": "http://example.com/cps"
|
||||
}, {
|
||||
"type": "id-qt-unotice",
|
||||
"value": "Do What Thou Wilt"
|
||||
} ]
|
||||
}
|
||||
],
|
||||
"expiry": "2160h",
|
||||
"CSRWhitelist": {
|
||||
"PublicKeyAlgorithm": true,
|
||||
"PublicKey": true,
|
||||
"SignatureAlgorithm": true
|
||||
},
|
||||
"ClientProvidesSerialNumbers": true,
|
||||
"allowed_extensions": [ "1.3.6.1.5.5.7.1.24" ],
|
||||
"lint_error_level": "pass",
|
||||
"ignored_lints": [
|
||||
"n_subject_common_name_included"
|
||||
]
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"usages": [
|
||||
"digital signature"
|
||||
],
|
||||
"expiry": "8760h"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ignoredLints": ["n_subject_common_name_included"],
|
||||
"orphanQueueDir": "/tmp/orphaned-certificates-b",
|
||||
"features": {
|
||||
"StoreIssuerInfo": true
|
||||
"StoreIssuerInfo": true,
|
||||
"NonCFSSLSigner": true
|
||||
}
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in New Issue