github.com/letsencrypt/boulder@v0.20251208.0/cmd/cert-checker/main_test.go (about)

     1  package notmain
     2  
     3  import (
     4  	"context"
     5  	"crypto"
     6  	"crypto/ecdsa"
     7  	"crypto/elliptic"
     8  	"crypto/rand"
     9  	"crypto/rsa"
    10  	"crypto/x509"
    11  	"crypto/x509/pkix"
    12  	"database/sql"
    13  	"encoding/asn1"
    14  	"encoding/pem"
    15  	"errors"
    16  	"log"
    17  	"math/big"
    18  	mrand "math/rand/v2"
    19  	"os"
    20  	"slices"
    21  	"strings"
    22  	"sync"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/jmhodges/clock"
    27  	"google.golang.org/protobuf/types/known/timestamppb"
    28  
    29  	"github.com/letsencrypt/boulder/core"
    30  	corepb "github.com/letsencrypt/boulder/core/proto"
    31  	"github.com/letsencrypt/boulder/ctpolicy/loglist"
    32  	"github.com/letsencrypt/boulder/goodkey"
    33  	"github.com/letsencrypt/boulder/goodkey/sagoodkey"
    34  	"github.com/letsencrypt/boulder/identifier"
    35  	"github.com/letsencrypt/boulder/linter"
    36  	blog "github.com/letsencrypt/boulder/log"
    37  	"github.com/letsencrypt/boulder/metrics"
    38  	"github.com/letsencrypt/boulder/policy"
    39  	"github.com/letsencrypt/boulder/sa"
    40  	sapb "github.com/letsencrypt/boulder/sa/proto"
    41  	"github.com/letsencrypt/boulder/sa/satest"
    42  	"github.com/letsencrypt/boulder/test"
    43  	isa "github.com/letsencrypt/boulder/test/inmem/sa"
    44  	"github.com/letsencrypt/boulder/test/vars"
    45  )
    46  
    47  var (
    48  	testValidityDuration  = 24 * 90 * time.Hour
    49  	testValidityDurations = map[time.Duration]bool{testValidityDuration: true}
    50  	pa                    *policy.AuthorityImpl
    51  	kp                    goodkey.KeyPolicy
    52  )
    53  
    54  func init() {
    55  	var err error
    56  	pa, err = policy.New(
    57  		map[identifier.IdentifierType]bool{identifier.TypeDNS: true, identifier.TypeIP: true},
    58  		map[core.AcmeChallenge]bool{},
    59  		blog.NewMock())
    60  	if err != nil {
    61  		log.Fatal(err)
    62  	}
    63  	err = pa.LoadIdentPolicyFile("../../test/ident-policy.yaml")
    64  	if err != nil {
    65  		log.Fatal(err)
    66  	}
    67  	kp, err = sagoodkey.NewPolicy(nil, nil)
    68  	if err != nil {
    69  		log.Fatal(err)
    70  	}
    71  }
    72  
    73  func BenchmarkCheckCert(b *testing.B) {
    74  	checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
    75  	testKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    76  	expiry := time.Now().AddDate(0, 0, 1)
    77  	serial := big.NewInt(1337)
    78  	rawCert := x509.Certificate{
    79  		Subject: pkix.Name{
    80  			CommonName: "example.com",
    81  		},
    82  		NotAfter:     expiry,
    83  		DNSNames:     []string{"example-a.com"},
    84  		SerialNumber: serial,
    85  	}
    86  	certDer, _ := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey)
    87  	cert := &corepb.Certificate{
    88  		Serial:  core.SerialToString(serial),
    89  		Digest:  core.Fingerprint256(certDer),
    90  		Der:     certDer,
    91  		Issued:  timestamppb.New(time.Now()),
    92  		Expires: timestamppb.New(expiry),
    93  	}
    94  
    95  	for b.Loop() {
    96  		checker.checkCert(context.Background(), cert)
    97  	}
    98  }
    99  
   100  func TestCheckWildcardCert(t *testing.T) {
   101  	saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
   102  	test.AssertNotError(t, err, "Couldn't connect to database")
   103  	saCleanup := test.ResetBoulderTestDatabase(t)
   104  	defer func() {
   105  		saCleanup()
   106  	}()
   107  
   108  	testKey, _ := rsa.GenerateKey(rand.Reader, 2048)
   109  	fc := clock.NewFake()
   110  	checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
   111  	issued := checker.clock.Now().Add(-time.Minute)
   112  	goodExpiry := issued.Add(testValidityDuration - time.Second)
   113  	serial := big.NewInt(1337)
   114  
   115  	wildcardCert := x509.Certificate{
   116  		Subject: pkix.Name{
   117  			CommonName: "*.example.com",
   118  		},
   119  		NotBefore:             issued,
   120  		NotAfter:              goodExpiry,
   121  		DNSNames:              []string{"*.example.com"},
   122  		SerialNumber:          serial,
   123  		BasicConstraintsValid: true,
   124  		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
   125  		KeyUsage:              x509.KeyUsageDigitalSignature,
   126  		OCSPServer:            []string{"http://example.com/ocsp"},
   127  		IssuingCertificateURL: []string{"http://example.com/cert"},
   128  	}
   129  	wildcardCertDer, err := x509.CreateCertificate(rand.Reader, &wildcardCert, &wildcardCert, &testKey.PublicKey, testKey)
   130  	test.AssertNotError(t, err, "Couldn't create certificate")
   131  	parsed, err := x509.ParseCertificate(wildcardCertDer)
   132  	test.AssertNotError(t, err, "Couldn't parse created certificate")
   133  	cert := &corepb.Certificate{
   134  		Serial:  core.SerialToString(serial),
   135  		Digest:  core.Fingerprint256(wildcardCertDer),
   136  		Expires: timestamppb.New(parsed.NotAfter),
   137  		Issued:  timestamppb.New(parsed.NotBefore),
   138  		Der:     wildcardCertDer,
   139  	}
   140  	_, problems := checker.checkCert(context.Background(), cert)
   141  	for _, p := range problems {
   142  		t.Error(p)
   143  	}
   144  }
   145  
   146  func TestCheckCertReturnsSANs(t *testing.T) {
   147  	saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
   148  	test.AssertNotError(t, err, "Couldn't connect to database")
   149  	saCleanup := test.ResetBoulderTestDatabase(t)
   150  	defer func() {
   151  		saCleanup()
   152  	}()
   153  	checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
   154  
   155  	certPEM, err := os.ReadFile("testdata/quite_invalid.pem")
   156  	if err != nil {
   157  		t.Fatal(err)
   158  	}
   159  
   160  	block, _ := pem.Decode(certPEM)
   161  	if block == nil {
   162  		t.Fatal("failed to parse cert PEM")
   163  	}
   164  
   165  	cert := &corepb.Certificate{
   166  		Serial:  "00000000000",
   167  		Digest:  core.Fingerprint256(block.Bytes),
   168  		Expires: timestamppb.New(time.Now().Add(time.Hour)),
   169  		Issued:  timestamppb.New(time.Now()),
   170  		Der:     block.Bytes,
   171  	}
   172  
   173  	names, problems := checker.checkCert(context.Background(), cert)
   174  	if !slices.Equal(names, []string{"quite_invalid.com", "al--so--wr--ong.com", "127.0.0.1"}) {
   175  		t.Errorf("didn't get expected DNS names. other problems: %s", strings.Join(problems, "\n"))
   176  	}
   177  }
   178  
   179  type keyGen interface {
   180  	genKey() (crypto.Signer, error)
   181  }
   182  
   183  type ecP256Generator struct{}
   184  
   185  func (*ecP256Generator) genKey() (crypto.Signer, error) {
   186  	return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   187  }
   188  
   189  type rsa2048Generator struct{}
   190  
   191  func (*rsa2048Generator) genKey() (crypto.Signer, error) {
   192  	return rsa.GenerateKey(rand.Reader, 2048)
   193  }
   194  
   195  func TestCheckCert(t *testing.T) {
   196  	saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
   197  	test.AssertNotError(t, err, "Couldn't connect to database")
   198  	saCleanup := test.ResetBoulderTestDatabase(t)
   199  	defer func() {
   200  		saCleanup()
   201  	}()
   202  
   203  	testCases := []struct {
   204  		name string
   205  		key  keyGen
   206  	}{
   207  		{
   208  			name: "RSA 2048 key",
   209  			key:  &rsa2048Generator{},
   210  		},
   211  		{
   212  			name: "ECDSA P256 key",
   213  			key:  &ecP256Generator{},
   214  		},
   215  	}
   216  	for _, tc := range testCases {
   217  		t.Run(tc.name, func(t *testing.T) {
   218  			testKey, _ := tc.key.genKey()
   219  
   220  			checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
   221  
   222  			// Create a RFC 7633 OCSP Must Staple Extension.
   223  			// OID 1.3.6.1.5.5.7.1.24
   224  			ocspMustStaple := pkix.Extension{
   225  				Id:       asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24},
   226  				Critical: false,
   227  				Value:    []uint8{0x30, 0x3, 0x2, 0x1, 0x5},
   228  			}
   229  
   230  			// Create a made up PKIX extension
   231  			imaginaryExtension := pkix.Extension{
   232  				Id:       asn1.ObjectIdentifier{1, 3, 3, 7},
   233  				Critical: false,
   234  				Value:    []uint8{0xC0, 0xFF, 0xEE},
   235  			}
   236  
   237  			issued := checker.clock.Now().Add(-time.Minute)
   238  			goodExpiry := issued.Add(testValidityDuration - time.Second)
   239  			serial := big.NewInt(1337)
   240  			longName := "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeexample.com"
   241  			rawCert := x509.Certificate{
   242  				Subject: pkix.Name{
   243  					CommonName: longName,
   244  				},
   245  				NotBefore: issued,
   246  				NotAfter:  goodExpiry.AddDate(0, 0, 1), // Period too long
   247  				DNSNames: []string{
   248  					"example-a.com",
   249  					"foodnotbombs.mil",
   250  					// `dev-myqnapcloud.com` is included because it is an exact private
   251  					// entry on the public suffix list
   252  					"dev-myqnapcloud.com",
   253  					// don't include longName in the SANs, so the unique CN gets flagged
   254  				},
   255  				SerialNumber:          serial,
   256  				BasicConstraintsValid: false,
   257  				ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
   258  				KeyUsage:              x509.KeyUsageDigitalSignature,
   259  				OCSPServer:            []string{"http://example.com/ocsp"},
   260  				IssuingCertificateURL: []string{"http://example.com/cert"},
   261  				ExtraExtensions:       []pkix.Extension{ocspMustStaple, imaginaryExtension},
   262  			}
   263  			brokenCertDer, err := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, testKey.Public(), testKey)
   264  			test.AssertNotError(t, err, "Couldn't create certificate")
   265  			// Problems
   266  			//   Digest doesn't match
   267  			//   Serial doesn't match
   268  			//   Expiry doesn't match
   269  			//   Issued doesn't match
   270  			cert := &corepb.Certificate{
   271  				Serial:  "8485f2687eba29ad455ae4e31c8679206fec",
   272  				Der:     brokenCertDer,
   273  				Issued:  timestamppb.New(issued.Add(12 * time.Hour)),
   274  				Expires: timestamppb.New(goodExpiry.AddDate(0, 0, 2)), // Expiration doesn't match
   275  			}
   276  
   277  			_, problems := checker.checkCert(context.Background(), cert)
   278  
   279  			problemsMap := map[string]int{
   280  				"Stored digest doesn't match certificate digest":                            1,
   281  				"Stored serial doesn't match certificate serial":                            1,
   282  				"Stored expiration doesn't match certificate NotAfter":                      1,
   283  				"Certificate doesn't have basic constraints set":                            1,
   284  				"Certificate has unacceptable validity period":                              1,
   285  				"Stored issuance date is outside of 6 hour window of certificate NotBefore": 1,
   286  				"Certificate has incorrect key usage extensions":                            1,
   287  				"Certificate has common name >64 characters long (65)":                      1,
   288  				"Certificate contains an unexpected extension: 1.3.3.7":                     1,
   289  				"Certificate Common Name does not appear in Subject Alternative Names: \"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeexample.com\" !< [example-a.com foodnotbombs.mil dev-myqnapcloud.com]": 1,
   290  			}
   291  			for _, p := range problems {
   292  				_, ok := problemsMap[p]
   293  				if !ok {
   294  					t.Errorf("Found unexpected problem '%s'.", p)
   295  				}
   296  				delete(problemsMap, p)
   297  			}
   298  			for k := range problemsMap {
   299  				t.Errorf("Expected problem but didn't find '%s' in problems: %q.", k, problems)
   300  			}
   301  
   302  			// Same settings as above, but the stored serial number in the DB is invalid.
   303  			cert.Serial = "not valid"
   304  			_, problems = checker.checkCert(context.Background(), cert)
   305  			foundInvalidSerialProblem := false
   306  			for _, p := range problems {
   307  				if p == "Stored serial is invalid" {
   308  					foundInvalidSerialProblem = true
   309  				}
   310  			}
   311  			test.Assert(t, foundInvalidSerialProblem, "Invalid certificate serial number in DB did not trigger problem.")
   312  
   313  			// Fix the problems
   314  			rawCert.Subject.CommonName = "example-a.com"
   315  			rawCert.DNSNames = []string{"example-a.com"}
   316  			rawCert.NotAfter = goodExpiry
   317  			rawCert.BasicConstraintsValid = true
   318  			rawCert.ExtraExtensions = []pkix.Extension{ocspMustStaple}
   319  			rawCert.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
   320  			goodCertDer, err := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, testKey.Public(), testKey)
   321  			test.AssertNotError(t, err, "Couldn't create certificate")
   322  			parsed, err := x509.ParseCertificate(goodCertDer)
   323  			test.AssertNotError(t, err, "Couldn't parse created certificate")
   324  			cert.Serial = core.SerialToString(serial)
   325  			cert.Digest = core.Fingerprint256(goodCertDer)
   326  			cert.Der = goodCertDer
   327  			cert.Expires = timestamppb.New(parsed.NotAfter)
   328  			cert.Issued = timestamppb.New(parsed.NotBefore)
   329  			_, problems = checker.checkCert(context.Background(), cert)
   330  			test.AssertEquals(t, len(problems), 0)
   331  		})
   332  	}
   333  }
   334  
   335  func TestGetAndProcessCerts(t *testing.T) {
   336  	saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
   337  	test.AssertNotError(t, err, "Couldn't connect to database")
   338  	fc := clock.NewFake()
   339  	fc.Set(fc.Now().Add(time.Hour))
   340  
   341  	checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
   342  	sa, err := sa.NewSQLStorageAuthority(saDbMap, saDbMap, nil, 1, 0, fc, blog.NewMock(), metrics.NoopRegisterer)
   343  	test.AssertNotError(t, err, "Couldn't create SA to insert certificates")
   344  	saCleanUp := test.ResetBoulderTestDatabase(t)
   345  	defer func() {
   346  		saCleanUp()
   347  	}()
   348  
   349  	testKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   350  	// Problems
   351  	//   Expiry period is too long
   352  	rawCert := x509.Certificate{
   353  		Subject: pkix.Name{
   354  			CommonName: "not-blacklisted.com",
   355  		},
   356  		NotBefore:             fc.Now(),
   357  		NotAfter:              fc.Now().Add(999999 * time.Hour),
   358  		BasicConstraintsValid: true,
   359  		DNSNames:              []string{"not-blacklisted.com"},
   360  		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
   361  	}
   362  	reg := satest.CreateWorkingRegistration(t, isa.SA{Impl: sa})
   363  	test.AssertNotError(t, err, "Couldn't create registration")
   364  	for range 5 {
   365  		rawCert.SerialNumber = big.NewInt(mrand.Int64())
   366  		certDER, err := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey)
   367  		test.AssertNotError(t, err, "Couldn't create certificate")
   368  		_, err = sa.AddCertificate(context.Background(), &sapb.AddCertificateRequest{
   369  			Der:    certDER,
   370  			RegID:  reg.Id,
   371  			Issued: timestamppb.New(fc.Now()),
   372  		})
   373  		test.AssertNotError(t, err, "Couldn't add certificate")
   374  	}
   375  
   376  	batchSize = 2
   377  	err = checker.getCerts(context.Background())
   378  	test.AssertNotError(t, err, "Failed to retrieve certificates")
   379  	test.AssertEquals(t, len(checker.certs), 5)
   380  	wg := new(sync.WaitGroup)
   381  	wg.Add(1)
   382  	checker.processCerts(context.Background(), wg, false)
   383  	test.AssertEquals(t, checker.issuedReport.BadCerts, int64(5))
   384  	test.AssertEquals(t, len(checker.issuedReport.Entries), 5)
   385  }
   386  
   387  // mismatchedCountDB is a certDB implementation for `getCerts` that returns one
   388  // high value when asked how many rows there are, and then returns nothing when
   389  // asked for the actual rows.
   390  type mismatchedCountDB struct{}
   391  
   392  // `getCerts` calls `SelectInt` first to determine how many rows there are
   393  // matching the `getCertsCountQuery` criteria. For this mock we return
   394  // a non-zero number
   395  func (db mismatchedCountDB) SelectNullInt(_ context.Context, _ string, _ ...any) (sql.NullInt64, error) {
   396  	return sql.NullInt64{
   397  			Int64: 99999,
   398  			Valid: true,
   399  		},
   400  		nil
   401  }
   402  
   403  // `getCerts` then calls `Select` to retrieve the Certificate rows. We pull
   404  // a dastardly switch-a-roo here and return an empty set
   405  func (db mismatchedCountDB) Select(_ context.Context, output any, _ string, _ ...any) ([]any, error) {
   406  	return nil, nil
   407  }
   408  
   409  func (db mismatchedCountDB) SelectOne(_ context.Context, _ any, _ string, _ ...any) error {
   410  	return errors.New("unimplemented")
   411  }
   412  
   413  /*
   414   * In Boulder #2004[0] we identified that there is a race in `getCerts`
   415   * between the first call to `SelectOne` to identify how many rows there are,
   416   * and the subsequent call to `Select` to get the actual rows in batches. This
   417   * manifests in an index out of range panic where the cert checker thinks there
   418   * are more rows than there are and indexes into an empty set of certificates to
   419   * update the lastSerial field of the query `args`. This has been fixed by
   420   * adding a len() check in the inner `getCerts` loop that processes the certs
   421   * one batch at a time.
   422   *
   423   * TestGetCertsEmptyResults tests the fix remains in place by using a mock that
   424   * exploits this corner case deliberately. The `mismatchedCountDB` mock (defined
   425   * above) will return a high count for the `SelectOne` call, but an empty slice
   426   * for the `Select` call. Without the fix in place this reliably produced the
   427   * "index out of range" panic from #2004. With the fix in place the test passes.
   428   *
   429   * 0: https://github.com/letsencrypt/boulder/issues/2004
   430   */
   431  func TestGetCertsEmptyResults(t *testing.T) {
   432  	saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
   433  	test.AssertNotError(t, err, "Couldn't connect to database")
   434  	checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
   435  	checker.dbMap = mismatchedCountDB{}
   436  
   437  	batchSize = 3
   438  	err = checker.getCerts(context.Background())
   439  	test.AssertNotError(t, err, "Failed to retrieve certificates")
   440  }
   441  
   442  // emptyDB is a certDB object with methods used for testing that 'null'
   443  // responses received from the database are handled properly.
   444  type emptyDB struct {
   445  	certDB
   446  }
   447  
   448  // SelectNullInt is a method that returns a false sql.NullInt64 struct to
   449  // mock a null DB response
   450  func (db emptyDB) SelectNullInt(_ context.Context, _ string, _ ...any) (sql.NullInt64, error) {
   451  	return sql.NullInt64{Valid: false},
   452  		nil
   453  }
   454  
   455  // TestGetCertsNullResults tests that a null response from the database will
   456  // be handled properly. It uses the emptyDB above to mock the response
   457  // expected if the DB finds no certificates to match the SELECT query and
   458  // should return an error.
   459  func TestGetCertsNullResults(t *testing.T) {
   460  	checker := newChecker(emptyDB{}, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
   461  
   462  	err := checker.getCerts(context.Background())
   463  	test.AssertError(t, err, "Should have gotten error from empty DB")
   464  	if !strings.Contains(err.Error(), "no rows found for certificates issued between") {
   465  		t.Errorf("expected error to contain 'no rows found for certificates issued between', got '%s'", err.Error())
   466  	}
   467  }
   468  
   469  // lateDB is a certDB object that helps with TestGetCertsLate.
   470  // It pretends to contain a single cert issued at the given time.
   471  type lateDB struct {
   472  	issuedTime    time.Time
   473  	selectedACert bool
   474  }
   475  
   476  // SelectNullInt is a method that returns a false sql.NullInt64 struct to
   477  // mock a null DB response
   478  func (db *lateDB) SelectNullInt(_ context.Context, _ string, args ...any) (sql.NullInt64, error) {
   479  	args2 := args[0].(map[string]any)
   480  	begin := args2["begin"].(time.Time)
   481  	end := args2["end"].(time.Time)
   482  	if begin.Compare(db.issuedTime) < 0 && end.Compare(db.issuedTime) > 0 {
   483  		return sql.NullInt64{Int64: 23, Valid: true}, nil
   484  	}
   485  	return sql.NullInt64{Valid: false}, nil
   486  }
   487  
   488  func (db *lateDB) Select(_ context.Context, output any, _ string, args ...any) ([]any, error) {
   489  	db.selectedACert = true
   490  	// For expediency we respond with an empty list of certificates; the checker will treat this as if it's
   491  	// reached the end of the list of certificates to process.
   492  	return nil, nil
   493  }
   494  
   495  func (db *lateDB) SelectOne(_ context.Context, _ any, _ string, _ ...any) error {
   496  	return nil
   497  }
   498  
   499  // TestGetCertsLate checks for correct behavior when certificates exist only late in the provided window.
   500  func TestGetCertsLate(t *testing.T) {
   501  	clk := clock.NewFake()
   502  	db := &lateDB{issuedTime: clk.Now().Add(-time.Hour)}
   503  	checkPeriod := 24 * time.Hour
   504  	checker := newChecker(db, clk, pa, kp, checkPeriod, testValidityDurations, nil, blog.NewMock())
   505  
   506  	err := checker.getCerts(context.Background())
   507  	test.AssertNotError(t, err, "getting certs")
   508  
   509  	if !db.selectedACert {
   510  		t.Errorf("checker never selected a certificate after getting a MIN(id)")
   511  	}
   512  }
   513  
   514  func TestSaveReport(t *testing.T) {
   515  	r := report{
   516  		begin:     time.Time{},
   517  		end:       time.Time{},
   518  		GoodCerts: 2,
   519  		BadCerts:  1,
   520  		Entries: map[string]reportEntry{
   521  			"020000000000004b475da49b91da5c17": {
   522  				Valid: true,
   523  			},
   524  			"020000000000004d1613e581432cba7e": {
   525  				Valid: true,
   526  			},
   527  			"020000000000004e402bc21035c6634a": {
   528  				Valid:    false,
   529  				Problems: []string{"None really..."},
   530  			},
   531  		},
   532  	}
   533  
   534  	err := r.dump()
   535  	test.AssertNotError(t, err, "Failed to dump results")
   536  }
   537  
   538  func TestIsForbiddenDomain(t *testing.T) {
   539  	// Note: These testcases are not an exhaustive representation of domains
   540  	// Boulder won't issue for, but are instead testing the defense-in-depth
   541  	// `isForbiddenDomain` function called *after* the PA has vetted the name
   542  	// against the complex identifier policy file.
   543  	testcases := []struct {
   544  		Name     string
   545  		Expected bool
   546  	}{
   547  		/* Expected to be forbidden test cases */
   548  		// Whitespace only
   549  		{Name: "", Expected: true},
   550  		{Name: "   ", Expected: true},
   551  		// Anything .local
   552  		{Name: "yokel.local", Expected: true},
   553  		{Name: "off.on.remote.local", Expected: true},
   554  		{Name: ".local", Expected: true},
   555  		// Localhost is verboten
   556  		{Name: "localhost", Expected: true},
   557  		// Anything .localhost
   558  		{Name: ".localhost", Expected: true},
   559  		{Name: "local.localhost", Expected: true},
   560  		{Name: "extremely.local.localhost", Expected: true},
   561  
   562  		/* Expected to be allowed test cases */
   563  		{Name: "ok.computer.com", Expected: false},
   564  		{Name: "ok.millionaires", Expected: false},
   565  		{Name: "ok.milly", Expected: false},
   566  		{Name: "ok", Expected: false},
   567  		{Name: "nearby.locals", Expected: false},
   568  		{Name: "yocalhost", Expected: false},
   569  		{Name: "jokes.yocalhost", Expected: false},
   570  	}
   571  
   572  	for _, tc := range testcases {
   573  		result, _ := isForbiddenDomain(tc.Name)
   574  		test.AssertEquals(t, result, tc.Expected)
   575  	}
   576  }
   577  
   578  func TestIgnoredLint(t *testing.T) {
   579  	saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
   580  	test.AssertNotError(t, err, "Couldn't connect to database")
   581  	saCleanup := test.ResetBoulderTestDatabase(t)
   582  	defer func() {
   583  		saCleanup()
   584  	}()
   585  
   586  	err = loglist.InitLintList("../../test/ct-test-srv/log_list.json")
   587  	test.AssertNotError(t, err, "failed to load ct log list")
   588  	testKey, _ := rsa.GenerateKey(rand.Reader, 2048)
   589  	checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
   590  	serial := big.NewInt(1337)
   591  
   592  	x509OID, err := x509.OIDFromInts([]uint64{1, 2, 3})
   593  	test.AssertNotError(t, err, "failed to create x509.OID")
   594  
   595  	template := &x509.Certificate{
   596  		Subject: pkix.Name{
   597  			CommonName: "CPU's Cool CA",
   598  		},
   599  		SerialNumber:          serial,
   600  		NotBefore:             time.Now(),
   601  		NotAfter:              time.Now().Add(testValidityDuration - time.Second),
   602  		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
   603  		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
   604  		Policies:              []x509.OID{x509OID},
   605  		BasicConstraintsValid: true,
   606  		IsCA:                  true,
   607  		IssuingCertificateURL: []string{"http://aia.example.org"},
   608  		SubjectKeyId:          []byte("foobar"),
   609  	}
   610  
   611  	// Create a self-signed issuer certificate to use
   612  	issuerDer, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey)
   613  	test.AssertNotError(t, err, "failed to create self-signed issuer cert")
   614  	issuerCert, err := x509.ParseCertificate(issuerDer)
   615  	test.AssertNotError(t, err, "failed to parse self-signed issuer cert")
   616  
   617  	// Reconfigure the template for an EE cert with a Subj. CN
   618  	serial = big.NewInt(1338)
   619  	template.SerialNumber = serial
   620  	template.Subject.CommonName = "zombo.com"
   621  	template.DNSNames = []string{"zombo.com"}
   622  	template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
   623  	template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
   624  	template.IsCA = false
   625  
   626  	subjectCertDer, err := x509.CreateCertificate(rand.Reader, template, issuerCert, testKey.Public(), testKey)
   627  	test.AssertNotError(t, err, "failed to create EE cert")
   628  	subjectCert, err := x509.ParseCertificate(subjectCertDer)
   629  	test.AssertNotError(t, err, "failed to parse EE cert")
   630  
   631  	cert := &corepb.Certificate{
   632  		Serial:  core.SerialToString(serial),
   633  		Der:     subjectCertDer,
   634  		Digest:  core.Fingerprint256(subjectCertDer),
   635  		Issued:  timestamppb.New(subjectCert.NotBefore),
   636  		Expires: timestamppb.New(subjectCert.NotAfter),
   637  	}
   638  
   639  	// Without any ignored lints we expect several errors and warnings about SCTs,
   640  	// the common name, and the subject key identifier extension.
   641  	expectedProblems := []string{
   642  		"zlint warn: w_subject_common_name_included",
   643  		"zlint warn: w_ext_subject_key_identifier_not_recommended_subscriber",
   644  		"zlint info: w_ct_sct_policy_count_unsatisfied Certificate had 0 embedded SCTs. Browser policy may require 2 for this certificate.",
   645  		"zlint error: e_scts_from_same_operator Certificate had too few embedded SCTs; browser policy requires 2.",
   646  	}
   647  	slices.Sort(expectedProblems)
   648  
   649  	// Check the certificate with a nil ignore map. This should return the
   650  	// expected zlint problems.
   651  	_, problems := checker.checkCert(context.Background(), cert)
   652  	slices.Sort(problems)
   653  	test.AssertDeepEquals(t, problems, expectedProblems)
   654  
   655  	// Check the certificate again with an ignore map that excludes the affected
   656  	// lints. This should return no problems.
   657  	lints, err := linter.NewRegistry([]string{
   658  		"w_subject_common_name_included",
   659  		"w_ext_subject_key_identifier_not_recommended_subscriber",
   660  		"w_ct_sct_policy_count_unsatisfied",
   661  		"e_scts_from_same_operator",
   662  	})
   663  	test.AssertNotError(t, err, "creating test lint registry")
   664  	checker.lints = lints
   665  	_, problems = checker.checkCert(context.Background(), cert)
   666  	test.AssertEquals(t, len(problems), 0)
   667  }
   668  
   669  func TestPrecertCorrespond(t *testing.T) {
   670  	checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
   671  	checker.getPrecert = func(_ context.Context, _ string) ([]byte, error) {
   672  		return []byte("hello"), nil
   673  	}
   674  	testKey, _ := rsa.GenerateKey(rand.Reader, 2048)
   675  	expiry := time.Now().AddDate(0, 0, 1)
   676  	serial := big.NewInt(1337)
   677  	rawCert := x509.Certificate{
   678  		Subject: pkix.Name{
   679  			CommonName: "example.com",
   680  		},
   681  		NotAfter:     expiry,
   682  		DNSNames:     []string{"example-a.com"},
   683  		SerialNumber: serial,
   684  	}
   685  	certDer, _ := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey)
   686  	cert := &corepb.Certificate{
   687  		Serial:  core.SerialToString(serial),
   688  		Digest:  core.Fingerprint256(certDer),
   689  		Der:     certDer,
   690  		Issued:  timestamppb.New(time.Now()),
   691  		Expires: timestamppb.New(expiry),
   692  	}
   693  	_, problems := checker.checkCert(context.Background(), cert)
   694  	if len(problems) == 0 {
   695  		t.Errorf("expected precert correspondence problem")
   696  	}
   697  	// Ensure that at least one of the problems was related to checking correspondence
   698  	for _, p := range problems {
   699  		if strings.Contains(p, "does not correspond to precert") {
   700  			return
   701  		}
   702  	}
   703  	t.Fatalf("expected precert correspondence problem, but got: %v", problems)
   704  }