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:
Roland Bracewell Shoemaker 2020-08-17 15:53:28 -07:00 committed by GitHub
parent 8556d8a801
commit 85851a6f2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1630 additions and 435 deletions

301
ca/ca.go
View File

@ -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))

View File

@ -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)

View File

@ -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.

View File

@ -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)

View File

@ -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) {

View File

@ -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)
}

View File

@ -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) {

View File

@ -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)

21
policyasn1/policy.go Normal file
View File

@ -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"`
}

491
signer/signer.go Normal file
View File

@ -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
}

655
signer/signer_test.go Normal file
View File

@ -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")
}

View File

@ -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
}
},

View File

@ -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
}
},