boulder/issuance/cert_test.go

973 lines
33 KiB
Go

package issuance
import (
"crypto"
"crypto/dsa"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"net"
"reflect"
"strings"
"testing"
"time"
ct "github.com/google/certificate-transparency-go"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/config"
"github.com/letsencrypt/boulder/ctpolicy/loglist"
"github.com/letsencrypt/boulder/linter"
"github.com/letsencrypt/boulder/test"
)
var (
goodSKID = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
)
func defaultProfile() *Profile {
p, _ := NewProfile(defaultProfileConfig())
return p
}
func TestGenerateValidity(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Date(2015, time.June, 04, 11, 04, 38, 0, time.UTC))
tests := []struct {
name string
backdate time.Duration
validity time.Duration
notBefore time.Time
notAfter time.Time
}{
{
name: "normal usage",
backdate: time.Hour, // 90% of one hour is 54 minutes
validity: 7 * 24 * time.Hour,
notBefore: time.Date(2015, time.June, 04, 10, 10, 38, 0, time.UTC),
notAfter: time.Date(2015, time.June, 11, 10, 10, 37, 0, time.UTC),
},
{
name: "zero backdate",
backdate: 0,
validity: 7 * 24 * time.Hour,
notBefore: time.Date(2015, time.June, 04, 11, 04, 38, 0, time.UTC),
notAfter: time.Date(2015, time.June, 11, 11, 04, 37, 0, time.UTC),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
p := Profile{maxBackdate: tc.backdate, maxValidity: tc.validity}
notBefore, notAfter := p.GenerateValidity(fc.Now())
test.AssertEquals(t, notBefore, tc.notBefore)
test.AssertEquals(t, notAfter, tc.notAfter)
})
}
}
func TestCRLURL(t *testing.T) {
issuer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, clock.NewFake())
if err != nil {
t.Fatalf("newIssuer: %s", err)
}
url := issuer.crlURL(4928)
want := "http://crl-url.example.org/4928.crl"
if url != want {
t.Errorf("crlURL(4928)=%s, want %s", url, want)
}
}
func TestRequestValid(t *testing.T) {
fc := clock.NewFake()
fc.Add(time.Hour * 24)
tests := []struct {
name string
issuer *Issuer
profile *Profile
request *IssuanceRequest
expectedError string
}{
{
name: "unsupported key type",
issuer: &Issuer{},
profile: &Profile{},
request: &IssuanceRequest{PublicKey: MarshalablePublicKey{&dsa.PublicKey{}}},
expectedError: "unsupported public key type",
},
{
name: "inactive (rsa)",
issuer: &Issuer{},
profile: &Profile{},
request: &IssuanceRequest{PublicKey: MarshalablePublicKey{&rsa.PublicKey{}}},
expectedError: "inactive issuer cannot issue precert",
},
{
name: "inactive (ecdsa)",
issuer: &Issuer{},
profile: &Profile{},
request: &IssuanceRequest{PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}}},
expectedError: "inactive issuer cannot issue precert",
},
{
name: "skid too short",
issuer: &Issuer{
active: true,
},
profile: &Profile{},
request: &IssuanceRequest{
PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}},
SubjectKeyId: []byte{0, 1, 2, 3, 4},
},
expectedError: "unexpected subject key ID length",
},
{
name: "both sct list and ct poison provided",
issuer: &Issuer{
active: true,
},
profile: &Profile{},
request: &IssuanceRequest{
PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}},
SubjectKeyId: goodSKID,
IncludeCTPoison: true,
sctList: []ct.SignedCertificateTimestamp{},
},
expectedError: "cannot include both ct poison and sct list extensions",
},
{
name: "negative validity",
issuer: &Issuer{
active: true,
},
profile: &Profile{},
request: &IssuanceRequest{
PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}},
SubjectKeyId: goodSKID,
NotBefore: fc.Now().Add(time.Hour),
NotAfter: fc.Now(),
},
expectedError: "NotAfter must be after NotBefore",
},
{
name: "validity larger than max",
issuer: &Issuer{
active: true,
},
profile: &Profile{
maxValidity: time.Minute,
},
request: &IssuanceRequest{
PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}},
SubjectKeyId: goodSKID,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
},
expectedError: "validity period is more than the maximum allowed period (1h0m0s>1m0s)",
},
{
name: "validity larger than max due to inclusivity",
issuer: &Issuer{
active: true,
},
profile: &Profile{
maxValidity: time.Hour,
},
request: &IssuanceRequest{
PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}},
SubjectKeyId: goodSKID,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
},
expectedError: "validity period is more than the maximum allowed period (1h0m1s>1h0m0s)",
},
{
name: "validity backdated more than max",
issuer: &Issuer{
active: true,
},
profile: &Profile{
maxValidity: time.Hour * 2,
maxBackdate: time.Hour,
},
request: &IssuanceRequest{
PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}},
SubjectKeyId: goodSKID,
NotBefore: fc.Now().Add(-time.Hour * 2),
NotAfter: fc.Now().Add(-time.Hour),
},
expectedError: "NotBefore is backdated more than the maximum allowed period (2h0m0s>1h0m0s)",
},
{
name: "validity is forward dated",
issuer: &Issuer{
active: true,
},
profile: &Profile{
maxValidity: time.Hour * 2,
maxBackdate: time.Hour,
},
request: &IssuanceRequest{
PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}},
SubjectKeyId: goodSKID,
NotBefore: fc.Now().Add(time.Hour),
NotAfter: fc.Now().Add(time.Hour * 2),
},
expectedError: "NotBefore is in the future",
},
{
name: "serial too short",
issuer: &Issuer{
active: true,
},
profile: &Profile{
maxValidity: time.Hour * 2,
},
request: &IssuanceRequest{
PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}},
SubjectKeyId: goodSKID,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{0, 1, 2, 3, 4, 5, 6, 7},
},
expectedError: "serial must be between 9 and 19 bytes",
},
{
name: "serial too long",
issuer: &Issuer{
active: true,
},
profile: &Profile{
maxValidity: time.Hour * 2,
},
request: &IssuanceRequest{
PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}},
SubjectKeyId: goodSKID,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
},
expectedError: "serial must be between 9 and 19 bytes",
},
{
name: "good with poison",
issuer: &Issuer{
active: true,
},
profile: &Profile{
maxValidity: time.Hour * 2,
},
request: &IssuanceRequest{
PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}},
SubjectKeyId: goodSKID,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
IncludeCTPoison: true,
},
},
{
name: "good with scts",
issuer: &Issuer{
active: true,
},
profile: &Profile{
maxValidity: time.Hour * 2,
},
request: &IssuanceRequest{
PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}},
SubjectKeyId: goodSKID,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
sctList: []ct.SignedCertificateTimestamp{},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.issuer.requestValid(fc, tc.profile, tc.request)
if err != nil {
if tc.expectedError == "" {
t.Errorf("failed with unexpected error: %s", err)
} else if tc.expectedError != err.Error() {
t.Errorf("failed with unexpected error, wanted: %q, got: %q", tc.expectedError, err.Error())
}
return
} else if tc.expectedError != "" {
t.Errorf("didn't fail, expected %q", tc.expectedError)
}
})
}
}
func TestGenerateTemplate(t *testing.T) {
issuer := &Issuer{
issuerURL: "http://issuer",
crlURLBase: "http://crl/",
sigAlg: x509.SHA256WithRSA,
}
actual := issuer.generateTemplate()
expected := &x509.Certificate{
BasicConstraintsValid: true,
SignatureAlgorithm: x509.SHA256WithRSA,
IssuingCertificateURL: []string{"http://issuer"},
Policies: []x509.OID{domainValidatedOID},
// These fields are only included if specified in the profile.
OCSPServer: nil,
CRLDistributionPoints: nil,
}
test.AssertDeepEquals(t, actual, expected)
}
func TestIssue(t *testing.T) {
for _, tc := range []struct {
name string
generateFunc func() (crypto.Signer, error)
ku x509.KeyUsage
}{
{
name: "RSA",
generateFunc: func() (crypto.Signer, error) {
return rsa.GenerateKey(rand.Reader, 2048)
},
ku: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
},
{
name: "ECDSA",
generateFunc: func() (crypto.Signer, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
},
ku: x509.KeyUsageDigitalSignature,
},
} {
t.Run(tc.name, func(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := tc.generateFunc()
test.AssertNotError(t, err, "failed to generate test key")
lintCertBytes, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
IPAddresses: []net.IP{net.ParseIP("128.101.101.101"), net.ParseIP("3fff:aaa:a:c0ff:ee:a:bad:deed")},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
IncludeCTPoison: true,
})
test.AssertNotError(t, err, "Prepare failed")
_, err = x509.ParseCertificate(lintCertBytes)
test.AssertNotError(t, err, "failed to parse certificate")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
err = cert.CheckSignatureFrom(issuerCert.Certificate)
test.AssertNotError(t, err, "signature validation failed")
test.AssertDeepEquals(t, cert.DNSNames, []string{"example.com"})
// net.ParseIP always returns a 16-byte address; IPv4 addresses are
// returned in IPv4-mapped IPv6 form. But RFC 5280, Sec. 4.2.1.6
// requires that IPv4 addresses be encoded as 4 bytes.
//
// The issuance pipeline calls x509.marshalSANs, which reduces IPv4
// addresses back to 4 bytes. Adding .To4() both allows this test to
// succeed, and covers this requirement.
test.AssertDeepEquals(t, cert.IPAddresses, []net.IP{net.ParseIP("128.101.101.101").To4(), net.ParseIP("3fff:aaa:a:c0ff:ee:a:bad:deed")})
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
test.AssertEquals(t, len(cert.Extensions), 10) // Constraints, KU, EKU, SKID, AKID, AIA, CRLDP, SAN, Policies, Poison
test.AssertEquals(t, cert.KeyUsage, tc.ku)
if len(cert.CRLDistributionPoints) != 1 || !strings.HasPrefix(cert.CRLDistributionPoints[0], "http://crl-url.example.org/") {
t.Errorf("want CRLDistributionPoints=[http://crl-url.example.org/x.crl], got %v", cert.CRLDistributionPoints)
}
})
}
}
func TestIssueDNSNamesOnly(t *testing.T) {
fc := clock.NewFake()
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
if err != nil {
t.Fatalf("newIssuer: %s", err)
}
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("ecdsa.GenerateKey: %s", err)
}
_, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
IncludeCTPoison: true,
})
if err != nil {
t.Fatalf("signer.Prepare: %s", err)
}
certBytes, err := signer.Issue(issuanceToken)
if err != nil {
t.Fatalf("signer.Issue: %s", err)
}
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
t.Fatalf("x509.ParseCertificate: %s", err)
}
if !reflect.DeepEqual(cert.DNSNames, []string{"example.com"}) {
t.Errorf("got DNSNames %s, wanted example.com", cert.DNSNames)
}
// BRs 7.1.2.7.12 requires iPAddress, if present, to contain an entry.
if cert.IPAddresses != nil {
t.Errorf("got IPAddresses %s, wanted nil", cert.IPAddresses)
}
}
func TestIssueIPAddressesOnly(t *testing.T) {
fc := clock.NewFake()
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
if err != nil {
t.Fatalf("newIssuer: %s", err)
}
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("ecdsa.GenerateKey: %s", err)
}
_, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
IPAddresses: []net.IP{net.ParseIP("128.101.101.101"), net.ParseIP("3fff:aaa:a:c0ff:ee:a:bad:deed")},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
IncludeCTPoison: true,
})
if err != nil {
t.Fatalf("signer.Prepare: %s", err)
}
certBytes, err := signer.Issue(issuanceToken)
if err != nil {
t.Fatalf("signer.Issue: %s", err)
}
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
t.Fatalf("x509.ParseCertificate: %s", err)
}
// BRs 7.1.2.7.12 requires dNSName, if present, to contain an entry.
if cert.DNSNames != nil {
t.Errorf("got DNSNames %s, wanted nil", cert.DNSNames)
}
if !reflect.DeepEqual(cert.IPAddresses, []net.IP{net.ParseIP("128.101.101.101").To4(), net.ParseIP("3fff:aaa:a:c0ff:ee:a:bad:deed")}) {
t.Errorf("got IPAddresses %s, wanted 128.101.101.101 (4-byte) & 3fff:aaa:a:c0ff:ee:a:bad:deed (16-byte)", cert.IPAddresses)
}
}
func TestIssueWithCRLDP(t *testing.T) {
fc := clock.NewFake()
issuerConfig := defaultIssuerConfig()
issuerConfig.CRLURLBase = "http://crls.example.net/"
issuerConfig.CRLShards = 999
signer, err := newIssuer(issuerConfig, issuerCert, issuerSigner, fc)
if err != nil {
t.Fatalf("newIssuer: %s", err)
}
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("ecdsa.GenerateKey: %s", err)
}
profile := defaultProfile()
profile.includeCRLDistributionPoints = true
_, issuanceToken, err := signer.Prepare(profile, &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
IncludeCTPoison: true,
})
if err != nil {
t.Fatalf("signer.Prepare: %s", err)
}
certBytes, err := signer.Issue(issuanceToken)
if err != nil {
t.Fatalf("signer.Issue: %s", err)
}
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
t.Fatalf("x509.ParseCertificate: %s", err)
}
// Because CRL shard is calculated deterministically from serial, we know which shard will be chosen.
expectedCRLDP := []string{"http://crls.example.net/919.crl"}
if !reflect.DeepEqual(cert.CRLDistributionPoints, expectedCRLDP) {
t.Errorf("CRLDP=%+v, want %+v", cert.CRLDistributionPoints, expectedCRLDP)
}
}
func TestIssueCommonName(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
prof := defaultProfileConfig()
prof.IgnoredLints = append(prof.IgnoredLints, "w_subject_common_name_included")
cnProfile, err := NewProfile(prof)
test.AssertNotError(t, err, "NewProfile failed")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
ir := &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com", "www.example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
IncludeCTPoison: true,
}
// In the default profile, the common name is allowed if requested.
ir.CommonName = "example.com"
_, issuanceToken, err := signer.Prepare(cnProfile, ir)
test.AssertNotError(t, err, "Prepare failed")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
test.AssertEquals(t, cert.Subject.CommonName, "example.com")
// But not including the common name should be acceptable as well.
ir.CommonName = ""
_, issuanceToken, err = signer.Prepare(cnProfile, ir)
test.AssertNotError(t, err, "Prepare failed")
certBytes, err = signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
cert, err = x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
test.AssertEquals(t, cert.Subject.CommonName, "")
// And the common name should be omitted if the profile is so configured.
ir.CommonName = "example.com"
cnProfile.omitCommonName = true
_, issuanceToken, err = signer.Prepare(cnProfile, ir)
test.AssertNotError(t, err, "Prepare failed")
certBytes, err = signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
cert, err = x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
test.AssertEquals(t, cert.Subject.CommonName, "")
}
func TestIssueOmissions(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
pc := defaultProfileConfig()
pc.OmitCommonName = true
pc.OmitKeyEncipherment = true
pc.OmitClientAuth = true
pc.OmitSKID = true
pc.IgnoredLints = []string{
// Reduce the lint ignores to just the minimal (SCT-related) set.
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
// Ignore the warning about *not* including the SubjectKeyIdentifier extension:
// zlint has both lints (one enforcing RFC5280, the other the BRs).
"w_ext_subject_key_identifier_missing_sub_cert",
}
prof, err := NewProfile(pc)
test.AssertNotError(t, err, "building test profile")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := rsa.GenerateKey(rand.Reader, 2048)
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(prof, &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
CommonName: "example.com",
IncludeCTPoison: true,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
})
test.AssertNotError(t, err, "Prepare failed")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
test.AssertEquals(t, cert.Subject.CommonName, "")
test.AssertEquals(t, cert.KeyUsage, x509.KeyUsageDigitalSignature)
test.AssertDeepEquals(t, cert.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
test.AssertEquals(t, len(cert.SubjectKeyId), 0)
}
func TestIssueCTPoison(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
IncludeCTPoison: true,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
})
test.AssertNotError(t, err, "Prepare failed")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate")
err = cert.CheckSignatureFrom(issuerCert.Certificate)
test.AssertNotError(t, err, "signature validation failed")
test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
test.AssertDeepEquals(t, cert.PublicKey, pk.Public())
test.AssertEquals(t, len(cert.Extensions), 10) // Constraints, KU, EKU, SKID, AKID, AIA, CRLDP, SAN, Policies, Poison
test.AssertDeepEquals(t, cert.Extensions[9], ctPoisonExt)
}
func mustDecodeB64(b string) []byte {
out, err := base64.StdEncoding.DecodeString(b)
if err != nil {
panic(err)
}
return out
}
func TestIssueSCTList(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
err := loglist.InitLintList("../test/ct-test-srv/log_list.json")
test.AssertNotError(t, err, "failed to load log list")
pc := defaultProfileConfig()
pc.IgnoredLints = []string{
// Only ignore the SKID lint, i.e., don't ignore the "missing SCT" lints.
"w_ext_subject_key_identifier_not_recommended_subscriber",
}
enforceSCTsProfile, err := NewProfile(pc)
test.AssertNotError(t, err, "NewProfile failed")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(enforceSCTsProfile, &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
IncludeCTPoison: true,
})
test.AssertNotError(t, err, "Prepare failed")
precertBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed")
precert, err := x509.ParseCertificate(precertBytes)
test.AssertNotError(t, err, "failed to parse certificate")
sctList := []ct.SignedCertificateTimestamp{
{
SCTVersion: ct.V1,
LogID: ct.LogID{KeyID: *(*[32]byte)(mustDecodeB64("OJiMlNA1mMOTLd/pI7q68npCDrlsQeFaqAwasPwEvQM="))},
},
{
SCTVersion: ct.V1,
LogID: ct.LogID{KeyID: *(*[32]byte)(mustDecodeB64("UtToynGEyMkkXDMQei8Ll54oMwWHI0IieDEKs12/Td4="))},
},
}
request2, err := RequestFromPrecert(precert, sctList)
test.AssertNotError(t, err, "generating request from precert")
_, issuanceToken2, err := signer.Prepare(enforceSCTsProfile, request2)
test.AssertNotError(t, err, "preparing final cert issuance")
finalCertBytes, err := signer.Issue(issuanceToken2)
test.AssertNotError(t, err, "Issue failed")
finalCert, err := x509.ParseCertificate(finalCertBytes)
test.AssertNotError(t, err, "failed to parse certificate")
err = finalCert.CheckSignatureFrom(issuerCert.Certificate)
test.AssertNotError(t, err, "signature validation failed")
test.AssertByteEquals(t, finalCert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
test.AssertDeepEquals(t, finalCert.PublicKey, pk.Public())
test.AssertEquals(t, len(finalCert.Extensions), 10) // Constraints, KU, EKU, SKID, AKID, AIA, CRLDP, SAN, Policies, Poison
test.AssertDeepEquals(t, finalCert.Extensions[9], pkix.Extension{
Id: sctListOID,
Value: []byte{
4, 100, 0, 98, 0, 47, 0, 56, 152, 140, 148, 208, 53, 152, 195, 147, 45,
223, 233, 35, 186, 186, 242, 122, 66, 14, 185, 108, 65, 225, 90, 168, 12,
26, 176, 252, 4, 189, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47,
0, 82, 212, 232, 202, 113, 132, 200, 201, 36, 92, 51, 16, 122, 47, 11,
151, 158, 40, 51, 5, 135, 35, 66, 34, 120, 49, 10, 179, 93, 191, 77, 222,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
},
})
}
func TestIssueBadLint(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
pc := defaultProfileConfig()
pc.IgnoredLints = []string{}
noSkipLintsProfile, err := NewProfile(pc)
test.AssertNotError(t, err, "NewProfile failed")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
_, _, err = signer.Prepare(noSkipLintsProfile, &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example-com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
IncludeCTPoison: true,
})
test.AssertError(t, err, "Prepare didn't fail")
test.AssertErrorIs(t, err, linter.ErrLinting)
test.AssertContains(t, err.Error(), "tbsCertificate linting failed: failed lint(s)")
}
func TestIssuanceToken(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
_, err = signer.Issue(&issuanceToken{})
test.AssertError(t, err, "expected issuance with a zero token to fail")
_, err = signer.Issue(nil)
test.AssertError(t, err, "expected issuance with a nil token to fail")
pk, err := rsa.GenerateKey(rand.Reader, 2048)
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
IncludeCTPoison: true,
})
test.AssertNotError(t, err, "expected Prepare to succeed")
_, err = signer.Issue(issuanceToken)
test.AssertNotError(t, err, "expected first issuance to succeed")
_, err = signer.Issue(issuanceToken)
test.AssertError(t, err, "expected second issuance with the same issuance token to fail")
test.AssertContains(t, err.Error(), "issuance token already redeemed")
_, issuanceToken, err = signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
IncludeCTPoison: true,
})
test.AssertNotError(t, err, "expected Prepare to succeed")
signer2, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
_, err = signer2.Issue(issuanceToken)
test.AssertError(t, err, "expected redeeming an issuance token with the wrong issuer to fail")
test.AssertContains(t, err.Error(), "wrong issuer")
}
func TestInvalidProfile(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
err := loglist.InitLintList("../test/ct-test-srv/log_list.json")
test.AssertNotError(t, err, "failed to load log list")
signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
_, _, err = signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
IncludeCTPoison: true,
precertDER: []byte{6, 6, 6},
})
test.AssertError(t, err, "Invalid IssuanceRequest")
_, _, err = signer.Prepare(defaultProfile(), &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
sctList: []ct.SignedCertificateTimestamp{
{
SCTVersion: ct.V1,
LogID: ct.LogID{KeyID: *(*[32]byte)(mustDecodeB64("OJiMlNA1mMOTLd/pI7q68npCDrlsQeFaqAwasPwEvQM="))},
},
},
precertDER: []byte{},
})
test.AssertError(t, err, "Invalid IssuanceRequest")
}
// Generate a precert from one profile and a final cert from another, and verify
// that the final cert errors out when linted because the lint cert doesn't
// corresponding with the precert.
func TestMismatchedProfiles(t *testing.T) {
fc := clock.NewFake()
fc.Set(time.Now())
err := loglist.InitLintList("../test/ct-test-srv/log_list.json")
test.AssertNotError(t, err, "failed to load log list")
issuer1, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
pc := defaultProfileConfig()
pc.IgnoredLints = append(pc.IgnoredLints, "w_subject_common_name_included")
cnProfile, err := NewProfile(pc)
test.AssertNotError(t, err, "NewProfile failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := issuer1.Prepare(cnProfile, &IssuanceRequest{
PublicKey: MarshalablePublicKey{pk.Public()},
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
CommonName: "example.com",
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
IncludeCTPoison: true,
})
test.AssertNotError(t, err, "making IssuanceRequest")
precertDER, err := issuer1.Issue(issuanceToken)
test.AssertNotError(t, err, "signing precert")
// Create a new profile that differs slightly (no common name)
pc = defaultProfileConfig()
pc.OmitCommonName = false
test.AssertNotError(t, err, "building test lint registry")
noCNProfile, err := NewProfile(pc)
test.AssertNotError(t, err, "NewProfile failed")
issuer2, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc)
test.AssertNotError(t, err, "NewIssuer failed")
sctList := []ct.SignedCertificateTimestamp{
{
SCTVersion: ct.V1,
LogID: ct.LogID{KeyID: *(*[32]byte)(mustDecodeB64("OJiMlNA1mMOTLd/pI7q68npCDrlsQeFaqAwasPwEvQM="))},
},
{
SCTVersion: ct.V1,
LogID: ct.LogID{KeyID: *(*[32]byte)(mustDecodeB64("UtToynGEyMkkXDMQei8Ll54oMwWHI0IieDEKs12/Td4="))},
},
}
precert, err := x509.ParseCertificate(precertDER)
test.AssertNotError(t, err, "parsing precert")
request2, err := RequestFromPrecert(precert, sctList)
test.AssertNotError(t, err, "RequestFromPrecert")
request2.CommonName = ""
_, _, err = issuer2.Prepare(noCNProfile, request2)
test.AssertError(t, err, "preparing final cert issuance")
test.AssertContains(t, err.Error(), "precert does not correspond to linted final cert")
}
func TestNewProfile(t *testing.T) {
for _, tc := range []struct {
name string
config ProfileConfig
wantErr string
}{
{
name: "happy path",
config: ProfileConfig{
MaxValidityBackdate: config.Duration{Duration: 1 * time.Hour},
MaxValidityPeriod: config.Duration{Duration: 90 * 24 * time.Hour},
IncludeCRLDistributionPoints: true,
},
},
{
name: "large backdate",
config: ProfileConfig{
MaxValidityBackdate: config.Duration{Duration: 24 * time.Hour},
MaxValidityPeriod: config.Duration{Duration: 90 * 24 * time.Hour},
},
wantErr: "backdate \"24h0m0s\" is too large",
},
{
name: "large validity",
config: ProfileConfig{
MaxValidityBackdate: config.Duration{Duration: 1 * time.Hour},
MaxValidityPeriod: config.Duration{Duration: 397 * 24 * time.Hour},
},
wantErr: "validity period \"9528h0m0s\" is too large",
},
{
name: "no revocation info",
config: ProfileConfig{
MaxValidityBackdate: config.Duration{Duration: 1 * time.Hour},
MaxValidityPeriod: config.Duration{Duration: 90 * 24 * time.Hour},
IncludeCRLDistributionPoints: false,
},
wantErr: "revocation mechanism must be included",
},
} {
t.Run(tc.name, func(t *testing.T) {
gotProfile, gotErr := NewProfile(&tc.config)
if tc.wantErr != "" {
if gotErr == nil {
t.Errorf("NewProfile(%#v) = %#v, but want err %q", tc.config, gotProfile, tc.wantErr)
}
if !strings.Contains(gotErr.Error(), tc.wantErr) {
t.Errorf("NewProfile(%#v) = %q, but want %q", tc.config, gotErr, tc.wantErr)
}
} else {
if gotErr != nil {
t.Errorf("NewProfile(%#v) = %q, but want no error", tc.config, gotErr)
}
}
})
}
}