973 lines
33 KiB
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|