703 lines
25 KiB
Go
703 lines
25 KiB
Go
package notmain
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"database/sql"
|
|
"encoding/asn1"
|
|
"encoding/pem"
|
|
"errors"
|
|
"log"
|
|
"math/big"
|
|
mrand "math/rand/v2"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jmhodges/clock"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
"github.com/letsencrypt/boulder/core"
|
|
corepb "github.com/letsencrypt/boulder/core/proto"
|
|
"github.com/letsencrypt/boulder/ctpolicy/loglist"
|
|
"github.com/letsencrypt/boulder/goodkey"
|
|
"github.com/letsencrypt/boulder/goodkey/sagoodkey"
|
|
"github.com/letsencrypt/boulder/identifier"
|
|
"github.com/letsencrypt/boulder/linter"
|
|
blog "github.com/letsencrypt/boulder/log"
|
|
"github.com/letsencrypt/boulder/metrics"
|
|
"github.com/letsencrypt/boulder/policy"
|
|
"github.com/letsencrypt/boulder/sa"
|
|
sapb "github.com/letsencrypt/boulder/sa/proto"
|
|
"github.com/letsencrypt/boulder/sa/satest"
|
|
"github.com/letsencrypt/boulder/test"
|
|
isa "github.com/letsencrypt/boulder/test/inmem/sa"
|
|
"github.com/letsencrypt/boulder/test/vars"
|
|
)
|
|
|
|
var (
|
|
testValidityDuration = 24 * 90 * time.Hour
|
|
testValidityDurations = map[time.Duration]bool{testValidityDuration: true}
|
|
pa *policy.AuthorityImpl
|
|
kp goodkey.KeyPolicy
|
|
)
|
|
|
|
func init() {
|
|
var err error
|
|
pa, err = policy.New(
|
|
map[identifier.IdentifierType]bool{identifier.TypeDNS: true, identifier.TypeIP: true},
|
|
map[core.AcmeChallenge]bool{},
|
|
blog.NewMock())
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
err = pa.LoadHostnamePolicyFile("../../test/hostname-policy.yaml")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
kp, err = sagoodkey.NewPolicy(nil, nil)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func BenchmarkCheckCert(b *testing.B) {
|
|
checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
|
|
testKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
expiry := time.Now().AddDate(0, 0, 1)
|
|
serial := big.NewInt(1337)
|
|
rawCert := x509.Certificate{
|
|
Subject: pkix.Name{
|
|
CommonName: "example.com",
|
|
},
|
|
NotAfter: expiry,
|
|
DNSNames: []string{"example-a.com"},
|
|
SerialNumber: serial,
|
|
}
|
|
certDer, _ := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey)
|
|
cert := &corepb.Certificate{
|
|
Serial: core.SerialToString(serial),
|
|
Digest: core.Fingerprint256(certDer),
|
|
Der: certDer,
|
|
Issued: timestamppb.New(time.Now()),
|
|
Expires: timestamppb.New(expiry),
|
|
}
|
|
b.ResetTimer()
|
|
for range b.N {
|
|
checker.checkCert(context.Background(), cert)
|
|
}
|
|
}
|
|
|
|
func TestCheckWildcardCert(t *testing.T) {
|
|
saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
|
|
test.AssertNotError(t, err, "Couldn't connect to database")
|
|
saCleanup := test.ResetBoulderTestDatabase(t)
|
|
defer func() {
|
|
saCleanup()
|
|
}()
|
|
|
|
testKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
|
fc := clock.NewFake()
|
|
checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
|
|
issued := checker.clock.Now().Add(-time.Minute)
|
|
goodExpiry := issued.Add(testValidityDuration - time.Second)
|
|
serial := big.NewInt(1337)
|
|
|
|
wildcardCert := x509.Certificate{
|
|
Subject: pkix.Name{
|
|
CommonName: "*.example.com",
|
|
},
|
|
NotBefore: issued,
|
|
NotAfter: goodExpiry,
|
|
DNSNames: []string{"*.example.com"},
|
|
SerialNumber: serial,
|
|
BasicConstraintsValid: true,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
OCSPServer: []string{"http://example.com/ocsp"},
|
|
IssuingCertificateURL: []string{"http://example.com/cert"},
|
|
}
|
|
wildcardCertDer, err := x509.CreateCertificate(rand.Reader, &wildcardCert, &wildcardCert, &testKey.PublicKey, testKey)
|
|
test.AssertNotError(t, err, "Couldn't create certificate")
|
|
parsed, err := x509.ParseCertificate(wildcardCertDer)
|
|
test.AssertNotError(t, err, "Couldn't parse created certificate")
|
|
cert := &corepb.Certificate{
|
|
Serial: core.SerialToString(serial),
|
|
Digest: core.Fingerprint256(wildcardCertDer),
|
|
Expires: timestamppb.New(parsed.NotAfter),
|
|
Issued: timestamppb.New(parsed.NotBefore),
|
|
Der: wildcardCertDer,
|
|
}
|
|
_, problems := checker.checkCert(context.Background(), cert)
|
|
for _, p := range problems {
|
|
t.Error(p)
|
|
}
|
|
}
|
|
|
|
func TestCheckCertReturnsSANs(t *testing.T) {
|
|
saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
|
|
test.AssertNotError(t, err, "Couldn't connect to database")
|
|
saCleanup := test.ResetBoulderTestDatabase(t)
|
|
defer func() {
|
|
saCleanup()
|
|
}()
|
|
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
|
|
|
|
certPEM, err := os.ReadFile("testdata/quite_invalid.pem")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
block, _ := pem.Decode(certPEM)
|
|
if block == nil {
|
|
t.Fatal("failed to parse cert PEM")
|
|
}
|
|
|
|
cert := &corepb.Certificate{
|
|
Serial: "00000000000",
|
|
Digest: core.Fingerprint256(block.Bytes),
|
|
Expires: timestamppb.New(time.Now().Add(time.Hour)),
|
|
Issued: timestamppb.New(time.Now()),
|
|
Der: block.Bytes,
|
|
}
|
|
|
|
names, problems := checker.checkCert(context.Background(), cert)
|
|
if !slices.Equal(names, []string{"quite_invalid.com", "al--so--wr--ong.com", "127.0.0.1"}) {
|
|
t.Errorf("didn't get expected DNS names. other problems: %s", strings.Join(problems, "\n"))
|
|
}
|
|
}
|
|
|
|
type keyGen interface {
|
|
genKey() (crypto.Signer, error)
|
|
}
|
|
|
|
type ecP256Generator struct{}
|
|
|
|
func (*ecP256Generator) genKey() (crypto.Signer, error) {
|
|
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
}
|
|
|
|
type rsa2048Generator struct{}
|
|
|
|
func (*rsa2048Generator) genKey() (crypto.Signer, error) {
|
|
return rsa.GenerateKey(rand.Reader, 2048)
|
|
}
|
|
|
|
func TestCheckCert(t *testing.T) {
|
|
saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
|
|
test.AssertNotError(t, err, "Couldn't connect to database")
|
|
saCleanup := test.ResetBoulderTestDatabase(t)
|
|
defer func() {
|
|
saCleanup()
|
|
}()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
key keyGen
|
|
}{
|
|
{
|
|
name: "RSA 2048 key",
|
|
key: &rsa2048Generator{},
|
|
},
|
|
{
|
|
name: "ECDSA P256 key",
|
|
key: &ecP256Generator{},
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
testKey, _ := tc.key.genKey()
|
|
|
|
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
|
|
|
|
// Create a RFC 7633 OCSP Must Staple Extension.
|
|
// OID 1.3.6.1.5.5.7.1.24
|
|
ocspMustStaple := pkix.Extension{
|
|
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24},
|
|
Critical: false,
|
|
Value: []uint8{0x30, 0x3, 0x2, 0x1, 0x5},
|
|
}
|
|
|
|
// Create a made up PKIX extension
|
|
imaginaryExtension := pkix.Extension{
|
|
Id: asn1.ObjectIdentifier{1, 3, 3, 7},
|
|
Critical: false,
|
|
Value: []uint8{0xC0, 0xFF, 0xEE},
|
|
}
|
|
|
|
issued := checker.clock.Now().Add(-time.Minute)
|
|
goodExpiry := issued.Add(testValidityDuration - time.Second)
|
|
serial := big.NewInt(1337)
|
|
longName := "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeexample.com"
|
|
rawCert := x509.Certificate{
|
|
Subject: pkix.Name{
|
|
CommonName: longName,
|
|
},
|
|
NotBefore: issued,
|
|
NotAfter: goodExpiry.AddDate(0, 0, 1), // Period too long
|
|
DNSNames: []string{
|
|
"example-a.com",
|
|
"foodnotbombs.mil",
|
|
// `dev-myqnapcloud.com` is included because it is an exact private
|
|
// entry on the public suffix list
|
|
"dev-myqnapcloud.com",
|
|
// don't include longName in the SANs, so the unique CN gets flagged
|
|
},
|
|
SerialNumber: serial,
|
|
BasicConstraintsValid: false,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
OCSPServer: []string{"http://example.com/ocsp"},
|
|
IssuingCertificateURL: []string{"http://example.com/cert"},
|
|
ExtraExtensions: []pkix.Extension{ocspMustStaple, imaginaryExtension},
|
|
}
|
|
brokenCertDer, err := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, testKey.Public(), testKey)
|
|
test.AssertNotError(t, err, "Couldn't create certificate")
|
|
// Problems
|
|
// Digest doesn't match
|
|
// Serial doesn't match
|
|
// Expiry doesn't match
|
|
// Issued doesn't match
|
|
cert := &corepb.Certificate{
|
|
Serial: "8485f2687eba29ad455ae4e31c8679206fec",
|
|
Der: brokenCertDer,
|
|
Issued: timestamppb.New(issued.Add(12 * time.Hour)),
|
|
Expires: timestamppb.New(goodExpiry.AddDate(0, 0, 2)), // Expiration doesn't match
|
|
}
|
|
|
|
_, problems := checker.checkCert(context.Background(), cert)
|
|
|
|
problemsMap := map[string]int{
|
|
"Stored digest doesn't match certificate digest": 1,
|
|
"Stored serial doesn't match certificate serial": 1,
|
|
"Stored expiration doesn't match certificate NotAfter": 1,
|
|
"Certificate doesn't have basic constraints set": 1,
|
|
"Certificate has unacceptable validity period": 1,
|
|
"Stored issuance date is outside of 6 hour window of certificate NotBefore": 1,
|
|
"Certificate has incorrect key usage extensions": 1,
|
|
"Certificate has common name >64 characters long (65)": 1,
|
|
"Certificate contains an unexpected extension: 1.3.3.7": 1,
|
|
"Certificate Common Name does not appear in Subject Alternative Names: \"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeexample.com\" !< [example-a.com foodnotbombs.mil dev-myqnapcloud.com]": 1,
|
|
}
|
|
for _, p := range problems {
|
|
_, ok := problemsMap[p]
|
|
if !ok {
|
|
t.Errorf("Found unexpected problem '%s'.", p)
|
|
}
|
|
delete(problemsMap, p)
|
|
}
|
|
for k := range problemsMap {
|
|
t.Errorf("Expected problem but didn't find '%s' in problems: %q.", k, problems)
|
|
}
|
|
|
|
// Same settings as above, but the stored serial number in the DB is invalid.
|
|
cert.Serial = "not valid"
|
|
_, problems = checker.checkCert(context.Background(), cert)
|
|
foundInvalidSerialProblem := false
|
|
for _, p := range problems {
|
|
if p == "Stored serial is invalid" {
|
|
foundInvalidSerialProblem = true
|
|
}
|
|
}
|
|
test.Assert(t, foundInvalidSerialProblem, "Invalid certificate serial number in DB did not trigger problem.")
|
|
|
|
// Fix the problems
|
|
rawCert.Subject.CommonName = "example-a.com"
|
|
rawCert.DNSNames = []string{"example-a.com"}
|
|
rawCert.NotAfter = goodExpiry
|
|
rawCert.BasicConstraintsValid = true
|
|
rawCert.ExtraExtensions = []pkix.Extension{ocspMustStaple}
|
|
rawCert.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
|
|
goodCertDer, err := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, testKey.Public(), testKey)
|
|
test.AssertNotError(t, err, "Couldn't create certificate")
|
|
parsed, err := x509.ParseCertificate(goodCertDer)
|
|
test.AssertNotError(t, err, "Couldn't parse created certificate")
|
|
cert.Serial = core.SerialToString(serial)
|
|
cert.Digest = core.Fingerprint256(goodCertDer)
|
|
cert.Der = goodCertDer
|
|
cert.Expires = timestamppb.New(parsed.NotAfter)
|
|
cert.Issued = timestamppb.New(parsed.NotBefore)
|
|
_, problems = checker.checkCert(context.Background(), cert)
|
|
test.AssertEquals(t, len(problems), 0)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetAndProcessCerts(t *testing.T) {
|
|
saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
|
|
test.AssertNotError(t, err, "Couldn't connect to database")
|
|
fc := clock.NewFake()
|
|
fc.Set(fc.Now().Add(time.Hour))
|
|
|
|
checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
|
|
sa, err := sa.NewSQLStorageAuthority(saDbMap, saDbMap, nil, 1, 0, fc, blog.NewMock(), metrics.NoopRegisterer)
|
|
test.AssertNotError(t, err, "Couldn't create SA to insert certificates")
|
|
saCleanUp := test.ResetBoulderTestDatabase(t)
|
|
defer func() {
|
|
saCleanUp()
|
|
}()
|
|
|
|
testKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
// Problems
|
|
// Expiry period is too long
|
|
rawCert := x509.Certificate{
|
|
Subject: pkix.Name{
|
|
CommonName: "not-blacklisted.com",
|
|
},
|
|
BasicConstraintsValid: true,
|
|
DNSNames: []string{"not-blacklisted.com"},
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
|
}
|
|
reg := satest.CreateWorkingRegistration(t, isa.SA{Impl: sa})
|
|
test.AssertNotError(t, err, "Couldn't create registration")
|
|
for range 5 {
|
|
rawCert.SerialNumber = big.NewInt(mrand.Int64())
|
|
certDER, err := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey)
|
|
test.AssertNotError(t, err, "Couldn't create certificate")
|
|
_, err = sa.AddCertificate(context.Background(), &sapb.AddCertificateRequest{
|
|
Der: certDER,
|
|
RegID: reg.Id,
|
|
Issued: timestamppb.New(fc.Now()),
|
|
})
|
|
test.AssertNotError(t, err, "Couldn't add certificate")
|
|
}
|
|
|
|
batchSize = 2
|
|
err = checker.getCerts(context.Background())
|
|
test.AssertNotError(t, err, "Failed to retrieve certificates")
|
|
test.AssertEquals(t, len(checker.certs), 5)
|
|
wg := new(sync.WaitGroup)
|
|
wg.Add(1)
|
|
checker.processCerts(context.Background(), wg, false)
|
|
test.AssertEquals(t, checker.issuedReport.BadCerts, int64(5))
|
|
test.AssertEquals(t, len(checker.issuedReport.Entries), 5)
|
|
}
|
|
|
|
// mismatchedCountDB is a certDB implementation for `getCerts` that returns one
|
|
// high value when asked how many rows there are, and then returns nothing when
|
|
// asked for the actual rows.
|
|
type mismatchedCountDB struct{}
|
|
|
|
// `getCerts` calls `SelectInt` first to determine how many rows there are
|
|
// matching the `getCertsCountQuery` criteria. For this mock we return
|
|
// a non-zero number
|
|
func (db mismatchedCountDB) SelectNullInt(_ context.Context, _ string, _ ...interface{}) (sql.NullInt64, error) {
|
|
return sql.NullInt64{
|
|
Int64: 99999,
|
|
Valid: true,
|
|
},
|
|
nil
|
|
}
|
|
|
|
// `getCerts` then calls `Select` to retrieve the Certificate rows. We pull
|
|
// a dastardly switch-a-roo here and return an empty set
|
|
func (db mismatchedCountDB) Select(_ context.Context, output interface{}, _ string, _ ...interface{}) ([]interface{}, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (db mismatchedCountDB) SelectOne(_ context.Context, _ interface{}, _ string, _ ...interface{}) error {
|
|
return errors.New("unimplemented")
|
|
}
|
|
|
|
/*
|
|
* In Boulder #2004[0] we identified that there is a race in `getCerts`
|
|
* between the first call to `SelectOne` to identify how many rows there are,
|
|
* and the subsequent call to `Select` to get the actual rows in batches. This
|
|
* manifests in an index out of range panic where the cert checker thinks there
|
|
* are more rows than there are and indexes into an empty set of certificates to
|
|
* update the lastSerial field of the query `args`. This has been fixed by
|
|
* adding a len() check in the inner `getCerts` loop that processes the certs
|
|
* one batch at a time.
|
|
*
|
|
* TestGetCertsEmptyResults tests the fix remains in place by using a mock that
|
|
* exploits this corner case deliberately. The `mismatchedCountDB` mock (defined
|
|
* above) will return a high count for the `SelectOne` call, but an empty slice
|
|
* for the `Select` call. Without the fix in place this reliably produced the
|
|
* "index out of range" panic from #2004. With the fix in place the test passes.
|
|
*
|
|
* 0: https://github.com/letsencrypt/boulder/issues/2004
|
|
*/
|
|
func TestGetCertsEmptyResults(t *testing.T) {
|
|
saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
|
|
test.AssertNotError(t, err, "Couldn't connect to database")
|
|
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
|
|
checker.dbMap = mismatchedCountDB{}
|
|
|
|
batchSize = 3
|
|
err = checker.getCerts(context.Background())
|
|
test.AssertNotError(t, err, "Failed to retrieve certificates")
|
|
}
|
|
|
|
// emptyDB is a certDB object with methods used for testing that 'null'
|
|
// responses received from the database are handled properly.
|
|
type emptyDB struct {
|
|
certDB
|
|
}
|
|
|
|
// SelectNullInt is a method that returns a false sql.NullInt64 struct to
|
|
// mock a null DB response
|
|
func (db emptyDB) SelectNullInt(_ context.Context, _ string, _ ...interface{}) (sql.NullInt64, error) {
|
|
return sql.NullInt64{Valid: false},
|
|
nil
|
|
}
|
|
|
|
// TestGetCertsNullResults tests that a null response from the database will
|
|
// be handled properly. It uses the emptyDB above to mock the response
|
|
// expected if the DB finds no certificates to match the SELECT query and
|
|
// should return an error.
|
|
func TestGetCertsNullResults(t *testing.T) {
|
|
checker := newChecker(emptyDB{}, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
|
|
|
|
err := checker.getCerts(context.Background())
|
|
test.AssertError(t, err, "Should have gotten error from empty DB")
|
|
if !strings.Contains(err.Error(), "no rows found for certificates issued between") {
|
|
t.Errorf("expected error to contain 'no rows found for certificates issued between', got '%s'", err.Error())
|
|
}
|
|
}
|
|
|
|
// lateDB is a certDB object that helps with TestGetCertsLate.
|
|
// It pretends to contain a single cert issued at the given time.
|
|
type lateDB struct {
|
|
issuedTime time.Time
|
|
selectedACert bool
|
|
}
|
|
|
|
// SelectNullInt is a method that returns a false sql.NullInt64 struct to
|
|
// mock a null DB response
|
|
func (db *lateDB) SelectNullInt(_ context.Context, _ string, args ...interface{}) (sql.NullInt64, error) {
|
|
args2 := args[0].(map[string]interface{})
|
|
begin := args2["begin"].(time.Time)
|
|
end := args2["end"].(time.Time)
|
|
if begin.Compare(db.issuedTime) < 0 && end.Compare(db.issuedTime) > 0 {
|
|
return sql.NullInt64{Int64: 23, Valid: true}, nil
|
|
}
|
|
return sql.NullInt64{Valid: false}, nil
|
|
}
|
|
|
|
func (db *lateDB) Select(_ context.Context, output interface{}, _ string, args ...interface{}) ([]interface{}, error) {
|
|
db.selectedACert = true
|
|
// For expediency we respond with an empty list of certificates; the checker will treat this as if it's
|
|
// reached the end of the list of certificates to process.
|
|
return nil, nil
|
|
}
|
|
|
|
func (db *lateDB) SelectOne(_ context.Context, _ interface{}, _ string, _ ...interface{}) error {
|
|
return nil
|
|
}
|
|
|
|
// TestGetCertsLate checks for correct behavior when certificates exist only late in the provided window.
|
|
func TestGetCertsLate(t *testing.T) {
|
|
clk := clock.NewFake()
|
|
db := &lateDB{issuedTime: clk.Now().Add(-time.Hour)}
|
|
checkPeriod := 24 * time.Hour
|
|
checker := newChecker(db, clk, pa, kp, checkPeriod, testValidityDurations, nil, blog.NewMock())
|
|
|
|
err := checker.getCerts(context.Background())
|
|
test.AssertNotError(t, err, "getting certs")
|
|
|
|
if !db.selectedACert {
|
|
t.Errorf("checker never selected a certificate after getting a MIN(id)")
|
|
}
|
|
}
|
|
|
|
func TestSaveReport(t *testing.T) {
|
|
r := report{
|
|
begin: time.Time{},
|
|
end: time.Time{},
|
|
GoodCerts: 2,
|
|
BadCerts: 1,
|
|
Entries: map[string]reportEntry{
|
|
"020000000000004b475da49b91da5c17": {
|
|
Valid: true,
|
|
},
|
|
"020000000000004d1613e581432cba7e": {
|
|
Valid: true,
|
|
},
|
|
"020000000000004e402bc21035c6634a": {
|
|
Valid: false,
|
|
Problems: []string{"None really..."},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := r.dump()
|
|
test.AssertNotError(t, err, "Failed to dump results")
|
|
}
|
|
|
|
func TestIsForbiddenDomain(t *testing.T) {
|
|
// Note: These testcases are not an exhaustive representation of domains
|
|
// Boulder won't issue for, but are instead testing the defense-in-depth
|
|
// `isForbiddenDomain` function called *after* the PA has vetted the name
|
|
// against the complex hostname policy file.
|
|
testcases := []struct {
|
|
Name string
|
|
Expected bool
|
|
}{
|
|
/* Expected to be forbidden test cases */
|
|
// Whitespace only
|
|
{Name: "", Expected: true},
|
|
{Name: " ", Expected: true},
|
|
// Anything .local
|
|
{Name: "yokel.local", Expected: true},
|
|
{Name: "off.on.remote.local", Expected: true},
|
|
{Name: ".local", Expected: true},
|
|
// Localhost is verboten
|
|
{Name: "localhost", Expected: true},
|
|
// Anything .localhost
|
|
{Name: ".localhost", Expected: true},
|
|
{Name: "local.localhost", Expected: true},
|
|
{Name: "extremely.local.localhost", Expected: true},
|
|
|
|
/* Expected to be allowed test cases */
|
|
{Name: "ok.computer.com", Expected: false},
|
|
{Name: "ok.millionaires", Expected: false},
|
|
{Name: "ok.milly", Expected: false},
|
|
{Name: "ok", Expected: false},
|
|
{Name: "nearby.locals", Expected: false},
|
|
{Name: "yocalhost", Expected: false},
|
|
{Name: "jokes.yocalhost", Expected: false},
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
result, _ := isForbiddenDomain(tc.Name)
|
|
test.AssertEquals(t, result, tc.Expected)
|
|
}
|
|
}
|
|
|
|
func TestIgnoredLint(t *testing.T) {
|
|
saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
|
|
test.AssertNotError(t, err, "Couldn't connect to database")
|
|
saCleanup := test.ResetBoulderTestDatabase(t)
|
|
defer func() {
|
|
saCleanup()
|
|
}()
|
|
|
|
err = loglist.InitLintList("../../test/ct-test-srv/log_list.json")
|
|
test.AssertNotError(t, err, "failed to load ct log list")
|
|
testKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
|
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
|
|
serial := big.NewInt(1337)
|
|
|
|
x509OID, err := x509.OIDFromInts([]uint64{1, 2, 3})
|
|
test.AssertNotError(t, err, "failed to create x509.OID")
|
|
|
|
template := &x509.Certificate{
|
|
Subject: pkix.Name{
|
|
CommonName: "CPU's Cool CA",
|
|
},
|
|
SerialNumber: serial,
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(testValidityDuration - time.Second),
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
|
Policies: []x509.OID{x509OID},
|
|
BasicConstraintsValid: true,
|
|
IsCA: true,
|
|
IssuingCertificateURL: []string{"http://aia.example.org"},
|
|
SubjectKeyId: []byte("foobar"),
|
|
}
|
|
|
|
// Create a self-signed issuer certificate to use
|
|
issuerDer, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey)
|
|
test.AssertNotError(t, err, "failed to create self-signed issuer cert")
|
|
issuerCert, err := x509.ParseCertificate(issuerDer)
|
|
test.AssertNotError(t, err, "failed to parse self-signed issuer cert")
|
|
|
|
// Reconfigure the template for an EE cert with a Subj. CN
|
|
serial = big.NewInt(1338)
|
|
template.SerialNumber = serial
|
|
template.Subject.CommonName = "zombo.com"
|
|
template.DNSNames = []string{"zombo.com"}
|
|
template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
|
|
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
|
|
template.IsCA = false
|
|
|
|
subjectCertDer, err := x509.CreateCertificate(rand.Reader, template, issuerCert, testKey.Public(), testKey)
|
|
test.AssertNotError(t, err, "failed to create EE cert")
|
|
subjectCert, err := x509.ParseCertificate(subjectCertDer)
|
|
test.AssertNotError(t, err, "failed to parse EE cert")
|
|
|
|
cert := &corepb.Certificate{
|
|
Serial: core.SerialToString(serial),
|
|
Der: subjectCertDer,
|
|
Digest: core.Fingerprint256(subjectCertDer),
|
|
Issued: timestamppb.New(subjectCert.NotBefore),
|
|
Expires: timestamppb.New(subjectCert.NotAfter),
|
|
}
|
|
|
|
// Without any ignored lints we expect several errors and warnings about SCTs,
|
|
// the common name, and the subject key identifier extension.
|
|
expectedProblems := []string{
|
|
"zlint warn: w_subject_common_name_included",
|
|
"zlint warn: w_ext_subject_key_identifier_not_recommended_subscriber",
|
|
"zlint info: w_ct_sct_policy_count_unsatisfied Certificate had 0 embedded SCTs. Browser policy may require 2 for this certificate.",
|
|
"zlint error: e_scts_from_same_operator Certificate had too few embedded SCTs; browser policy requires 2.",
|
|
}
|
|
slices.Sort(expectedProblems)
|
|
|
|
// Check the certificate with a nil ignore map. This should return the
|
|
// expected zlint problems.
|
|
_, problems := checker.checkCert(context.Background(), cert)
|
|
slices.Sort(problems)
|
|
test.AssertDeepEquals(t, problems, expectedProblems)
|
|
|
|
// Check the certificate again with an ignore map that excludes the affected
|
|
// lints. This should return no problems.
|
|
lints, err := linter.NewRegistry([]string{
|
|
"w_subject_common_name_included",
|
|
"w_ext_subject_key_identifier_not_recommended_subscriber",
|
|
"w_ct_sct_policy_count_unsatisfied",
|
|
"e_scts_from_same_operator",
|
|
})
|
|
test.AssertNotError(t, err, "creating test lint registry")
|
|
checker.lints = lints
|
|
_, problems = checker.checkCert(context.Background(), cert)
|
|
test.AssertEquals(t, len(problems), 0)
|
|
}
|
|
|
|
func TestPrecertCorrespond(t *testing.T) {
|
|
checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
|
|
checker.getPrecert = func(_ context.Context, _ string) ([]byte, error) {
|
|
return []byte("hello"), nil
|
|
}
|
|
testKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
|
expiry := time.Now().AddDate(0, 0, 1)
|
|
serial := big.NewInt(1337)
|
|
rawCert := x509.Certificate{
|
|
Subject: pkix.Name{
|
|
CommonName: "example.com",
|
|
},
|
|
NotAfter: expiry,
|
|
DNSNames: []string{"example-a.com"},
|
|
SerialNumber: serial,
|
|
}
|
|
certDer, _ := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey)
|
|
cert := &corepb.Certificate{
|
|
Serial: core.SerialToString(serial),
|
|
Digest: core.Fingerprint256(certDer),
|
|
Der: certDer,
|
|
Issued: timestamppb.New(time.Now()),
|
|
Expires: timestamppb.New(expiry),
|
|
}
|
|
_, problems := checker.checkCert(context.Background(), cert)
|
|
if len(problems) == 0 {
|
|
t.Errorf("expected precert correspondence problem")
|
|
}
|
|
// Ensure that at least one of the problems was related to checking correspondence
|
|
for _, p := range problems {
|
|
if strings.Contains(p, "does not correspond to precert") {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("expected precert correspondence problem, but got: %v", problems)
|
|
}
|