697 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			697 lines
		
	
	
		
			24 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"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/jmhodges/clock"
 | |
| 	"google.golang.org/protobuf/types/known/timestamppb"
 | |
| 
 | |
| 	"github.com/letsencrypt/boulder/core"
 | |
| 	"github.com/letsencrypt/boulder/ctpolicy/loglist"
 | |
| 	"github.com/letsencrypt/boulder/goodkey"
 | |
| 	"github.com/letsencrypt/boulder/goodkey/sagoodkey"
 | |
| 	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[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, 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 := core.Certificate{
 | |
| 		Serial:  core.SerialToString(serial),
 | |
| 		Digest:  core.Fingerprint256(certDer),
 | |
| 		DER:     certDer,
 | |
| 		Issued:  time.Now(),
 | |
| 		Expires: expiry,
 | |
| 	}
 | |
| 	b.ResetTimer()
 | |
| 	for range b.N {
 | |
| 		checker.checkCert(context.Background(), cert, nil)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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, 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 := core.Certificate{
 | |
| 		Serial:  core.SerialToString(serial),
 | |
| 		Digest:  core.Fingerprint256(wildcardCertDer),
 | |
| 		Expires: parsed.NotAfter,
 | |
| 		Issued:  parsed.NotBefore,
 | |
| 		DER:     wildcardCertDer,
 | |
| 	}
 | |
| 	_, problems := checker.checkCert(context.Background(), cert, nil)
 | |
| 	for _, p := range problems {
 | |
| 		t.Error(p)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestCheckCertReturnsDNSNames(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, 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 := core.Certificate{
 | |
| 		Serial:  "00000000000",
 | |
| 		Digest:  core.Fingerprint256(block.Bytes),
 | |
| 		Expires: time.Now().Add(time.Hour),
 | |
| 		Issued:  time.Now(),
 | |
| 		DER:     block.Bytes,
 | |
| 	}
 | |
| 
 | |
| 	names, problems := checker.checkCert(context.Background(), cert, nil)
 | |
| 	if !slices.Equal(names, []string{"quite_invalid.com", "al--so--wr--ong.com"}) {
 | |
| 		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, 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 := core.Certificate{
 | |
| 				Serial:  "8485f2687eba29ad455ae4e31c8679206fec",
 | |
| 				DER:     brokenCertDer,
 | |
| 				Issued:  issued.Add(12 * time.Hour),
 | |
| 				Expires: goodExpiry.AddDate(0, 0, 2), // Expiration doesn't match
 | |
| 			}
 | |
| 
 | |
| 			_, problems := checker.checkCert(context.Background(), cert, nil)
 | |
| 
 | |
| 			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, nil)
 | |
| 			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 = parsed.NotAfter
 | |
| 			cert.Issued = parsed.NotBefore
 | |
| 			_, problems = checker.checkCert(context.Background(), cert, nil)
 | |
| 			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, 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, nil)
 | |
| 	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) {
 | |
| 	// But actually return nothing
 | |
| 	outputPtr, _ := output.(*[]sa.CertWithID)
 | |
| 	*outputPtr = []sa.CertWithID{}
 | |
| 	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, 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, 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, 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, blog.NewMock())
 | |
| 	serial := big.NewInt(1337)
 | |
| 
 | |
| 	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},
 | |
| 		PolicyIdentifiers: []asn1.ObjectIdentifier{
 | |
| 			{1, 2, 3},
 | |
| 		},
 | |
| 		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 := core.Certificate{
 | |
| 		Serial:  core.SerialToString(serial),
 | |
| 		DER:     subjectCertDer,
 | |
| 		Digest:  core.Fingerprint256(subjectCertDer),
 | |
| 		Issued:  subjectCert.NotBefore,
 | |
| 		Expires: 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.",
 | |
| 	}
 | |
| 	sort.Strings(expectedProblems)
 | |
| 
 | |
| 	// Check the certificate with a nil ignore map. This should return the
 | |
| 	// expected zlint problems.
 | |
| 	_, problems := checker.checkCert(context.Background(), cert, nil)
 | |
| 	sort.Strings(problems)
 | |
| 	test.AssertDeepEquals(t, problems, expectedProblems)
 | |
| 
 | |
| 	// Check the certificate again with an ignore map that excludes the affected
 | |
| 	// lints. This should return no problems.
 | |
| 	_, problems = checker.checkCert(context.Background(), cert, map[string]bool{
 | |
| 		"w_subject_common_name_included":                          true,
 | |
| 		"w_ext_subject_key_identifier_not_recommended_subscriber": true,
 | |
| 		"w_ct_sct_policy_count_unsatisfied":                       true,
 | |
| 		"e_scts_from_same_operator":                               true,
 | |
| 	})
 | |
| 	test.AssertEquals(t, len(problems), 0)
 | |
| }
 | |
| 
 | |
| func TestPrecertCorrespond(t *testing.T) {
 | |
| 	checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, 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 := core.Certificate{
 | |
| 		Serial:  core.SerialToString(serial),
 | |
| 		Digest:  core.Fingerprint256(certDer),
 | |
| 		DER:     certDer,
 | |
| 		Issued:  time.Now(),
 | |
| 		Expires: expiry,
 | |
| 	}
 | |
| 	_, problems := checker.checkCert(context.Background(), cert, nil)
 | |
| 	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)
 | |
| }
 |