1305 lines
44 KiB
Go
1305 lines
44 KiB
Go
package ca
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"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/prometheus/client_golang/prometheus"
|
|
"github.com/zmap/zlint/v3/lint"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
|
|
capb "github.com/letsencrypt/boulder/ca/proto"
|
|
"github.com/letsencrypt/boulder/config"
|
|
"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"
|
|
"github.com/letsencrypt/boulder/issuance"
|
|
"github.com/letsencrypt/boulder/linter"
|
|
blog "github.com/letsencrypt/boulder/log"
|
|
"github.com/letsencrypt/boulder/metrics"
|
|
"github.com/letsencrypt/boulder/must"
|
|
"github.com/letsencrypt/boulder/policy"
|
|
sapb "github.com/letsencrypt/boulder/sa/proto"
|
|
"github.com/letsencrypt/boulder/test"
|
|
)
|
|
|
|
func TestImplementation(t *testing.T) {
|
|
t.Parallel()
|
|
test.AssertImplementsGRPCServer(t, &certificateAuthorityImpl{}, capb.UnimplementedCertificateAuthorityServer{})
|
|
}
|
|
|
|
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
|
|
// * 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 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")
|
|
|
|
// OIDExtensionCTPoison is defined in RFC 6962 s3.1.
|
|
OIDExtensionCTPoison = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}
|
|
|
|
// OIDExtensionSCTList is defined in RFC 6962 s3.3.
|
|
OIDExtensionSCTList = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}
|
|
)
|
|
|
|
const arbitraryRegID int64 = 1001
|
|
|
|
func mustRead(path string) []byte {
|
|
return must.Do(os.ReadFile(path))
|
|
}
|
|
|
|
type testCtx struct {
|
|
pa core.PolicyAuthority
|
|
ocsp *ocspImpl
|
|
crl *crlImpl
|
|
defaultCertProfileName string
|
|
lints lint.Registry
|
|
certProfiles map[string]issuance.ProfileConfig
|
|
certExpiry time.Duration
|
|
certBackdate time.Duration
|
|
serialPrefix int
|
|
maxNames int
|
|
boulderIssuers []*issuance.Issuer
|
|
keyPolicy goodkey.KeyPolicy
|
|
fc clock.FakeClock
|
|
stats prometheus.Registerer
|
|
signatureCount *prometheus.CounterVec
|
|
signErrorCount *prometheus.CounterVec
|
|
logger *blog.Mock
|
|
}
|
|
|
|
type mockSA struct {
|
|
certificate core.Certificate
|
|
}
|
|
|
|
func (m *mockSA) AddCertificate(ctx context.Context, req *sapb.AddCertificateRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) {
|
|
m.certificate.DER = req.Der
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *mockSA) AddPrecertificate(ctx context.Context, req *sapb.AddCertificateRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) {
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (m *mockSA) AddSerial(ctx context.Context, req *sapb.AddSerialRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) {
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func (m *mockSA) GetCertificate(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) {
|
|
return nil, berrors.NotFoundError("cannot find the cert")
|
|
}
|
|
|
|
func (m *mockSA) GetLintPrecertificate(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) {
|
|
return nil, berrors.NotFoundError("cannot find the precert")
|
|
}
|
|
|
|
func (m *mockSA) SetCertificateStatusReady(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*emptypb.Empty, error) {
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
var ctx = context.Background()
|
|
|
|
func setup(t *testing.T) *testCtx {
|
|
features.Reset()
|
|
fc := clock.NewFake()
|
|
fc.Add(1 * time.Hour)
|
|
|
|
pa, err := policy.New(nil, blog.NewMock())
|
|
test.AssertNotError(t, err, "Couldn't create PA")
|
|
err = pa.LoadHostnamePolicyFile("../test/hostname-policy.yaml")
|
|
test.AssertNotError(t, err, "Couldn't set hostname policy")
|
|
|
|
certProfiles := make(map[string]issuance.ProfileConfig, 0)
|
|
certProfiles["defaultBoulderCertificateProfile"] = issuance.ProfileConfig{
|
|
AllowMustStaple: true,
|
|
AllowCTPoison: true,
|
|
AllowSCTList: true,
|
|
AllowCommonName: true,
|
|
Policies: []issuance.PolicyConfig{
|
|
{OID: "2.23.140.1.2.1"},
|
|
},
|
|
MaxValidityPeriod: config.Duration{Duration: time.Hour * 8760},
|
|
MaxValidityBackdate: config.Duration{Duration: time.Hour},
|
|
}
|
|
certProfiles["longerLived"] = issuance.ProfileConfig{
|
|
AllowMustStaple: true,
|
|
AllowCTPoison: true,
|
|
AllowSCTList: true,
|
|
AllowCommonName: true,
|
|
Policies: []issuance.PolicyConfig{
|
|
{OID: "2.23.140.1.2.1"},
|
|
},
|
|
MaxValidityPeriod: config.Duration{Duration: time.Hour * 8761},
|
|
MaxValidityBackdate: config.Duration{Duration: time.Hour},
|
|
}
|
|
test.AssertEquals(t, len(certProfiles), 2)
|
|
|
|
boulderIssuers := make([]*issuance.Issuer, 4)
|
|
for i, name := range []string{"int-r3", "int-r4", "int-e1", "int-e2"} {
|
|
boulderIssuers[i], err = issuance.LoadIssuer(issuance.IssuerConfig{
|
|
Active: true,
|
|
IssuerURL: fmt.Sprintf("http://not-example.com/i/%s", name),
|
|
OCSPURL: "http://not-example.com/o",
|
|
CRLURLBase: fmt.Sprintf("http://not-example.com/c/%s/", name),
|
|
Location: issuance.IssuerLoc{
|
|
File: fmt.Sprintf("../test/hierarchy/%s.key.pem", name),
|
|
CertFile: fmt.Sprintf("../test/hierarchy/%s.cert.pem", name),
|
|
},
|
|
}, fc)
|
|
test.AssertNotError(t, err, "Couldn't load test issuer")
|
|
}
|
|
|
|
keyPolicy := goodkey.KeyPolicy{
|
|
AllowRSA: true,
|
|
AllowECDSANISTP256: true,
|
|
AllowECDSANISTP384: true,
|
|
}
|
|
signatureCount := prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "signatures",
|
|
Help: "Number of signatures",
|
|
},
|
|
[]string{"purpose", "issuer"})
|
|
signErrorCount := prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "signature_errors",
|
|
Help: "A counter of signature errors labelled by error type",
|
|
}, []string{"type"})
|
|
|
|
lints, err := linter.NewRegistry([]string{"w_subject_common_name_included"})
|
|
test.AssertNotError(t, err, "Failed to create zlint registry")
|
|
|
|
ocsp, err := NewOCSPImpl(
|
|
boulderIssuers,
|
|
24*time.Hour,
|
|
0,
|
|
time.Second,
|
|
blog.NewMock(),
|
|
metrics.NoopRegisterer,
|
|
signatureCount,
|
|
signErrorCount,
|
|
fc,
|
|
)
|
|
test.AssertNotError(t, err, "Failed to create ocsp impl")
|
|
|
|
crl, err := NewCRLImpl(
|
|
boulderIssuers,
|
|
issuance.CRLProfileConfig{
|
|
ValidityInterval: config.Duration{Duration: 216 * time.Hour},
|
|
MaxBackdate: config.Duration{Duration: time.Hour},
|
|
},
|
|
"http://c.boulder.test",
|
|
100,
|
|
blog.NewMock(),
|
|
)
|
|
test.AssertNotError(t, err, "Failed to create crl impl")
|
|
|
|
return &testCtx{
|
|
pa: pa,
|
|
ocsp: ocsp,
|
|
crl: crl,
|
|
defaultCertProfileName: "defaultBoulderCertificateProfile",
|
|
lints: lints,
|
|
certProfiles: certProfiles,
|
|
certExpiry: 8760 * time.Hour,
|
|
certBackdate: time.Hour,
|
|
serialPrefix: 17,
|
|
maxNames: 2,
|
|
boulderIssuers: boulderIssuers,
|
|
keyPolicy: keyPolicy,
|
|
fc: fc,
|
|
stats: metrics.NoopRegisterer,
|
|
signatureCount: signatureCount,
|
|
signErrorCount: signErrorCount,
|
|
logger: blog.NewMock(),
|
|
}
|
|
}
|
|
|
|
func TestSerialPrefix(t *testing.T) {
|
|
t.Parallel()
|
|
testCtx := setup(t)
|
|
|
|
_, err := NewCertificateAuthorityImpl(
|
|
nil,
|
|
nil,
|
|
nil,
|
|
"",
|
|
nil,
|
|
nil,
|
|
nil,
|
|
testCtx.certExpiry,
|
|
testCtx.certBackdate,
|
|
0,
|
|
testCtx.maxNames,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
testCtx.stats,
|
|
nil,
|
|
nil,
|
|
testCtx.fc)
|
|
test.AssertError(t, err, "CA should have failed with no SerialPrefix")
|
|
|
|
_, err = NewCertificateAuthorityImpl(
|
|
nil,
|
|
nil,
|
|
nil,
|
|
"",
|
|
nil,
|
|
nil,
|
|
nil,
|
|
testCtx.certExpiry,
|
|
testCtx.certBackdate,
|
|
128,
|
|
testCtx.maxNames,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
testCtx.stats,
|
|
nil,
|
|
nil,
|
|
testCtx.fc)
|
|
test.AssertError(t, err, "CA should have failed with too-large SerialPrefix")
|
|
}
|
|
|
|
type TestCertificateIssuance struct {
|
|
ca *certificateAuthorityImpl
|
|
sa *mockSA
|
|
req *x509.CertificateRequest
|
|
certDER []byte
|
|
cert *x509.Certificate
|
|
}
|
|
|
|
func TestIssuePrecertificate(t *testing.T) {
|
|
t.Parallel()
|
|
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},
|
|
{"UnknownExtension", UnsupportedExtensionCSR, issueCertificateSubTestUnknownExtension},
|
|
{"CTPoisonExtension", CTPoisonExtensionCSR, issueCertificateSubTestCTPoisonExtension},
|
|
{"CTPoisonExtensionEmpty", CTPoisonExtensionEmptyCSR, issueCertificateSubTestCTPoisonExtension},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
// TODO(#7454) Remove this rebinding
|
|
testCase := testCase
|
|
|
|
// The loop through the issuance modes must be inside the loop through
|
|
// |testCases| because the "certificate-for-precertificate" tests use
|
|
// the precertificates previously generated from the preceding
|
|
// "precertificate" test.
|
|
for _, mode := range []string{"precertificate", "certificate-for-precertificate"} {
|
|
ca, sa := issueCertificateSubTestSetup(t, nil)
|
|
t.Run(fmt.Sprintf("%s - %s", mode, testCase.name), func(t *testing.T) {
|
|
t.Parallel()
|
|
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.AssertNotNil(t, poisonExtension, "Precert doesn't contain poison extension")
|
|
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,
|
|
certDER: certDER,
|
|
cert: cert,
|
|
}
|
|
|
|
testCase.subTest(t, &i)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func issueCertificateSubTestSetup(t *testing.T, e *ECDSAAllowList) (*certificateAuthorityImpl, *mockSA) {
|
|
testCtx := setup(t)
|
|
ecdsaAllowList := &ECDSAAllowList{}
|
|
if e == nil {
|
|
e = ecdsaAllowList
|
|
}
|
|
sa := &mockSA{}
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.boulderIssuers,
|
|
testCtx.defaultCertProfileName,
|
|
testCtx.certProfiles,
|
|
testCtx.lints,
|
|
e,
|
|
testCtx.certExpiry,
|
|
testCtx.certBackdate,
|
|
testCtx.serialPrefix,
|
|
testCtx.maxNames,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
testCtx.stats,
|
|
testCtx.signatureCount,
|
|
testCtx.signErrorCount,
|
|
testCtx.fc)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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.Add(time.Second).Sub(i.cert.NotBefore), i.ca.validityPeriod)
|
|
}
|
|
|
|
// Test failure mode when no issuers are present.
|
|
func TestNoIssuers(t *testing.T) {
|
|
t.Parallel()
|
|
testCtx := setup(t)
|
|
sa := &mockSA{}
|
|
_, err := NewCertificateAuthorityImpl(
|
|
sa,
|
|
testCtx.pa,
|
|
nil, // No issuers
|
|
testCtx.defaultCertProfileName,
|
|
testCtx.certProfiles,
|
|
testCtx.lints,
|
|
nil,
|
|
testCtx.certExpiry,
|
|
testCtx.certBackdate,
|
|
testCtx.serialPrefix,
|
|
testCtx.maxNames,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
testCtx.stats,
|
|
testCtx.signatureCount,
|
|
testCtx.signErrorCount,
|
|
testCtx.fc)
|
|
test.AssertError(t, err, "No issuers found during CA construction.")
|
|
test.AssertEquals(t, err.Error(), "must have at least one issuer")
|
|
}
|
|
|
|
// Test issuing when multiple issuers are present.
|
|
func TestMultipleIssuers(t *testing.T) {
|
|
t.Parallel()
|
|
testCtx := setup(t)
|
|
sa := &mockSA{}
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.boulderIssuers,
|
|
testCtx.defaultCertProfileName,
|
|
testCtx.certProfiles,
|
|
testCtx.lints,
|
|
nil,
|
|
testCtx.certExpiry,
|
|
testCtx.certBackdate,
|
|
testCtx.serialPrefix,
|
|
testCtx.maxNames,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
testCtx.stats,
|
|
testCtx.signatureCount,
|
|
testCtx.signErrorCount,
|
|
testCtx.fc)
|
|
test.AssertNotError(t, err, "Failed to remake CA")
|
|
|
|
selectedProfile := ca.certProfiles.defaultName
|
|
_, ok := ca.certProfiles.profileByName[selectedProfile]
|
|
test.Assert(t, ok, "Certificate profile was expected to exist")
|
|
|
|
// Test that an RSA CSR gets issuance from an RSA issuer.
|
|
issuedCert, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, CertProfileName: selectedProfile})
|
|
test.AssertNotError(t, err, "Failed to issue certificate")
|
|
cert, err := x509.ParseCertificate(issuedCert.DER)
|
|
test.AssertNotError(t, err, "Certificate failed to parse")
|
|
validated := false
|
|
for _, issuer := range ca.issuers.byAlg[x509.RSA] {
|
|
err = cert.CheckSignatureFrom(issuer.Cert.Certificate)
|
|
if err == nil {
|
|
validated = true
|
|
break
|
|
}
|
|
}
|
|
test.Assert(t, validated, "Certificate failed signature validation")
|
|
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
|
|
|
|
// Test that an ECDSA CSR gets issuance from an ECDSA issuer.
|
|
issuedCert, err = ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID, CertProfileName: selectedProfile})
|
|
test.AssertNotError(t, err, "Failed to issue certificate")
|
|
cert, err = x509.ParseCertificate(issuedCert.DER)
|
|
test.AssertNotError(t, err, "Certificate failed to parse")
|
|
validated = false
|
|
for _, issuer := range ca.issuers.byAlg[x509.ECDSA] {
|
|
err = cert.CheckSignatureFrom(issuer.Cert.Certificate)
|
|
if err == nil {
|
|
validated = true
|
|
break
|
|
}
|
|
}
|
|
test.Assert(t, validated, "Certificate failed signature validation")
|
|
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 2)
|
|
}
|
|
|
|
func TestUnpredictableIssuance(t *testing.T) {
|
|
testCtx := setup(t)
|
|
sa := &mockSA{}
|
|
|
|
// Load our own set of issuer configs, specifically with:
|
|
// - 3 issuers,
|
|
// - 2 of which are active
|
|
boulderIssuers := make([]*issuance.Issuer, 3)
|
|
var err error
|
|
for i, name := range []string{"int-e1", "int-e2", "int-r3"} {
|
|
boulderIssuers[i], err = issuance.LoadIssuer(issuance.IssuerConfig{
|
|
Active: i != 0, // Make one of the ECDSA issuers inactive.
|
|
IssuerURL: fmt.Sprintf("http://not-example.com/i/%s", name),
|
|
OCSPURL: "http://not-example.com/o",
|
|
CRLURLBase: fmt.Sprintf("http://not-example.com/c/%s/", name),
|
|
Location: issuance.IssuerLoc{
|
|
File: fmt.Sprintf("../test/hierarchy/%s.key.pem", name),
|
|
CertFile: fmt.Sprintf("../test/hierarchy/%s.cert.pem", name),
|
|
},
|
|
}, testCtx.fc)
|
|
test.AssertNotError(t, err, "Couldn't load test issuer")
|
|
}
|
|
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
sa,
|
|
testCtx.pa,
|
|
boulderIssuers,
|
|
testCtx.defaultCertProfileName,
|
|
testCtx.certProfiles,
|
|
testCtx.lints,
|
|
nil,
|
|
testCtx.certExpiry,
|
|
testCtx.certBackdate,
|
|
testCtx.serialPrefix,
|
|
testCtx.maxNames,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
testCtx.stats,
|
|
testCtx.signatureCount,
|
|
testCtx.signErrorCount,
|
|
testCtx.fc)
|
|
test.AssertNotError(t, err, "Failed to remake CA")
|
|
|
|
// Then, modify the resulting issuer maps so that the RSA issuer appears to
|
|
// be an ECDSA issuer. This would be easier if we had three ECDSA issuers to
|
|
// use here, but that doesn't exist in //test/hierarchy (yet).
|
|
ca.issuers.byAlg[x509.ECDSA] = append(ca.issuers.byAlg[x509.ECDSA], ca.issuers.byAlg[x509.RSA]...)
|
|
ca.issuers.byAlg[x509.RSA] = []*issuance.Issuer{}
|
|
|
|
// Issue the same (ECDSA-keyed) certificate 20 times. None of the issuances
|
|
// should come from the inactive issuer (int-e1). At least one issuance should
|
|
// come from each of the two active issuers (int-e2 and int-r3). With 20
|
|
// trials, the probability that all 20 issuances come from the same issuer is
|
|
// 0.5 ^ 20 = 9.5e-7 ~= 1e-6 = 1 in a million, so we do not consider this test
|
|
// to be flaky.
|
|
req := &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID}
|
|
seenE2 := false
|
|
seenR3 := false
|
|
for i := 0; i < 20; i++ {
|
|
result, err := ca.IssuePrecertificate(ctx, req)
|
|
test.AssertNotError(t, err, "Failed to issue test certificate")
|
|
cert, err := x509.ParseCertificate(result.DER)
|
|
test.AssertNotError(t, err, "Failed to parse test certificate")
|
|
if strings.Contains(cert.Issuer.CommonName, "E1") {
|
|
t.Fatal("Issued certificate from inactive issuer")
|
|
} else if strings.Contains(cert.Issuer.CommonName, "E2") {
|
|
seenE2 = true
|
|
} else if strings.Contains(cert.Issuer.CommonName, "R3") {
|
|
seenR3 = true
|
|
}
|
|
}
|
|
test.Assert(t, seenE2, "Expected at least one issuance from active issuer")
|
|
test.Assert(t, seenR3, "Expected at least one issuance from active issuer")
|
|
}
|
|
|
|
func TestProfiles(t *testing.T) {
|
|
t.Parallel()
|
|
testCtx := setup(t)
|
|
test.AssertEquals(t, len(testCtx.certProfiles), 2)
|
|
|
|
sa := &mockSA{}
|
|
|
|
duplicateProfiles := make(map[string]issuance.ProfileConfig, 0)
|
|
// These profiles contain the same data which will produce an identical
|
|
// hash, even though the names are different.
|
|
duplicateProfiles["defaultBoulderCertificateProfile"] = issuance.ProfileConfig{
|
|
AllowMustStaple: false,
|
|
AllowCTPoison: false,
|
|
AllowSCTList: false,
|
|
AllowCommonName: false,
|
|
Policies: []issuance.PolicyConfig{
|
|
{OID: "2.23.140.1.2.1"},
|
|
},
|
|
MaxValidityPeriod: config.Duration{Duration: time.Hour * 8760},
|
|
MaxValidityBackdate: config.Duration{Duration: time.Hour},
|
|
}
|
|
duplicateProfiles["uhoh_ohno"] = issuance.ProfileConfig{
|
|
AllowMustStaple: false,
|
|
AllowCTPoison: false,
|
|
AllowSCTList: false,
|
|
AllowCommonName: false,
|
|
Policies: []issuance.PolicyConfig{
|
|
{OID: "2.23.140.1.2.1"},
|
|
},
|
|
MaxValidityPeriod: config.Duration{Duration: time.Hour * 8760},
|
|
MaxValidityBackdate: config.Duration{Duration: time.Hour},
|
|
}
|
|
test.AssertEquals(t, len(duplicateProfiles), 2)
|
|
|
|
jackedProfiles := make(map[string]issuance.ProfileConfig, 0)
|
|
jackedProfiles["ruhroh"] = issuance.ProfileConfig{
|
|
AllowMustStaple: false,
|
|
AllowCTPoison: false,
|
|
AllowSCTList: false,
|
|
AllowCommonName: false,
|
|
Policies: []issuance.PolicyConfig{
|
|
{OID: "2.23.140.1.2.1"},
|
|
},
|
|
MaxValidityPeriod: config.Duration{Duration: time.Hour * 9000},
|
|
MaxValidityBackdate: config.Duration{Duration: time.Hour},
|
|
}
|
|
test.AssertEquals(t, len(jackedProfiles), 1)
|
|
|
|
type nameToHash struct {
|
|
name string
|
|
hash [32]byte
|
|
}
|
|
|
|
emptyMap := make(map[string]issuance.ProfileConfig, 0)
|
|
testCases := []struct {
|
|
name string
|
|
profileConfigs map[string]issuance.ProfileConfig
|
|
defaultName string
|
|
expectedErrSubstr string
|
|
expectedProfiles []nameToHash
|
|
}{
|
|
{
|
|
name: "no profiles",
|
|
profileConfigs: emptyMap,
|
|
expectedErrSubstr: "at least one certificate profile",
|
|
},
|
|
{
|
|
name: "nil profile map",
|
|
profileConfigs: nil,
|
|
expectedErrSubstr: "at least one certificate profile",
|
|
},
|
|
{
|
|
name: "duplicate hash",
|
|
profileConfigs: duplicateProfiles,
|
|
expectedErrSubstr: "duplicate certificate profile hash",
|
|
},
|
|
{
|
|
name: "default profiles from setup func",
|
|
profileConfigs: testCtx.certProfiles,
|
|
expectedProfiles: []nameToHash{
|
|
{
|
|
name: testCtx.defaultCertProfileName,
|
|
hash: [32]byte{205, 182, 88, 236, 32, 18, 154, 120, 148, 194, 42, 215, 117, 140, 13, 169, 127, 196, 219, 67, 82, 36, 147, 67, 254, 117, 65, 112, 202, 60, 185, 9},
|
|
},
|
|
{
|
|
name: "longerLived",
|
|
hash: [32]byte{80, 228, 198, 83, 7, 184, 187, 236, 113, 17, 103, 213, 226, 245, 172, 212, 135, 241, 125, 92, 122, 200, 34, 159, 139, 72, 191, 41, 1, 244, 86, 62},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "no profile matching default name",
|
|
profileConfigs: jackedProfiles,
|
|
expectedErrSubstr: "profile object was not found for that name",
|
|
},
|
|
{
|
|
name: "certificate profile hash changed mid-issuance",
|
|
profileConfigs: jackedProfiles,
|
|
defaultName: "ruhroh",
|
|
expectedProfiles: []nameToHash{
|
|
{
|
|
// We'll change the mapped hash key under the hood during
|
|
// the test.
|
|
name: "ruhroh",
|
|
hash: [32]byte{84, 131, 8, 59, 3, 244, 7, 36, 151, 161, 118, 68, 117, 183, 197, 177, 179, 232, 215, 10, 188, 48, 159, 195, 195, 140, 19, 204, 201, 182, 239, 235},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
// TODO(#7454) Remove this rebinding
|
|
tc := tc
|
|
// This is handled by boulder-ca, not the CA package.
|
|
if tc.defaultName == "" {
|
|
tc.defaultName = testCtx.defaultCertProfileName
|
|
}
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
tCA, err := NewCertificateAuthorityImpl(
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.boulderIssuers,
|
|
tc.defaultName,
|
|
tc.profileConfigs,
|
|
testCtx.lints,
|
|
nil,
|
|
testCtx.certExpiry,
|
|
testCtx.certBackdate,
|
|
testCtx.serialPrefix,
|
|
testCtx.maxNames,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
testCtx.stats,
|
|
testCtx.signatureCount,
|
|
testCtx.signErrorCount,
|
|
testCtx.fc,
|
|
)
|
|
|
|
if tc.expectedErrSubstr != "" {
|
|
test.AssertContains(t, err.Error(), tc.expectedErrSubstr)
|
|
test.AssertError(t, err, "No profile found during CA construction.")
|
|
} else {
|
|
test.AssertNotError(t, err, "Profiles should exist, but were not found")
|
|
}
|
|
|
|
if tc.expectedProfiles != nil {
|
|
test.AssertEquals(t, len(tc.expectedProfiles), len(tCA.certProfiles.profileByName))
|
|
}
|
|
|
|
for _, expected := range tc.expectedProfiles {
|
|
cpwid, ok := tCA.certProfiles.profileByName[expected.name]
|
|
test.Assert(t, ok, "Profile name was not found, but should have been")
|
|
test.AssertEquals(t, expected.hash, cpwid.hash)
|
|
|
|
if tc.name == "certificate profile hash changed mid-issuance" {
|
|
// This is an attempt to simulate the hash changing, but the
|
|
// name remaining the same on a CA node in the duration
|
|
// between CA1 sending capb.IssuePrecerticateResponse and
|
|
// before the RA calls
|
|
// capb.IssueCertificateForPrecertificate. We expect the
|
|
// receiving CA2 to error that the hash we expect could not
|
|
// be found in the map.
|
|
originalHash := cpwid.hash
|
|
cpwid.hash = [32]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 6}
|
|
test.AssertNotEquals(t, originalHash, cpwid.hash)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestECDSAAllowList(t *testing.T) {
|
|
t.Parallel()
|
|
req := &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID}
|
|
|
|
// With allowlist containing arbitraryRegID, issuance should come from ECDSA issuer.
|
|
regIDMap := makeRegIDsMap([]int64{arbitraryRegID})
|
|
ca, _ := issueCertificateSubTestSetup(t, &ECDSAAllowList{regIDMap})
|
|
result, err := ca.IssuePrecertificate(ctx, req)
|
|
test.AssertNotError(t, err, "Failed to issue certificate")
|
|
cert, err := x509.ParseCertificate(result.DER)
|
|
test.AssertNotError(t, err, "Certificate failed to parse")
|
|
test.AssertEquals(t, cert.SignatureAlgorithm, x509.ECDSAWithSHA384)
|
|
|
|
// With allowlist not containing arbitraryRegID, issuance should fall back to RSA issuer.
|
|
regIDMap = makeRegIDsMap([]int64{2002})
|
|
ca, _ = issueCertificateSubTestSetup(t, &ECDSAAllowList{regIDMap})
|
|
result, err = ca.IssuePrecertificate(ctx, req)
|
|
test.AssertNotError(t, err, "Failed to issue certificate")
|
|
cert, err = x509.ParseCertificate(result.DER)
|
|
test.AssertNotError(t, err, "Certificate failed to parse")
|
|
test.AssertEquals(t, cert.SignatureAlgorithm, x509.SHA256WithRSA)
|
|
|
|
// With empty allowlist but ECDSAForAll enabled, issuance should come from ECDSA issuer.
|
|
ca, _ = issueCertificateSubTestSetup(t, nil)
|
|
features.Set(features.Config{ECDSAForAll: true})
|
|
defer features.Reset()
|
|
result, err = ca.IssuePrecertificate(ctx, req)
|
|
test.AssertNotError(t, err, "Failed to issue certificate")
|
|
cert, err = x509.ParseCertificate(result.DER)
|
|
test.AssertNotError(t, err, "Certificate failed to parse")
|
|
test.AssertEquals(t, cert.SignatureAlgorithm, x509.ECDSAWithSHA384)
|
|
}
|
|
|
|
func TestInvalidCSRs(t *testing.T) {
|
|
t.Parallel()
|
|
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},
|
|
|
|
// Test that the CA rejects CSRs that have bad signature algorithms.
|
|
//
|
|
// CSR generated by Go:
|
|
// * Random public key -- 2048 bits long
|
|
// * CN = (none)
|
|
// * DNSNames = not-example.com, www.not-example.com, mail.not-example.com
|
|
// * Signature Algorithm: sha1WithRSAEncryption
|
|
{"RejectBadAlgorithm", "./testdata/bad_algorithm.der.csr", nil, "Issued a certificate based on a CSR with a bad signature algorithm.", 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},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
// TODO(#7454) Remove this rebinding
|
|
testCase := testCase
|
|
testCtx := setup(t)
|
|
sa := &mockSA{}
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.boulderIssuers,
|
|
testCtx.defaultCertProfileName,
|
|
testCtx.certProfiles,
|
|
testCtx.lints,
|
|
nil,
|
|
testCtx.certExpiry,
|
|
testCtx.certBackdate,
|
|
testCtx.serialPrefix,
|
|
testCtx.maxNames,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
testCtx.stats,
|
|
testCtx.signatureCount,
|
|
testCtx.signErrorCount,
|
|
testCtx.fc)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
serializedCSR := mustRead(testCase.csrPath)
|
|
issueReq := &capb.IssueCertificateRequest{Csr: serializedCSR, RegistrationID: arbitraryRegID}
|
|
_, err = ca.IssuePrecertificate(ctx, issueReq)
|
|
|
|
test.AssertErrorIs(t, err, testCase.errorType)
|
|
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "cert"}, 0)
|
|
|
|
test.AssertError(t, err, testCase.errorMessage)
|
|
if testCase.check != nil {
|
|
testCase.check(t, ca, sa)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRejectValidityTooLong(t *testing.T) {
|
|
t.Parallel()
|
|
testCtx := setup(t)
|
|
sa := &mockSA{}
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.boulderIssuers,
|
|
testCtx.defaultCertProfileName,
|
|
testCtx.certProfiles,
|
|
testCtx.lints,
|
|
nil,
|
|
testCtx.certExpiry,
|
|
testCtx.certBackdate,
|
|
testCtx.serialPrefix,
|
|
testCtx.maxNames,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
testCtx.stats,
|
|
nil,
|
|
nil,
|
|
testCtx.fc)
|
|
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: CNandSANCSR, RegistrationID: arbitraryRegID})
|
|
test.AssertError(t, err, "Cannot issue a certificate that expires after the intermediate certificate")
|
|
test.AssertErrorIs(t, err, berrors.InternalServer)
|
|
}
|
|
|
|
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}
|
|
mustStapleFeatureValue := []byte{0x30, 0x03, 0x02, 0x01, 0x05}
|
|
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) {
|
|
test.AssertMetricWithLabelsEquals(t, i.ca.signatureCount, prometheus.Labels{"purpose": "precertificate"}, 1)
|
|
test.AssertEquals(t, countMustStaple(t, i.cert), 1)
|
|
}
|
|
|
|
func issueCertificateSubTestUnknownExtension(t *testing.T, i *TestCertificateIssuance) {
|
|
test.AssertMetricWithLabelsEquals(t, i.ca.signatureCount, prometheus.Labels{"purpose": "precertificate"}, 1)
|
|
|
|
// NOTE: The hard-coded value here will have to change over time as Boulder
|
|
// adds or removes (unrequested/default) extensions in certificates.
|
|
expectedExtensionCount := 9
|
|
test.AssertEquals(t, len(i.cert.Extensions), expectedExtensionCount)
|
|
}
|
|
|
|
func issueCertificateSubTestCTPoisonExtension(t *testing.T, i *TestCertificateIssuance) {
|
|
test.AssertMetricWithLabelsEquals(t, i.ca.signatureCount, prometheus.Labels{"purpose": "precertificate"}, 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 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) {
|
|
t.Parallel()
|
|
testCtx := setup(t)
|
|
sa := &mockSA{}
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.boulderIssuers,
|
|
testCtx.defaultCertProfileName,
|
|
testCtx.certProfiles,
|
|
testCtx.lints,
|
|
nil,
|
|
testCtx.certExpiry,
|
|
testCtx.certBackdate,
|
|
testCtx.serialPrefix,
|
|
testCtx.maxNames,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
testCtx.stats,
|
|
testCtx.signatureCount,
|
|
testCtx.signErrorCount,
|
|
testCtx.fc)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
_, ok := ca.certProfiles.profileByName[ca.certProfiles.defaultName]
|
|
test.Assert(t, ok, "Certificate profile was expected to exist")
|
|
|
|
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")
|
|
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
|
|
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 0)
|
|
|
|
// Check for poison extension
|
|
poisonExtension := findExtension(parsedPrecert.Extensions, OIDExtensionCTPoison)
|
|
test.AssertNotNil(t, poisonExtension, "Couldn't find CTPoison extension")
|
|
test.AssertEquals(t, poisonExtension.Critical, true)
|
|
test.AssertDeepEquals(t, poisonExtension.Value, []byte{0x05, 0x00}) // ASN.1 DER NULL
|
|
|
|
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,
|
|
CertProfileHash: precert.CertProfileHash,
|
|
})
|
|
test.AssertNotError(t, err, "Failed to issue cert from precert")
|
|
parsedCert, err := x509.ParseCertificate(cert.Der)
|
|
test.AssertNotError(t, err, "Failed to parse cert")
|
|
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 1)
|
|
|
|
// Check for SCT list extension
|
|
sctListExtension := findExtension(parsedCert.Extensions, OIDExtensionSCTList)
|
|
test.AssertNotNil(t, sctListExtension, "Couldn't find SCTList extension")
|
|
test.AssertEquals(t, sctListExtension.Critical, false)
|
|
var rawValue []byte
|
|
_, err = asn1.Unmarshal(sctListExtension.Value, &rawValue)
|
|
test.AssertNotError(t, err, "Failed to unmarshal extension value")
|
|
sctList, err := 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)))
|
|
}
|
|
|
|
func TestIssueCertificateForPrecertificateWithSpecificCertificateProfile(t *testing.T) {
|
|
t.Parallel()
|
|
testCtx := setup(t)
|
|
sa := &mockSA{}
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.boulderIssuers,
|
|
testCtx.defaultCertProfileName,
|
|
testCtx.certProfiles,
|
|
testCtx.lints,
|
|
nil,
|
|
testCtx.certExpiry,
|
|
testCtx.certBackdate,
|
|
testCtx.serialPrefix,
|
|
testCtx.maxNames,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
testCtx.stats,
|
|
testCtx.signatureCount,
|
|
testCtx.signErrorCount,
|
|
testCtx.fc)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
selectedProfile := "longerLived"
|
|
certProfile, ok := ca.certProfiles.profileByName[selectedProfile]
|
|
test.Assert(t, ok, "Certificate profile was expected to exist")
|
|
|
|
issueReq := capb.IssueCertificateRequest{
|
|
Csr: CNandSANCSR,
|
|
RegistrationID: arbitraryRegID,
|
|
OrderID: 0,
|
|
CertProfileName: selectedProfile,
|
|
}
|
|
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")
|
|
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
|
|
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 0)
|
|
|
|
// Check for poison extension
|
|
poisonExtension := findExtension(parsedPrecert.Extensions, OIDExtensionCTPoison)
|
|
test.AssertNotNil(t, poisonExtension, "Couldn't find CTPoison extension")
|
|
test.AssertEquals(t, poisonExtension.Critical, true)
|
|
test.AssertDeepEquals(t, poisonExtension.Value, []byte{0x05, 0x00}) // ASN.1 DER NULL
|
|
|
|
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,
|
|
CertProfileHash: certProfile.hash[:],
|
|
})
|
|
test.AssertNotError(t, err, "Failed to issue cert from precert")
|
|
parsedCert, err := x509.ParseCertificate(cert.Der)
|
|
test.AssertNotError(t, err, "Failed to parse cert")
|
|
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 1)
|
|
|
|
// Check for SCT list extension
|
|
sctListExtension := findExtension(parsedCert.Extensions, OIDExtensionSCTList)
|
|
test.AssertNotNil(t, sctListExtension, "Couldn't find SCTList extension")
|
|
test.AssertEquals(t, sctListExtension.Critical, false)
|
|
var rawValue []byte
|
|
_, err = asn1.Unmarshal(sctListExtension.Value, &rawValue)
|
|
test.AssertNotError(t, err, "Failed to unmarshal extension value")
|
|
sctList, err := 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)))
|
|
}
|
|
|
|
// deserializeSCTList deserializes a list of SCTs.
|
|
// Forked from github.com/cloudflare/cfssl/helpers
|
|
func deserializeSCTList(serializedSCTList []byte) ([]ct.SignedCertificateTimestamp, error) {
|
|
var sctList ctx509.SignedCertificateTimestampList
|
|
rest, err := cttls.Unmarshal(serializedSCTList, &sctList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(rest) != 0 {
|
|
return nil, errors.New("serialized SCT list contained trailing garbage")
|
|
}
|
|
list := make([]ct.SignedCertificateTimestamp, len(sctList.SCTList))
|
|
for i, serializedSCT := range sctList.SCTList {
|
|
var sct ct.SignedCertificateTimestamp
|
|
rest, err := cttls.Unmarshal(serializedSCT.Val, &sct)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(rest) != 0 {
|
|
return nil, errors.New("serialized SCT contained trailing garbage")
|
|
}
|
|
list[i] = sct
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
// 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, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// getCertErrorSA always returns an error for GetCertificate
|
|
type getCertErrorSA struct {
|
|
mockSA
|
|
}
|
|
|
|
func (m *getCertErrorSA) GetCertificate(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) {
|
|
return nil, fmt.Errorf("i don't like it")
|
|
}
|
|
|
|
func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
|
|
t.Parallel()
|
|
testCtx := setup(t)
|
|
sa := &dupeSA{}
|
|
ca, err := NewCertificateAuthorityImpl(
|
|
sa,
|
|
testCtx.pa,
|
|
testCtx.boulderIssuers,
|
|
testCtx.defaultCertProfileName,
|
|
testCtx.certProfiles,
|
|
testCtx.lints,
|
|
nil,
|
|
testCtx.certExpiry,
|
|
testCtx.certBackdate,
|
|
testCtx.serialPrefix,
|
|
testCtx.maxNames,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
testCtx.stats,
|
|
testCtx.signatureCount,
|
|
testCtx.signErrorCount,
|
|
testCtx.fc)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
sctBytes, err := makeSCTs()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
selectedProfile := ca.certProfiles.defaultName
|
|
certProfile, ok := ca.certProfiles.profileByName[selectedProfile]
|
|
test.Assert(t, ok, "Certificate profile was expected to exist")
|
|
|
|
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, OrderID: 0}
|
|
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
|
|
test.AssertNotError(t, err, "Failed to issue precert")
|
|
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
|
|
_, err = ca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
|
|
DER: precert.DER,
|
|
SCTs: sctBytes,
|
|
RegistrationID: arbitraryRegID,
|
|
OrderID: 0,
|
|
CertProfileHash: certProfile.hash[:],
|
|
})
|
|
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)
|
|
}
|
|
// The success metric doesn't increase when a duplicate certificate issuance
|
|
// is attempted.
|
|
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 0)
|
|
|
|
// Now check what happens if there is an error (e.g. timeout) while checking
|
|
// for the duplicate.
|
|
errorsa := &getCertErrorSA{}
|
|
errorca, err := NewCertificateAuthorityImpl(
|
|
errorsa,
|
|
testCtx.pa,
|
|
testCtx.boulderIssuers,
|
|
testCtx.defaultCertProfileName,
|
|
testCtx.certProfiles,
|
|
testCtx.lints,
|
|
nil,
|
|
testCtx.certExpiry,
|
|
testCtx.certBackdate,
|
|
testCtx.serialPrefix,
|
|
testCtx.maxNames,
|
|
testCtx.keyPolicy,
|
|
testCtx.logger,
|
|
testCtx.stats,
|
|
testCtx.signatureCount,
|
|
testCtx.signErrorCount,
|
|
testCtx.fc)
|
|
test.AssertNotError(t, err, "Failed to create CA")
|
|
|
|
_, err = errorca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
|
|
DER: precert.DER,
|
|
SCTs: sctBytes,
|
|
RegistrationID: arbitraryRegID,
|
|
OrderID: 0,
|
|
CertProfileHash: certProfile.hash[:],
|
|
})
|
|
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)
|
|
}
|
|
// The success metric doesn't increase when a duplicate certificate issuance
|
|
// is attempted.
|
|
test.AssertMetricWithLabelsEquals(t, ca.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 0)
|
|
}
|
|
|
|
func TestGenerateSKID(t *testing.T) {
|
|
t.Parallel()
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
test.AssertNotError(t, err, "Error generating key")
|
|
|
|
sha256skid, err := generateSKID(key.Public())
|
|
test.AssertNotError(t, err, "Error generating SKID")
|
|
test.AssertEquals(t, len(sha256skid), 20)
|
|
test.AssertEquals(t, cap(sha256skid), 20)
|
|
features.Reset()
|
|
}
|