1299 lines
42 KiB
Go
1299 lines
42 KiB
Go
package ca
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/big"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/beeker1121/goque"
|
|
cfsslConfig "github.com/cloudflare/cfssl/config"
|
|
"github.com/cloudflare/cfssl/helpers"
|
|
"github.com/cloudflare/cfssl/signer"
|
|
"github.com/cloudflare/cfssl/signer/local"
|
|
ct "github.com/google/certificate-transparency-go"
|
|
cttls "github.com/google/certificate-transparency-go/tls"
|
|
"github.com/jmhodges/clock"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/zmap/zlint/v2/lint"
|
|
"golang.org/x/crypto/ocsp"
|
|
|
|
ca_config "github.com/letsencrypt/boulder/ca/config"
|
|
caPB "github.com/letsencrypt/boulder/ca/proto"
|
|
"github.com/letsencrypt/boulder/cmd"
|
|
"github.com/letsencrypt/boulder/core"
|
|
corepb "github.com/letsencrypt/boulder/core/proto"
|
|
berrors "github.com/letsencrypt/boulder/errors"
|
|
"github.com/letsencrypt/boulder/features"
|
|
"github.com/letsencrypt/boulder/goodkey"
|
|
blog "github.com/letsencrypt/boulder/log"
|
|
"github.com/letsencrypt/boulder/metrics"
|
|
"github.com/letsencrypt/boulder/policy"
|
|
sapb "github.com/letsencrypt/boulder/sa/proto"
|
|
"github.com/letsencrypt/boulder/test"
|
|
)
|
|
|
|
var (
|
|
// * Random public key
|
|
// * CN = not-example.com
|
|
// * DNSNames = not-example.com, www.not-example.com
|
|
CNandSANCSR = mustRead("./testdata/cn_and_san.der.csr")
|
|
|
|
// CSR generated by Go:
|
|
// * Random public key
|
|
// * C = US
|
|
// * CN = [none]
|
|
// * DNSNames = not-example.com
|
|
NoCNCSR = mustRead("./testdata/no_cn.der.csr")
|
|
|
|
// CSR generated by Go:
|
|
// * Random public key
|
|
// * CN = not-example.com
|
|
// * Includes an extensionRequest attribute for a well-formed TLS Feature extension
|
|
MustStapleCSR = mustRead("./testdata/must_staple.der.csr")
|
|
|
|
// CSR generated by Go:
|
|
// * Random public key
|
|
// * CN = not-example.com
|
|
// * Includes extensionRequest attributes for *two* must-staple extensions
|
|
DuplicateMustStapleCSR = mustRead("./testdata/duplicate_must_staple.der.csr")
|
|
|
|
// CSR generated by Go:
|
|
// * Random public key
|
|
// * CN = not-example.com
|
|
// * Includes an extensionRequest attribute for an unknown extension with an
|
|
// empty value. That extension's OID, 2.25.123456789, is on the UUID arc.
|
|
// It isn't a real randomly-generated UUID because Go represents the
|
|
// components of the OID as 32-bit integers, which aren't large enough to
|
|
// hold a real 128-bit UUID; this doesn't matter as far as what we're
|
|
// testing here is concerned.
|
|
UnsupportedExtensionCSR = mustRead("./testdata/unsupported_extension.der.csr")
|
|
|
|
// CSR generated by Go:
|
|
// * Random public key
|
|
// * CN = not-example.com
|
|
// * Includes an extensionRequest attribute for the CT poison extension
|
|
// with a valid NULL value.
|
|
CTPoisonExtensionCSR = mustRead("./testdata/ct_poison_extension.der.csr")
|
|
|
|
// CSR generated by Go:
|
|
// * Random public key
|
|
// * CN = not-example.com
|
|
// * Includes an extensionRequest attribute for the CT poison extension
|
|
// with an invalid empty value.
|
|
CTPoisonExtensionEmptyCSR = mustRead("./testdata/ct_poison_extension_empty.der.csr")
|
|
|
|
// CSR generated by Go:
|
|
// * Random ECDSA public key.
|
|
// * CN = [none]
|
|
// * DNSNames = example.com, example2.com
|
|
ECDSACSR = mustRead("./testdata/ecdsa.der.csr")
|
|
|
|
// This is never modified, but it must be a var instead of a const so we can make references to it.
|
|
arbitraryRegID int64 = 1001
|
|
|
|
// OIDExtensionCTPoison is defined in RFC 6962 s3.1.
|
|
OIDExtensionCTPoison = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}
|
|
|
|
// The "certificate-for-precertificate" tests use the precertificate from a
|
|
// previous "precertificate" test, in order to verify that the CA is
|
|
// stateless with respect to these two operations, since a separate CA
|
|
// object instance will be used for generating each. Consequently, the
|
|
// "precertificate" tests must be before the "certificate-for-precertificate"
|
|
// tests in this list, and we cannot run these sub-tests concurrently.
|
|
//
|
|
// In order to test the case where the same CA object is used for issuing
|
|
// both the precertificate and the certificate, we'd need to contort
|
|
// |TestIssueCertificate| quite a bit, and since it isn't clear that that
|
|
// would be useful, we've avoided adding that case, at least for now.
|
|
issuanceModes = []IssuanceMode{
|
|
{name: "precertificate", issueCertificateForPrecertificate: false},
|
|
{name: "certificate-for-precertificate", issueCertificateForPrecertificate: true},
|
|
}
|
|
)
|
|
|
|
// CFSSL config
|
|
const rsaProfileName = "rsaEE"
|
|
const ecdsaProfileName = "ecdsaEE"
|
|
const caKeyFile = "../test/test-ca.key"
|
|
const caCertFile = "../test/test-ca.pem"
|
|
|
|
func mustRead(path string) []byte {
|
|
b, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("unable to read %#v: %s", path, err))
|
|
}
|
|
return b
|
|
}
|
|
|
|
type testCtx struct {
|
|
caConfig ca_config.CAConfig
|
|
pa core.PolicyAuthority
|
|
issuers []Issuer
|
|
keyPolicy goodkey.KeyPolicy
|
|
fc clock.FakeClock
|
|
stats prometheus.Registerer
|
|
logger *blog.Mock
|
|
}
|
|
|
|
type mockSA struct {
|
|
certificate core.Certificate
|
|
}
|
|
|
|
func (m *mockSA) AddCertificate(ctx context.Context, der []byte, _ int64, _ []byte, _ *time.Time) (string, error) {
|
|
m.certificate.DER = der
|
|
return "", nil
|
|
}
|
|
|
|
func (m *mockSA) AddPrecertificate(ctx context.Context, req *sapb.AddCertificateRequest) (*corepb.Empty, error) {
|
|
return &corepb.Empty{}, nil
|
|
}
|
|
|
|
func (m *mockSA) AddSerial(ctx context.Context, req *sapb.AddSerialRequest) (*corepb.Empty, error) {
|
|
return &corepb.Empty{}, nil
|
|
}
|
|
|
|
func (m *mockSA) SerialExists(ctx context.Context, req *sapb.Serial) (*sapb.Exists, error) {
|
|
e := true
|
|
return &sapb.Exists{Exists: &e}, nil
|
|
}
|
|
|
|
func (m *mockSA) GetCertificate(ctx context.Context, serial string) (core.Certificate, error) {
|
|
return core.Certificate{}, berrors.NotFoundError("cannot find the cert")
|
|
}
|
|
|
|
var caKey crypto.Signer
|
|
var caCert *x509.Certificate
|
|
var ctx = context.Background()
|
|
|
|
func init() {
|
|
var err error
|
|
caKey, err = helpers.ParsePrivateKeyPEM(mustRead(caKeyFile))
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Unable to parse %s: %s", caKeyFile, err))
|
|
}
|
|
caCert, err = core.LoadCert(caCertFile)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Unable to parse %s: %s", caCertFile, err))
|
|
}
|
|
}
|
|
|
|
func setup(t *testing.T) *testCtx {
|
|
features.Reset()
|
|
fc := clock.NewFake()
|
|
fc.Add(1 * time.Hour)
|
|
|
|
pa, err := policy.New(nil)
|
|
test.AssertNotError(t, err, "Couldn't create PA")
|
|
err = pa.SetHostnamePolicyFile("../test/hostname-policy.yaml")
|
|
test.AssertNotError(t, err, "Couldn't set hostname policy")
|
|
|
|
allowedExtensions := []cfsslConfig.OID{
|
|
cfsslConfig.OID(oidTLSFeature),
|
|
cfsslConfig.OID(OIDExtensionCTPoison),
|
|
}
|
|
|
|
// Create a CA
|
|
caConfig := ca_config.CAConfig{
|
|
RSAProfile: rsaProfileName,
|
|
ECDSAProfile: ecdsaProfileName,
|
|
SerialPrefix: 17,
|
|
Expiry: "8760h",
|
|
// TODO(briansmith): When the defaulting of Backdate is removed, this
|
|
// will need to be uncommented. Leave it commented for now to test the
|
|
// defaulting logic.
|
|
// Backdate: cmd.ConfigDuration{Duration: time.Hour},
|
|
LifespanOCSP: cmd.ConfigDuration{Duration: 45 * time.Minute},
|
|
MaxNames: 2,
|
|
CFSSL: cfsslConfig.Config{
|
|
Signing: &cfsslConfig.Signing{
|
|
Profiles: map[string]*cfsslConfig.SigningProfile{
|
|
rsaProfileName: {
|
|
Usage: []string{"digital signature", "key encipherment", "server auth"},
|
|
IssuerURL: []string{"http://not-example.com/issuer-url"},
|
|
OCSP: "http://not-example.com/ocsp",
|
|
CRL: "http://not-example.com/crl",
|
|
|
|
Policies: []cfsslConfig.CertificatePolicy{
|
|
{
|
|
ID: cfsslConfig.OID(asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}),
|
|
},
|
|
},
|
|
ExpiryString: "8760h",
|
|
Backdate: time.Hour,
|
|
CSRWhitelist: &cfsslConfig.CSRWhitelist{
|
|
PublicKeyAlgorithm: true,
|
|
PublicKey: true,
|
|
SignatureAlgorithm: true,
|
|
},
|
|
ClientProvidesSerialNumbers: true,
|
|
AllowedExtensions: allowedExtensions,
|
|
},
|
|
ecdsaProfileName: {
|
|
Usage: []string{"digital signature", "server auth"},
|
|
IssuerURL: []string{"http://not-example.com/issuer-url"},
|
|
OCSP: "http://not-example.com/ocsp",
|
|
CRL: "http://not-example.com/crl",
|
|
|
|
Policies: []cfsslConfig.CertificatePolicy{
|
|
{
|
|
ID: cfsslConfig.OID(asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}),
|
|
},
|
|
},
|
|
ExpiryString: "8760h",
|
|
Backdate: time.Hour,
|
|
CSRWhitelist: &cfsslConfig.CSRWhitelist{
|
|
PublicKeyAlgorithm: true,
|
|
PublicKey: true,
|
|
SignatureAlgorithm: true,
|
|
},
|
|
ClientProvidesSerialNumbers: true,
|
|
AllowedExtensions: allowedExtensions,
|
|
},
|
|
},
|
|
Default: &cfsslConfig.SigningProfile{
|
|
ExpiryString: "8760h",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
issuers := []Issuer{{caKey, caCert}}
|
|
|
|
keyPolicy := goodkey.KeyPolicy{
|
|
AllowRSA: true,
|
|
AllowECDSANISTP256: true,
|
|
AllowECDSANISTP384: true,
|
|
}
|
|
|
|
logger := blog.NewMock()
|
|
|
|
return &testCtx{
|
|
caConfig,
|
|
pa,
|
|
issuers,
|
|
keyPolicy,
|
|
fc,
|
|
metrics.NoopRegisterer,
|
|
logger,
|
|
}
|
|
}
|
|
|
|
func TestFailNoSerial(t *testing.T) {
|
|
testCtx := setup(t)
|
|
|
|
testCtx.caConfig.SerialPrefix = 0
|
|
_, err := NewCertificateAuthorityImpl(
|
|
testCtx.caConfig,
|
|
nil,
|
|
nil,
|
|
testCtx.fc,
|
|
testCtx.stats,
|
|
testCtx.issuers,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
nil)
|
|
test.AssertError(t, err, "CA should have failed with no SerialPrefix")
|
|
}
|
|
|
|
type TestCertificateIssuance struct {
|
|
ca *CertificateAuthorityImpl
|
|
sa *mockSA
|
|
req *x509.CertificateRequest
|
|
mode IssuanceMode
|
|
certDER []byte
|
|
cert *x509.Certificate
|
|
}
|
|
|
|
type IssuanceMode struct {
|
|
name string
|
|
issueCertificateForPrecertificate bool
|
|
}
|
|
|
|
func TestIssuePrecertificate(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
csr []byte
|
|
subTest func(t *testing.T, i *TestCertificateIssuance)
|
|
}{
|
|
{"IssuePrecertificate", CNandSANCSR, issueCertificateSubTestIssuePrecertificate},
|
|
{"ValidityUsesCAClock", CNandSANCSR, issueCertificateSubTestValidityUsesCAClock},
|
|
{"AllowNoCN", NoCNCSR, issueCertificateSubTestAllowNoCN},
|
|
{"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)
|
|
|
|
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")
|
|
|
|
issueReq := &caPB.IssueCertificateRequest{Csr: testCase.csr, RegistrationID: &arbitraryRegID}
|
|
|
|
var certDER []byte
|
|
response, err := ca.IssuePrecertificate(ctx, issueReq)
|
|
|
|
test.AssertNotError(t, err, "Failed to issue precertificate")
|
|
certDER = response.DER
|
|
|
|
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
|
|
}
|
|
|
|
i := TestCertificateIssuance{
|
|
ca: ca,
|
|
sa: sa,
|
|
req: req,
|
|
mode: mode,
|
|
certDER: certDER,
|
|
cert: cert,
|
|
}
|
|
|
|
testCase.subTest(t, &i)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func issueCertificateSubTestSetup(t *testing.T) (*CertificateAuthorityImpl, *mockSA) {
|
|
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")
|
|
ca.forceCNFromSAN = false
|
|
|
|
return ca, sa
|
|
}
|
|
|
|
func issueCertificateSubTestIssuePrecertificate(t *testing.T, i *TestCertificateIssuance) {
|
|
cert := i.cert
|
|
|
|
test.AssertEquals(t, cert.Subject.CommonName, "not-example.com")
|
|
|
|
if len(cert.DNSNames) == 1 {
|
|
if cert.DNSNames[0] != "not-example.com" {
|
|
t.Errorf("Improper list of domain names %v", cert.DNSNames)
|
|
}
|
|
t.Errorf("Improper list of domain names %v", cert.DNSNames)
|
|
}
|
|
|
|
if len(cert.Subject.Country) > 0 {
|
|
t.Errorf("Subject contained unauthorized values: %v", cert.Subject)
|
|
}
|
|
|
|
serialString := core.SerialToString(cert.SerialNumber)
|
|
if cert.Subject.SerialNumber != serialString {
|
|
t.Errorf("SerialNumber: want %#v, got %#v", serialString, cert.Subject.SerialNumber)
|
|
}
|
|
}
|
|
|
|
func issueCertificateSubTestValidityUsesCAClock(t *testing.T, i *TestCertificateIssuance) {
|
|
test.AssertEquals(t, i.cert.NotBefore, i.ca.clk.Now().Add(-1*i.ca.backdate))
|
|
test.AssertEquals(t, i.cert.NotAfter, i.cert.NotBefore.Add(i.ca.validityPeriod))
|
|
}
|
|
|
|
// Test issuing when multiple issuers are present.
|
|
func TestMultipleIssuers(t *testing.T) {
|
|
testCtx := setup(t)
|
|
// Load multiple issuers, and ensure the first one in the list is used.
|
|
newIssuerCert, err := core.LoadCert("../test/test-ca2.pem")
|
|
test.AssertNotError(t, err, "Failed to load new cert")
|
|
newIssuers := []Issuer{
|
|
{
|
|
Signer: caKey,
|
|
// newIssuerCert is first, so it will be the default.
|
|
Cert: newIssuerCert,
|
|
}, {
|
|
Signer: caKey,
|
|
Cert: caCert,
|
|
},
|
|
}
|
|
sa := &mockSA{}
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
testCtx.caConfig,
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.fc,
|
|
testCtx.stats,
|
|
newIssuers,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
nil)
|
|
test.AssertNotError(t, err, "Failed to remake CA")
|
|
|
|
issuedCert, err := ca.IssuePrecertificate(ctx, &caPB.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: &arbitraryRegID})
|
|
test.AssertNotError(t, err, "Failed to issue certificate")
|
|
|
|
cert, err := x509.ParseCertificate(issuedCert.DER)
|
|
test.AssertNotError(t, err, "Certificate failed to parse")
|
|
// Verify cert was signed by newIssuerCert, not caCert.
|
|
err = cert.CheckSignatureFrom(newIssuerCert)
|
|
test.AssertNotError(t, err, "Certificate failed signature validation")
|
|
}
|
|
|
|
func TestOCSP(t *testing.T) {
|
|
testCtx := setup(t)
|
|
sa := &mockSA{}
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
testCtx.caConfig,
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.fc,
|
|
testCtx.stats,
|
|
testCtx.issuers,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
nil)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
issueReq := caPB.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: &arbitraryRegID}
|
|
|
|
cert, err := ca.IssuePrecertificate(ctx, &issueReq)
|
|
test.AssertNotError(t, err, "Failed to issue")
|
|
parsedCert, err := x509.ParseCertificate(cert.DER)
|
|
test.AssertNotError(t, err, "Failed to parse cert")
|
|
status := string(core.OCSPStatusGood)
|
|
ocspResp, err := ca.GenerateOCSP(ctx, &caPB.GenerateOCSPRequest{
|
|
CertDER: cert.DER,
|
|
Status: &status,
|
|
})
|
|
test.AssertNotError(t, err, "Failed to generate OCSP")
|
|
parsed, err := ocsp.ParseResponse(ocspResp.Response, caCert)
|
|
test.AssertNotError(t, err, "Failed to parse validate OCSP")
|
|
test.AssertEquals(t, parsed.Status, 0)
|
|
test.AssertEquals(t, parsed.RevocationReason, 0)
|
|
test.AssertEquals(t, parsed.SerialNumber.Cmp(parsedCert.SerialNumber), 0)
|
|
|
|
// Test that signatures are checked.
|
|
_, err = ca.GenerateOCSP(ctx, &caPB.GenerateOCSPRequest{
|
|
CertDER: append(cert.DER, byte(0)),
|
|
Status: &status,
|
|
})
|
|
test.AssertError(t, err, "Generated OCSP for cert with bad signature")
|
|
|
|
// Load multiple issuers, including the old issuer, and ensure OCSP is still
|
|
// signed correctly.
|
|
newIssuerCert, err := core.LoadCert("../test/test-ca2.pem")
|
|
test.AssertNotError(t, err, "Failed to load new cert")
|
|
newIssuers := []Issuer{
|
|
{
|
|
Signer: caKey,
|
|
// newIssuerCert is first, so it will be the default.
|
|
Cert: newIssuerCert,
|
|
}, {
|
|
Signer: caKey,
|
|
Cert: caCert,
|
|
},
|
|
}
|
|
ca, err = NewCertificateAuthorityImpl(
|
|
testCtx.caConfig,
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.fc,
|
|
testCtx.stats,
|
|
newIssuers,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
nil)
|
|
test.AssertNotError(t, err, "Failed to remake CA")
|
|
|
|
// Now issue a new precert, signed by newIssuerCert
|
|
newCert, err := ca.IssuePrecertificate(ctx, &issueReq)
|
|
test.AssertNotError(t, err, "Failed to issue newCert")
|
|
parsedNewCert, err := x509.ParseCertificate(newCert.DER)
|
|
test.AssertNotError(t, err, "Failed to parse newCert")
|
|
|
|
err = parsedNewCert.CheckSignatureFrom(newIssuerCert)
|
|
t.Logf("check sig: %s", err)
|
|
|
|
// ocspResp2 is a second OCSP response for `cert` (issued by caCert), and
|
|
// should be signed by caCert.
|
|
ocspResp2, err := ca.GenerateOCSP(ctx, &caPB.GenerateOCSPRequest{
|
|
CertDER: append([]byte(nil), cert.DER...),
|
|
Status: &status,
|
|
})
|
|
test.AssertNotError(t, err, "Failed to sign second OCSP response")
|
|
_, err = ocsp.ParseResponse(ocspResp2.Response, caCert)
|
|
test.AssertNotError(t, err, "Failed to parse / validate second OCSP response")
|
|
|
|
// newCertOcspResp is an OCSP response for `newCert` (issued by newIssuer),
|
|
// and should be signed by newIssuer.
|
|
newCertOcspResp, err := ca.GenerateOCSP(ctx, &caPB.GenerateOCSPRequest{
|
|
CertDER: newCert.DER,
|
|
Status: &status,
|
|
})
|
|
test.AssertNotError(t, err, "Failed to generate OCSP")
|
|
parsedNewCertOcspResp, err := ocsp.ParseResponse(newCertOcspResp.Response, newIssuerCert)
|
|
test.AssertNotError(t, err, "Failed to parse / validate OCSP for newCert")
|
|
test.AssertEquals(t, parsedNewCertOcspResp.Status, 0)
|
|
test.AssertEquals(t, parsedNewCertOcspResp.RevocationReason, 0)
|
|
test.AssertEquals(t, parsedNewCertOcspResp.SerialNumber.Cmp(parsedNewCert.SerialNumber), 0)
|
|
}
|
|
|
|
func TestInvalidCSRs(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
csrPath string
|
|
check func(t *testing.T, ca *CertificateAuthorityImpl, sa *mockSA)
|
|
errorMessage string
|
|
errorType berrors.ErrorType
|
|
}{
|
|
// Test that the CA rejects CSRs that have no names.
|
|
//
|
|
// CSR generated by Go:
|
|
// * Random RSA public key.
|
|
// * CN = [none]
|
|
// * DNSNames = [none]
|
|
{"RejectNoHostnames", "./testdata/no_names.der.csr", nil, "Issued certificate with no names", berrors.BadCSR},
|
|
|
|
// Test that the CA rejects CSRs that have too many names.
|
|
//
|
|
// CSR generated by Go:
|
|
// * Random public key
|
|
// * CN = [none]
|
|
// * DNSNames = not-example.com, www.not-example.com, mail.example.com
|
|
{"RejectTooManyHostnames", "./testdata/too_many_names.der.csr", nil, "Issued certificate with too many names", berrors.BadCSR},
|
|
|
|
// Test that the CA rejects CSRs that have public keys that are too short.
|
|
//
|
|
// CSR generated by Go:
|
|
// * Random public key -- 512 bits long
|
|
// * CN = (none)
|
|
// * DNSNames = not-example.com, www.not-example.com, mail.not-example.com
|
|
{"RejectShortKey", "./testdata/short_key.der.csr", nil, "Issued a certificate with too short a key.", berrors.BadCSR},
|
|
|
|
// CSR generated by Go:
|
|
// * Random RSA public key.
|
|
// * CN = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com
|
|
// * DNSNames = [none]
|
|
{"RejectLongCommonName", "./testdata/long_cn.der.csr", nil, "Issued a certificate with a CN over 64 bytes.", berrors.BadCSR},
|
|
|
|
// CSR generated by OpenSSL:
|
|
// Edited signature to become invalid.
|
|
{"RejectWrongSignature", "./testdata/invalid_signature.der.csr", nil, "Issued a certificate based on a CSR with an invalid signature.", berrors.BadCSR},
|
|
|
|
// CSR generated by Go:
|
|
// * Random public key
|
|
// * CN = not-example.com
|
|
// * Includes an extensionRequest attribute for an empty TLS Feature extension
|
|
{"TLSFeatureUnknown", "./testdata/tls_feature_unknown.der.csr", issueCertificateSubTestTLSFeatureUnknown, "Issued a certificate based on a CSR with an empty TLS feature extension.", berrors.Malformed},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
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")
|
|
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
serializedCSR := mustRead(testCase.csrPath)
|
|
issueReq := &caPB.IssueCertificateRequest{Csr: serializedCSR, RegistrationID: &arbitraryRegID}
|
|
_, err = ca.IssuePrecertificate(ctx, issueReq)
|
|
|
|
test.Assert(t, berrors.Is(err, testCase.errorType), "Incorrect error type returned")
|
|
test.AssertEquals(t, signatureCountByPurpose("cert", ca.signatureCount), 0)
|
|
|
|
test.AssertError(t, err, testCase.errorMessage)
|
|
if testCase.check != nil {
|
|
testCase.check(t, ca, sa)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRejectValidityTooLong(t *testing.T) {
|
|
testCtx := setup(t)
|
|
sa := &mockSA{}
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
testCtx.caConfig,
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.fc,
|
|
testCtx.stats,
|
|
testCtx.issuers,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
nil)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
// This time is a few minutes before the notAfter in testdata/ca_cert.pem
|
|
future, err := time.Parse(time.RFC3339, "2025-02-10T00:30:00Z")
|
|
|
|
test.AssertNotError(t, err, "Failed to parse time")
|
|
testCtx.fc.Set(future)
|
|
// Test that the CA rejects CSRs that would expire after the intermediate cert
|
|
_, err = ca.IssuePrecertificate(ctx, &caPB.IssueCertificateRequest{Csr: NoCNCSR, RegistrationID: &arbitraryRegID})
|
|
test.AssertError(t, err, "Cannot issue a certificate that expires after the intermediate certificate")
|
|
test.Assert(t, berrors.Is(err, berrors.InternalServer), "Incorrect error type returned")
|
|
}
|
|
|
|
func TestSingleAIAEnforcement(t *testing.T) {
|
|
pa, err := policy.New(nil)
|
|
test.AssertNotError(t, err, "Couldn't create PA")
|
|
|
|
_, err = NewCertificateAuthorityImpl(
|
|
ca_config.CAConfig{
|
|
SerialPrefix: 1,
|
|
LifespanOCSP: cmd.ConfigDuration{Duration: time.Second},
|
|
CFSSL: cfsslConfig.Config{
|
|
Signing: &cfsslConfig.Signing{
|
|
Profiles: map[string]*cfsslConfig.SigningProfile{
|
|
rsaProfileName: {
|
|
IssuerURL: []string{"http://not-example.com/issuer-url", "bad"},
|
|
Usage: []string{"digital signature", "key encipherment", "server auth"},
|
|
OCSP: "http://not-example.com/ocsp",
|
|
CRL: "http://not-example.com/crl",
|
|
Policies: []cfsslConfig.CertificatePolicy{
|
|
{
|
|
ID: cfsslConfig.OID(asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}),
|
|
},
|
|
},
|
|
ExpiryString: "8760h",
|
|
Backdate: time.Hour,
|
|
CSRWhitelist: &cfsslConfig.CSRWhitelist{
|
|
PublicKeyAlgorithm: true,
|
|
PublicKey: true,
|
|
SignatureAlgorithm: true,
|
|
},
|
|
ClientProvidesSerialNumbers: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
&mockSA{},
|
|
pa,
|
|
clock.New(),
|
|
metrics.NoopRegisterer,
|
|
nil,
|
|
goodkey.KeyPolicy{},
|
|
&blog.Mock{},
|
|
nil,
|
|
)
|
|
test.AssertError(t, err, "NewCertificateAuthorityImpl allowed a profile with multiple issuer_urls")
|
|
test.AssertEquals(t, err.Error(), "only one issuer_url supported")
|
|
}
|
|
|
|
func issueCertificateSubTestAllowNoCN(t *testing.T, i *TestCertificateIssuance) {
|
|
cert := i.cert
|
|
|
|
if cert.Subject.CommonName != "" {
|
|
t.Errorf("want no CommonName, got %#v", cert.Subject.CommonName)
|
|
}
|
|
serial := core.SerialToString(cert.SerialNumber)
|
|
if cert.Subject.SerialNumber != serial {
|
|
t.Errorf("SerialNumber: want %#v, got %#v", serial, cert.Subject.SerialNumber)
|
|
}
|
|
|
|
expected := []string{}
|
|
expected = append(expected, i.req.DNSNames...)
|
|
sort.Strings(expected)
|
|
actual := []string{}
|
|
actual = append(actual, cert.DNSNames...)
|
|
sort.Strings(actual)
|
|
test.AssertDeepEquals(t, actual, expected)
|
|
}
|
|
|
|
func issueCertificateSubTestProfileSelectionRSA(t *testing.T, i *TestCertificateIssuance) {
|
|
// Certificates for RSA keys should be marked as usable for signatures and encryption.
|
|
expectedKeyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
|
|
t.Logf("expected key usage %v, got %v", expectedKeyUsage, i.cert.KeyUsage)
|
|
test.AssertEquals(t, i.cert.KeyUsage, expectedKeyUsage)
|
|
}
|
|
|
|
func issueCertificateSubTestProfileSelectionECDSA(t *testing.T, i *TestCertificateIssuance) {
|
|
// Certificates for ECDSA keys should be marked as usable for only signatures.
|
|
expectedKeyUsage := x509.KeyUsageDigitalSignature
|
|
t.Logf("expected key usage %v, got %v", expectedKeyUsage, i.cert.KeyUsage)
|
|
test.AssertEquals(t, i.cert.KeyUsage, expectedKeyUsage)
|
|
}
|
|
|
|
func countMustStaple(t *testing.T, cert *x509.Certificate) (count int) {
|
|
oidTLSFeature := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
|
|
for _, ext := range cert.Extensions {
|
|
if ext.Id.Equal(oidTLSFeature) {
|
|
test.Assert(t, !ext.Critical, "Extension was marked critical")
|
|
test.AssertByteEquals(t, ext.Value, mustStapleFeatureValue)
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func issueCertificateSubTestMustStaple(t *testing.T, i *TestCertificateIssuance) {
|
|
// a TLS feature extension should put a must-staple extension into the cert. Even
|
|
// if there are multiple TLS Feature extensions, only one extension should be included.
|
|
test.AssertEquals(t, test.CountCounterVec(csrExtensionCategory, csrExtensionTLSFeature, i.ca.csrExtensionCount), 1)
|
|
test.AssertEquals(t, test.CountCounterVec(csrExtensionCategory, csrExtensionTLSFeatureInvalid, i.ca.csrExtensionCount), 0)
|
|
test.AssertEquals(t, signatureCountByPurpose("precertificate", i.ca.signatureCount), 1)
|
|
test.AssertEquals(t, countMustStaple(t, i.cert), 1)
|
|
}
|
|
|
|
func issueCertificateSubTestTLSFeatureUnknown(t *testing.T, ca *CertificateAuthorityImpl, _ *mockSA) {
|
|
test.AssertEquals(t, test.CountCounterVec(csrExtensionCategory, csrExtensionTLSFeature, ca.csrExtensionCount), 1)
|
|
test.AssertEquals(t, test.CountCounterVec(csrExtensionCategory, csrExtensionTLSFeatureInvalid, ca.csrExtensionCount), 1)
|
|
}
|
|
|
|
func issueCertificateSubTestUnknownExtension(t *testing.T, i *TestCertificateIssuance) {
|
|
// Unsupported extensions in the CSR should be silently ignored.
|
|
test.AssertEquals(t, test.CountCounterVec(csrExtensionCategory, csrExtensionOther, i.ca.csrExtensionCount), 1)
|
|
test.AssertEquals(t, signatureCountByPurpose("precertificate", i.ca.signatureCount), 1)
|
|
|
|
// NOTE: The hard-coded value here will have to change over time as Boulder
|
|
// adds new (unrequested) extensions to certificates.
|
|
expectedExtensionCount := 10
|
|
test.AssertEquals(t, len(i.cert.Extensions), expectedExtensionCount)
|
|
}
|
|
|
|
func issueCertificateSubTestCTPoisonExtension(t *testing.T, i *TestCertificateIssuance) {
|
|
// The CT poison extension in the CSR should be silently ignored like an
|
|
// unknown extension, whether it has a valid or invalid value. The check
|
|
// for whether or not the poison extension is present in the issued
|
|
// certificate/precertificate is done in the caller.
|
|
test.AssertEquals(t, test.CountCounterVec(csrExtensionCategory, csrExtensionOther, i.ca.csrExtensionCount), 1)
|
|
test.AssertEquals(t, signatureCountByPurpose("precertificate", i.ca.signatureCount), 1)
|
|
}
|
|
|
|
func findExtension(extensions []pkix.Extension, id asn1.ObjectIdentifier) *pkix.Extension {
|
|
for _, ext := range extensions {
|
|
if ext.Id.Equal(id) {
|
|
return &ext
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func signatureCountByPurpose(signatureType string, signatureCount *prometheus.CounterVec) int {
|
|
return test.CountCounterVec("purpose", signatureType, signatureCount)
|
|
}
|
|
|
|
func makeSCTs() ([][]byte, error) {
|
|
sct := ct.SignedCertificateTimestamp{
|
|
SCTVersion: 0,
|
|
Timestamp: 2020,
|
|
Signature: ct.DigitallySigned{
|
|
Signature: []byte{0},
|
|
},
|
|
}
|
|
sctBytes, err := cttls.Marshal(sct)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return [][]byte{sctBytes}, err
|
|
}
|
|
|
|
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")
|
|
|
|
orderID := int64(0)
|
|
issueReq := caPB.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: &arbitraryRegID, OrderID: &orderID}
|
|
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
|
|
}
|
|
}
|
|
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: new(int64),
|
|
})
|
|
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")
|
|
}
|
|
|
|
// dupeSA returns a non-error to GetCertificate in order to simulate a request
|
|
// to issue a final certificate with a duplicate serial.
|
|
type dupeSA struct {
|
|
mockSA
|
|
}
|
|
|
|
func (m *dupeSA) GetCertificate(ctx context.Context, serial string) (core.Certificate, error) {
|
|
return core.Certificate{}, nil
|
|
}
|
|
|
|
// getCertErrorSA always returns an error for GetCertificate
|
|
type getCertErrorSA struct {
|
|
mockSA
|
|
}
|
|
|
|
func (m *getCertErrorSA) GetCertificate(ctx context.Context, serial string) (core.Certificate, error) {
|
|
return core.Certificate{}, fmt.Errorf("i don't like it")
|
|
}
|
|
|
|
func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
|
|
testCtx := setup(t)
|
|
sa := &dupeSA{}
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
testCtx.caConfig,
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.fc,
|
|
testCtx.stats,
|
|
testCtx.issuers,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
nil)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
sctBytes, err := makeSCTs()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
orderID := int64(0)
|
|
issueReq := caPB.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: &arbitraryRegID, OrderID: &orderID}
|
|
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
|
|
test.AssertNotError(t, err, "Failed to issue precert")
|
|
_, err = ca.IssueCertificateForPrecertificate(ctx, &caPB.IssueCertificateForPrecertificateRequest{
|
|
DER: precert.DER,
|
|
SCTs: sctBytes,
|
|
RegistrationID: &arbitraryRegID,
|
|
OrderID: new(int64),
|
|
})
|
|
if err == nil {
|
|
t.Error("Expected error issuing duplicate serial but got none.")
|
|
}
|
|
if !strings.Contains(err.Error(), "issuance of duplicate final certificate requested") {
|
|
t.Errorf("Wrong type of error issuing duplicate serial. Expected 'issuance of duplicate', got '%s'", err)
|
|
}
|
|
|
|
// Now check what happens if there is an error (e.g. timeout) while checking
|
|
// for the duplicate.
|
|
errorsa := &getCertErrorSA{}
|
|
errorca, err := NewCertificateAuthorityImpl(
|
|
testCtx.caConfig,
|
|
errorsa,
|
|
testCtx.pa,
|
|
testCtx.fc,
|
|
testCtx.stats,
|
|
testCtx.issuers,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
nil)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
_, err = errorca.IssueCertificateForPrecertificate(ctx, &caPB.IssueCertificateForPrecertificateRequest{
|
|
DER: precert.DER,
|
|
SCTs: sctBytes,
|
|
RegistrationID: &arbitraryRegID,
|
|
OrderID: new(int64),
|
|
})
|
|
if err == nil {
|
|
t.Fatal("Expected error issuing duplicate serial but got none.")
|
|
}
|
|
if !strings.Contains(err.Error(), "error checking for duplicate") {
|
|
t.Fatalf("Wrong type of error issuing duplicate serial. Expected 'error checking for duplicate', got '%s'", err)
|
|
}
|
|
}
|
|
|
|
type queueSA struct {
|
|
mockSA
|
|
|
|
fail bool
|
|
duplicate bool
|
|
|
|
issued *time.Time
|
|
issuedPrecert *time.Time
|
|
}
|
|
|
|
func (qsa *queueSA) AddCertificate(_ context.Context, _ []byte, _ int64, _ []byte, issued *time.Time) (string, error) {
|
|
if qsa.fail {
|
|
return "", errors.New("bad")
|
|
} else if qsa.duplicate {
|
|
return "", berrors.DuplicateError("is a dupe")
|
|
}
|
|
qsa.issued = issued
|
|
return "", nil
|
|
}
|
|
|
|
func (qsa *queueSA) AddPrecertificate(ctx context.Context, req *sapb.AddCertificateRequest) (*corepb.Empty, error) {
|
|
if qsa.fail {
|
|
return nil, errors.New("bad")
|
|
} else if qsa.duplicate {
|
|
return nil, berrors.DuplicateError("is a dupe")
|
|
}
|
|
issued := time.Unix(0, *req.Issued)
|
|
qsa.issuedPrecert = &issued
|
|
return nil, nil
|
|
}
|
|
|
|
// TestPrecertOrphanQueue tests that IssuePrecertificate writes precertificates
|
|
// to the orphan queue if storage fails, and that `integrateOrphan` later
|
|
// successfully writes those precertificates to the database. To do this, it
|
|
// uses the `queueSA` mock, which allows us to flip on and off a "fail" bit that
|
|
// decides whether it errors in response to storage requests.
|
|
func TestPrecertOrphanQueue(t *testing.T) {
|
|
tmpDir, err := ioutil.TempDir("", "orphan-queue-tmp")
|
|
defer os.Remove(tmpDir)
|
|
test.AssertNotError(t, err, "Failed to create temp directory")
|
|
orphanQueue, err := goque.OpenQueue(tmpDir)
|
|
test.AssertNotError(t, err, "Failed to open orphaned certificate queue")
|
|
|
|
qsa := &queueSA{fail: true}
|
|
testCtx := setup(t)
|
|
fakeNow := time.Date(2019, 9, 20, 0, 0, 0, 0, time.UTC)
|
|
testCtx.fc.Set(fakeNow)
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
testCtx.caConfig,
|
|
qsa,
|
|
testCtx.pa,
|
|
testCtx.fc,
|
|
testCtx.stats,
|
|
testCtx.issuers,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
orphanQueue)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
err = ca.integrateOrphan()
|
|
if err != goque.ErrEmpty {
|
|
t.Fatalf("Unexpected error, wanted %q, got %q", goque.ErrEmpty, err)
|
|
}
|
|
|
|
var one int64 = 1
|
|
_, err = ca.IssuePrecertificate(context.Background(), &caPB.IssueCertificateRequest{
|
|
RegistrationID: &one,
|
|
OrderID: &one,
|
|
Csr: CNandSANCSR,
|
|
})
|
|
test.AssertError(t, err, "Expected IssuePrecertificate to fail with `failSA`")
|
|
|
|
matches := testCtx.logger.GetAllMatching(`orphaning precertificate.* regID=\[1\], orderID=\[1\]`)
|
|
if len(matches) != 1 {
|
|
t.Errorf("no log line, or incorrect log line for orphaned precertificate:\n%s",
|
|
strings.Join(testCtx.logger.GetAllMatching(".*"), "\n"))
|
|
}
|
|
|
|
orphanCount := test.CountCounterVec("type", "precert", ca.orphanCount)
|
|
test.AssertEquals(t, orphanCount, 1)
|
|
|
|
qsa.fail = false
|
|
err = ca.integrateOrphan()
|
|
test.AssertNotError(t, err, "integrateOrphan failed")
|
|
if !qsa.issuedPrecert.Equal(fakeNow) {
|
|
t.Errorf("expected issued time to be %s, got %s", fakeNow, *qsa.issued)
|
|
}
|
|
err = ca.integrateOrphan()
|
|
if err != goque.ErrEmpty {
|
|
t.Fatalf("Unexpected error, wanted %q, got %q", goque.ErrEmpty, err)
|
|
}
|
|
|
|
adoptedCount := test.CountCounterVec("type", "precert", ca.adoptedOrphanCount)
|
|
test.AssertEquals(t, adoptedCount, 1)
|
|
}
|
|
|
|
func TestOrphanQueue(t *testing.T) {
|
|
tmpDir, err := ioutil.TempDir("", "orphan-queue-tmp")
|
|
defer os.Remove(tmpDir)
|
|
test.AssertNotError(t, err, "Failed to create temp directory")
|
|
orphanQueue, err := goque.OpenQueue(tmpDir)
|
|
test.AssertNotError(t, err, "Failed to open orphaned certificate queue")
|
|
|
|
qsa := &queueSA{fail: true}
|
|
testCtx := setup(t)
|
|
fakeNow, err := time.Parse("Mon Jan 2 15:04:05 2006", "Mon Jan 2 15:04:05 2006")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testCtx.fc.Set(fakeNow)
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
testCtx.caConfig,
|
|
qsa,
|
|
testCtx.pa,
|
|
testCtx.fc,
|
|
testCtx.stats,
|
|
testCtx.issuers,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
orphanQueue)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
err = ca.integrateOrphan()
|
|
if err != goque.ErrEmpty {
|
|
t.Fatalf("Unexpected error, wanted %q, got %q", goque.ErrEmpty, err)
|
|
}
|
|
|
|
// generate basic test cert
|
|
k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
test.AssertNotError(t, err, "Failed to generate test key")
|
|
tmpl := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
DNSNames: []string{"test.invalid"},
|
|
NotBefore: fakeNow.Add(-time.Hour),
|
|
}
|
|
certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, k.Public(), k)
|
|
test.AssertNotError(t, err, "Failed to generate test cert")
|
|
_, err = ca.storeCertificate(
|
|
context.Background(),
|
|
1,
|
|
1,
|
|
tmpl.SerialNumber,
|
|
certDER,
|
|
)
|
|
test.AssertError(t, err, "storeCertificate didn't fail when AddCertificate failed")
|
|
|
|
qsa.fail = false
|
|
err = ca.integrateOrphan()
|
|
test.AssertNotError(t, err, "integrateOrphan failed")
|
|
if !qsa.issued.Equal(fakeNow) {
|
|
t.Errorf("expected issued time to be %s, got %s", fakeNow, *qsa.issued)
|
|
}
|
|
err = ca.integrateOrphan()
|
|
if err != goque.ErrEmpty {
|
|
t.Fatalf("Unexpected error, wanted %q, got %q", goque.ErrEmpty, err)
|
|
}
|
|
|
|
// test with a duplicate cert
|
|
ca.queueOrphan(&orphanedCert{
|
|
DER: certDER,
|
|
OCSPResp: []byte{},
|
|
RegID: 1,
|
|
})
|
|
|
|
qsa.duplicate = true
|
|
err = ca.integrateOrphan()
|
|
test.AssertNotError(t, err, "integrateOrphan failed with duplicate cert")
|
|
if !qsa.issued.Equal(fakeNow) {
|
|
t.Errorf("expected issued time to be %s, got %s", fakeNow, *qsa.issued)
|
|
}
|
|
err = ca.integrateOrphan()
|
|
if err != goque.ErrEmpty {
|
|
t.Fatalf("Unexpected error, wanted %q, got %q", goque.ErrEmpty, err)
|
|
}
|
|
|
|
// add cert to queue, and recreate queue to make sure it still has the cert
|
|
qsa.fail = true
|
|
qsa.duplicate = false
|
|
_, err = ca.storeCertificate(
|
|
context.Background(),
|
|
1,
|
|
1,
|
|
tmpl.SerialNumber,
|
|
certDER,
|
|
)
|
|
test.AssertError(t, err, "storeCertificate didn't fail when AddCertificate failed")
|
|
err = orphanQueue.Close()
|
|
test.AssertNotError(t, err, "Failed to close the queue cleanly")
|
|
orphanQueue, err = goque.OpenQueue(tmpDir)
|
|
test.AssertNotError(t, err, "Failed to open orphaned certificate queue")
|
|
defer func() { _ = orphanQueue.Close() }()
|
|
ca.orphanQueue = orphanQueue
|
|
|
|
qsa.fail = false
|
|
err = ca.integrateOrphan()
|
|
test.AssertNotError(t, err, "integrateOrphan failed")
|
|
if !qsa.issued.Equal(fakeNow) {
|
|
t.Errorf("expected issued time to be %s, got %s", fakeNow, *qsa.issued)
|
|
}
|
|
err = ca.integrateOrphan()
|
|
if err != goque.ErrEmpty {
|
|
t.Fatalf("Unexpected error, wanted %q, got %q", goque.ErrEmpty, err)
|
|
}
|
|
}
|
|
|
|
type linttrapSigner struct {
|
|
lintErr error
|
|
}
|
|
|
|
func (s *linttrapSigner) Sign(signer.SignRequest) ([]byte, error) {
|
|
return nil, s.lintErr
|
|
}
|
|
|
|
func (s *linttrapSigner) SignFromPrecert(*x509.Certificate, []ct.SignedCertificateTimestamp) ([]byte, error) {
|
|
return nil, errors.New("SignFromPrecert not implemented for linttrapSigner")
|
|
}
|
|
|
|
func TestIssuePrecertificateLinting(t *testing.T) {
|
|
testCtx := setup(t)
|
|
sa := &mockSA{}
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
testCtx.caConfig,
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.fc,
|
|
testCtx.stats,
|
|
testCtx.issuers,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
nil)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
// Reconfigure the CA's eeSigner to be a linttrapSigner that always returns
|
|
// two LintResults.
|
|
ca.defaultIssuer.eeSigner = &linttrapSigner{
|
|
lintErr: &local.LintError{
|
|
ErrorResults: map[string]lint.LintResult{
|
|
"foobar": {
|
|
Status: lint.Error,
|
|
Details: "foobar is error",
|
|
},
|
|
"foobar2": {
|
|
Status: lint.Warn,
|
|
Details: "foobar2 is warning",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Clear the mock logger
|
|
testCtx.logger.Clear()
|
|
|
|
// Attempt to issue a pre-certificate
|
|
_, err = ca.IssuePrecertificate(ctx, &caPB.IssueCertificateRequest{
|
|
Csr: CNandSANCSR,
|
|
RegistrationID: &arbitraryRegID,
|
|
})
|
|
// It should error
|
|
test.AssertError(t, err, "expected err from IssuePrecertificate with linttrapSigner")
|
|
// The local.LintError should have been converted to an internal server error
|
|
// berror with the correct message.
|
|
test.Assert(t, berrors.Is(err, berrors.InternalServer), "Incorrect error type returned")
|
|
test.AssertEquals(t, err.Error(), "failed to sign certificate: pre-issuance linting found 2 error results")
|
|
|
|
// We also expect that an AUDIT level error is logged that includes the expect
|
|
// serialized JSON lintErrors
|
|
regex := `ERR: \[AUDIT\] Signing failed: serial=\[.*\] err=\[pre-issuance linting found 2 error results\] lintErrors=\{"foobar":\{"result":"error","details":"foobar is error"\},"foobar2":\{"result":"warn","details":"foobar2 is warning"\}\}`
|
|
matches := testCtx.logger.GetAllMatching(regex)
|
|
test.AssertEquals(t, len(matches), 1)
|
|
}
|
|
|
|
func TestGenerateOCSPWithIssuerID(t *testing.T) {
|
|
testCtx := setup(t)
|
|
sa := &mockSA{}
|
|
_ = features.Set(map[string]bool{"StoreIssuerInfo": true})
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
testCtx.caConfig,
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.fc,
|
|
testCtx.stats,
|
|
testCtx.issuers,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
nil)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
// GenerateOCSP with feature enabled + req contains bad IssuerID
|
|
issuerID := int64(666)
|
|
serial := "DEADDEADDEADDEADDEADDEADDEADDEADDEAD"
|
|
status := string(core.OCSPStatusGood)
|
|
_, err = ca.GenerateOCSP(context.Background(), &caPB.GenerateOCSPRequest{
|
|
IssuerID: &issuerID,
|
|
Serial: &serial,
|
|
Status: &status,
|
|
})
|
|
test.AssertError(t, err, "GenerateOCSP didn't fail with invalid IssuerID")
|
|
|
|
// GenerateOCSP with feature enabled + req contains good IssuerID
|
|
issuerID = idForIssuer(ca.defaultIssuer.cert)
|
|
_, err = ca.GenerateOCSP(context.Background(), &caPB.GenerateOCSPRequest{
|
|
IssuerID: &issuerID,
|
|
Serial: &serial,
|
|
Status: &status,
|
|
})
|
|
test.AssertNotError(t, err, "GenerateOCSP failed")
|
|
|
|
// GenerateOCSP with feature enabled + req doesn't contain IssuerID
|
|
issueReq := caPB.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: &arbitraryRegID}
|
|
cert, err := ca.IssuePrecertificate(ctx, &issueReq)
|
|
test.AssertNotError(t, err, "Failed to issue")
|
|
_, err = ca.GenerateOCSP(context.Background(), &caPB.GenerateOCSPRequest{
|
|
CertDER: cert.DER,
|
|
Status: &status,
|
|
})
|
|
test.AssertNotError(t, err, "GenerateOCSP failed")
|
|
}
|