...

Source file src/github.com/letsencrypt/boulder/cmd/cert-checker/main.go

Documentation: github.com/letsencrypt/boulder/cmd/cert-checker

     1  package notmain
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/x509"
     7  	"database/sql"
     8  	"encoding/json"
     9  	"flag"
    10  	"fmt"
    11  	"log/syslog"
    12  	"os"
    13  	"regexp"
    14  	"slices"
    15  	"sync"
    16  	"sync/atomic"
    17  	"time"
    18  
    19  	"github.com/jmhodges/clock"
    20  	"github.com/prometheus/client_golang/prometheus"
    21  	zX509 "github.com/zmap/zcrypto/x509"
    22  	"github.com/zmap/zlint/v3"
    23  	"github.com/zmap/zlint/v3/lint"
    24  
    25  	"github.com/letsencrypt/boulder/cmd"
    26  	"github.com/letsencrypt/boulder/config"
    27  	"github.com/letsencrypt/boulder/core"
    28  	corepb "github.com/letsencrypt/boulder/core/proto"
    29  	"github.com/letsencrypt/boulder/ctpolicy/loglist"
    30  	"github.com/letsencrypt/boulder/features"
    31  	"github.com/letsencrypt/boulder/goodkey"
    32  	"github.com/letsencrypt/boulder/goodkey/sagoodkey"
    33  	"github.com/letsencrypt/boulder/identifier"
    34  	_ "github.com/letsencrypt/boulder/linter"
    35  	blog "github.com/letsencrypt/boulder/log"
    36  	"github.com/letsencrypt/boulder/policy"
    37  	"github.com/letsencrypt/boulder/precert"
    38  	"github.com/letsencrypt/boulder/sa"
    39  )
    40  
    41  // For defense-in-depth in addition to using the PA & its hostnamePolicy to
    42  // check domain names we also perform a check against the regex's from the
    43  // forbiddenDomains array
    44  var forbiddenDomainPatterns = []*regexp.Regexp{
    45  	regexp.MustCompile(`^\s*$`),
    46  	regexp.MustCompile(`\.local$`),
    47  	regexp.MustCompile(`^localhost$`),
    48  	regexp.MustCompile(`\.localhost$`),
    49  }
    50  
    51  func isForbiddenDomain(name string) (bool, string) {
    52  	for _, r := range forbiddenDomainPatterns {
    53  		if matches := r.FindAllStringSubmatch(name, -1); len(matches) > 0 {
    54  			return true, r.String()
    55  		}
    56  	}
    57  	return false, ""
    58  }
    59  
    60  var batchSize = 1000
    61  
    62  type report struct {
    63  	begin     time.Time
    64  	end       time.Time
    65  	GoodCerts int64                  `json:"good-certs"`
    66  	BadCerts  int64                  `json:"bad-certs"`
    67  	DbErrs    int64                  `json:"db-errs"`
    68  	Entries   map[string]reportEntry `json:"entries"`
    69  }
    70  
    71  func (r *report) dump() error {
    72  	content, err := json.MarshalIndent(r, "", "  ")
    73  	if err != nil {
    74  		return err
    75  	}
    76  	fmt.Fprintln(os.Stdout, string(content))
    77  	return nil
    78  }
    79  
    80  type reportEntry struct {
    81  	Valid    bool     `json:"valid"`
    82  	DNSNames []string `json:"dnsNames"`
    83  	Problems []string `json:"problems,omitempty"`
    84  }
    85  
    86  // certDB is an interface collecting the borp.DbMap functions that the various
    87  // parts of cert-checker rely on. Using this adapter shim allows tests to swap
    88  // out the saDbMap implementation.
    89  type certDB interface {
    90  	Select(ctx context.Context, i interface{}, query string, args ...interface{}) ([]interface{}, error)
    91  	SelectOne(ctx context.Context, i interface{}, query string, args ...interface{}) error
    92  	SelectNullInt(ctx context.Context, query string, args ...interface{}) (sql.NullInt64, error)
    93  }
    94  
    95  // A function that looks up a precertificate by serial and returns its DER bytes. Used for
    96  // mocking in tests.
    97  type precertGetter func(context.Context, string) ([]byte, error)
    98  
    99  type certChecker struct {
   100  	pa                          core.PolicyAuthority
   101  	kp                          goodkey.KeyPolicy
   102  	dbMap                       certDB
   103  	getPrecert                  precertGetter
   104  	certs                       chan core.Certificate
   105  	clock                       clock.Clock
   106  	rMu                         *sync.Mutex
   107  	issuedReport                report
   108  	checkPeriod                 time.Duration
   109  	acceptableValidityDurations map[time.Duration]bool
   110  	logger                      blog.Logger
   111  }
   112  
   113  func newChecker(saDbMap certDB,
   114  	clk clock.Clock,
   115  	pa core.PolicyAuthority,
   116  	kp goodkey.KeyPolicy,
   117  	period time.Duration,
   118  	avd map[time.Duration]bool,
   119  	logger blog.Logger,
   120  ) certChecker {
   121  	precertGetter := func(ctx context.Context, serial string) ([]byte, error) {
   122  		precertPb, err := sa.SelectPrecertificate(ctx, saDbMap, serial)
   123  		if err != nil {
   124  			return nil, err
   125  		}
   126  		return precertPb.DER, nil
   127  	}
   128  	return certChecker{
   129  		pa:                          pa,
   130  		kp:                          kp,
   131  		dbMap:                       saDbMap,
   132  		getPrecert:                  precertGetter,
   133  		certs:                       make(chan core.Certificate, batchSize),
   134  		rMu:                         new(sync.Mutex),
   135  		clock:                       clk,
   136  		issuedReport:                report{Entries: make(map[string]reportEntry)},
   137  		checkPeriod:                 period,
   138  		acceptableValidityDurations: avd,
   139  		logger:                      logger,
   140  	}
   141  }
   142  
   143  // findStartingID returns the lowest `id` in the certificates table within the
   144  // time window specified. The time window is a half-open interval [begin, end).
   145  func (c *certChecker) findStartingID(ctx context.Context, begin, end time.Time) (int64, error) {
   146  	var output sql.NullInt64
   147  	var err error
   148  	var retries int
   149  
   150  	// Rather than querying `MIN(id)` across that whole window, we query it across the first
   151  	// hour of the window. This allows the query planner to use the index on `issued` more
   152  	// effectively. For a busy, actively issuing CA, that will always return results in the
   153  	// first query. For a less busy CA, or during integration tests, there may only exist
   154  	// certificates towards the end of the window, so we try querying later hourly chunks until
   155  	// we find a certificate or hit the end of the window. We also retry transient errors.
   156  	queryBegin := begin
   157  	queryEnd := begin.Add(time.Hour)
   158  
   159  	for queryBegin.Compare(end) < 0 {
   160  		output, err = c.dbMap.SelectNullInt(
   161  			ctx,
   162  			`SELECT MIN(id) FROM certificates
   163  				WHERE issued >= :begin AND
   164  					  issued < :end`,
   165  			map[string]interface{}{
   166  				"begin": queryBegin,
   167  				"end":   queryEnd,
   168  			},
   169  		)
   170  		if err != nil {
   171  			c.logger.AuditErrf("finding starting certificate: %s", err)
   172  			retries++
   173  			time.Sleep(core.RetryBackoff(retries, time.Second, time.Minute, 2))
   174  			continue
   175  		}
   176  		// https://mariadb.com/kb/en/min/
   177  		// MIN() returns NULL if there were no matching rows
   178  		// https://pkg.go.dev/database/sql#NullInt64
   179  		// Valid is true if Int64 is not NULL
   180  		if !output.Valid {
   181  			// No matching rows, try the next hour
   182  			queryBegin = queryBegin.Add(time.Hour)
   183  			queryEnd = queryEnd.Add(time.Hour)
   184  			if queryEnd.Compare(end) > 0 {
   185  				queryEnd = end
   186  			}
   187  			continue
   188  		}
   189  
   190  		return output.Int64, nil
   191  	}
   192  
   193  	// Fell through the loop without finding a valid ID
   194  	return 0, fmt.Errorf("no rows found for certificates issued between %s and %s", begin, end)
   195  }
   196  
   197  func (c *certChecker) getCerts(ctx context.Context) error {
   198  	// The end of the report is the current time, rounded up to the nearest second.
   199  	c.issuedReport.end = c.clock.Now().Truncate(time.Second).Add(time.Second)
   200  	// The beginning of the report is the end minus the check period, rounded down to the nearest second.
   201  	c.issuedReport.begin = c.issuedReport.end.Add(-c.checkPeriod).Truncate(time.Second)
   202  
   203  	initialID, err := c.findStartingID(ctx, c.issuedReport.begin, c.issuedReport.end)
   204  	if err != nil {
   205  		return err
   206  	}
   207  	if initialID > 0 {
   208  		// decrement the initial ID so that we select below as we aren't using >=
   209  		initialID -= 1
   210  	}
   211  
   212  	batchStartID := initialID
   213  	var retries int
   214  	for {
   215  		certs, err := sa.SelectCertificates(
   216  			ctx,
   217  			c.dbMap,
   218  			`WHERE id > :id AND
   219  			       issued >= :begin AND
   220  				   issued < :end
   221  			 ORDER BY id LIMIT :limit`,
   222  			map[string]interface{}{
   223  				"begin": c.issuedReport.begin,
   224  				"end":   c.issuedReport.end,
   225  				// Retrieve certs in batches of 1000 (the size of the certificate channel)
   226  				// so that we don't eat unnecessary amounts of memory and avoid the 16MB MySQL
   227  				// packet limit.
   228  				"limit": batchSize,
   229  				"id":    batchStartID,
   230  			},
   231  		)
   232  		if err != nil {
   233  			c.logger.AuditErrf("selecting certificates: %s", err)
   234  			retries++
   235  			time.Sleep(core.RetryBackoff(retries, time.Second, time.Minute, 2))
   236  			continue
   237  		}
   238  		retries = 0
   239  		for _, cert := range certs {
   240  			c.certs <- cert.Certificate
   241  		}
   242  		if len(certs) == 0 {
   243  			break
   244  		}
   245  		lastCert := certs[len(certs)-1]
   246  		batchStartID = lastCert.ID
   247  		if lastCert.Issued.After(c.issuedReport.end) {
   248  			break
   249  		}
   250  	}
   251  
   252  	// Close channel so range operations won't block once the channel empties out
   253  	close(c.certs)
   254  	return nil
   255  }
   256  
   257  func (c *certChecker) processCerts(ctx context.Context, wg *sync.WaitGroup, badResultsOnly bool, ignoredLints map[string]bool) {
   258  	for cert := range c.certs {
   259  		dnsNames, problems := c.checkCert(ctx, cert, ignoredLints)
   260  		valid := len(problems) == 0
   261  		c.rMu.Lock()
   262  		if !badResultsOnly || (badResultsOnly && !valid) {
   263  			c.issuedReport.Entries[cert.Serial] = reportEntry{
   264  				Valid:    valid,
   265  				DNSNames: dnsNames,
   266  				Problems: problems,
   267  			}
   268  		}
   269  		c.rMu.Unlock()
   270  		if !valid {
   271  			atomic.AddInt64(&c.issuedReport.BadCerts, 1)
   272  		} else {
   273  			atomic.AddInt64(&c.issuedReport.GoodCerts, 1)
   274  		}
   275  	}
   276  	wg.Done()
   277  }
   278  
   279  // Extensions that we allow in certificates
   280  var allowedExtensions = map[string]bool{
   281  	"1.3.6.1.5.5.7.1.1":       true, // Authority info access
   282  	"2.5.29.35":               true, // Authority key identifier
   283  	"2.5.29.19":               true, // Basic constraints
   284  	"2.5.29.32":               true, // Certificate policies
   285  	"2.5.29.31":               true, // CRL distribution points
   286  	"2.5.29.37":               true, // Extended key usage
   287  	"2.5.29.15":               true, // Key usage
   288  	"2.5.29.17":               true, // Subject alternative name
   289  	"2.5.29.14":               true, // Subject key identifier
   290  	"1.3.6.1.4.1.11129.2.4.2": true, // SCT list
   291  	"1.3.6.1.5.5.7.1.24":      true, // TLS feature
   292  }
   293  
   294  // For extensions that have a fixed value we check that it contains that value
   295  var expectedExtensionContent = map[string][]byte{
   296  	"1.3.6.1.5.5.7.1.24": {0x30, 0x03, 0x02, 0x01, 0x05}, // Must staple feature
   297  }
   298  
   299  // checkValidations checks the database for matching authorizations that were
   300  // likely valid at the time the certificate was issued. Authorizations with
   301  // status = "deactivated" are counted for this, so long as their validatedAt
   302  // is before the issuance and expiration is after.
   303  func (c *certChecker) checkValidations(ctx context.Context, cert core.Certificate, dnsNames []string) error {
   304  	authzs, err := sa.SelectAuthzsMatchingIssuance(ctx, c.dbMap, cert.RegistrationID, cert.Issued, dnsNames)
   305  	if err != nil {
   306  		return fmt.Errorf("error checking authzs for certificate %s: %w", cert.Serial, err)
   307  	}
   308  
   309  	if len(authzs) == 0 {
   310  		return fmt.Errorf("no relevant authzs found valid at %s", cert.Issued)
   311  	}
   312  
   313  	// We may get multiple authorizations for the same name, but that's okay.
   314  	// Any authorization for a given name is sufficient.
   315  	nameToAuthz := make(map[string]*corepb.Authorization)
   316  	for _, m := range authzs {
   317  		nameToAuthz[m.Identifier] = m
   318  	}
   319  
   320  	var errors []error
   321  	for _, name := range dnsNames {
   322  		_, ok := nameToAuthz[name]
   323  		if !ok {
   324  			errors = append(errors, fmt.Errorf("missing authz for %q", name))
   325  			continue
   326  		}
   327  	}
   328  	if len(errors) > 0 {
   329  		return fmt.Errorf("%s", errors)
   330  	}
   331  	return nil
   332  }
   333  
   334  // checkCert returns a list of DNS names in the certificate and a list of problems with the certificate.
   335  func (c *certChecker) checkCert(ctx context.Context, cert core.Certificate, ignoredLints map[string]bool) ([]string, []string) {
   336  	var dnsNames []string
   337  	var problems []string
   338  
   339  	// Check that the digests match.
   340  	if cert.Digest != core.Fingerprint256(cert.DER) {
   341  		problems = append(problems, "Stored digest doesn't match certificate digest")
   342  	}
   343  	// Parse the certificate.
   344  	parsedCert, err := zX509.ParseCertificate(cert.DER)
   345  	if err != nil {
   346  		problems = append(problems, fmt.Sprintf("Couldn't parse stored certificate: %s", err))
   347  	} else {
   348  		dnsNames = parsedCert.DNSNames
   349  		// Run zlint checks.
   350  		results := zlint.LintCertificate(parsedCert)
   351  		for name, res := range results.Results {
   352  			if ignoredLints[name] || res.Status <= lint.Pass {
   353  				continue
   354  			}
   355  			prob := fmt.Sprintf("zlint %s: %s", res.Status, name)
   356  			if res.Details != "" {
   357  				prob = fmt.Sprintf("%s %s", prob, res.Details)
   358  			}
   359  			problems = append(problems, prob)
   360  		}
   361  		// Check if stored serial is correct.
   362  		storedSerial, err := core.StringToSerial(cert.Serial)
   363  		if err != nil {
   364  			problems = append(problems, "Stored serial is invalid")
   365  		} else if parsedCert.SerialNumber.Cmp(storedSerial) != 0 {
   366  			problems = append(problems, "Stored serial doesn't match certificate serial")
   367  		}
   368  		// Check that we have the correct expiration time.
   369  		if !parsedCert.NotAfter.Equal(cert.Expires) {
   370  			problems = append(problems, "Stored expiration doesn't match certificate NotAfter")
   371  		}
   372  		// Check if basic constraints are set.
   373  		if !parsedCert.BasicConstraintsValid {
   374  			problems = append(problems, "Certificate doesn't have basic constraints set")
   375  		}
   376  		// Check that the cert isn't able to sign other certificates.
   377  		if parsedCert.IsCA {
   378  			problems = append(problems, "Certificate can sign other certificates")
   379  		}
   380  		// Check that the cert has a valid validity period. The validity
   381  		// period is computed inclusive of the whole final second indicated by
   382  		// notAfter.
   383  		validityDuration := parsedCert.NotAfter.Add(time.Second).Sub(parsedCert.NotBefore)
   384  		_, ok := c.acceptableValidityDurations[validityDuration]
   385  		if !ok {
   386  			problems = append(problems, "Certificate has unacceptable validity period")
   387  		}
   388  		// Check that the stored issuance time isn't too far back/forward dated.
   389  		if parsedCert.NotBefore.Before(cert.Issued.Add(-6*time.Hour)) || parsedCert.NotBefore.After(cert.Issued.Add(6*time.Hour)) {
   390  			problems = append(problems, "Stored issuance date is outside of 6 hour window of certificate NotBefore")
   391  		}
   392  		// Check if the CommonName is <= 64 characters.
   393  		if len(parsedCert.Subject.CommonName) > 64 {
   394  			problems = append(
   395  				problems,
   396  				fmt.Sprintf("Certificate has common name >64 characters long (%d)", len(parsedCert.Subject.CommonName)),
   397  			)
   398  		}
   399  		// Check that the PA is still willing to issue for each name in DNSNames
   400  		// + CommonName.
   401  		for _, name := range append(parsedCert.DNSNames, parsedCert.Subject.CommonName) {
   402  			id := identifier.ACMEIdentifier{Type: identifier.DNS, Value: name}
   403  			err = c.pa.WillingToIssueWildcards([]identifier.ACMEIdentifier{id})
   404  			if err != nil {
   405  				problems = append(problems, fmt.Sprintf("Policy Authority isn't willing to issue for '%s': %s", name, err))
   406  			} else {
   407  				// For defense-in-depth, even if the PA was willing to issue for a name
   408  				// we double check it against a list of forbidden domains. This way even
   409  				// if the hostnamePolicyFile malfunctions we will flag the forbidden
   410  				// domain matches
   411  				if forbidden, pattern := isForbiddenDomain(name); forbidden {
   412  					problems = append(problems, fmt.Sprintf(
   413  						"Policy Authority was willing to issue but domain '%s' matches "+
   414  							"forbiddenDomains entry %q", name, pattern))
   415  				}
   416  			}
   417  		}
   418  		// Check the cert has the correct key usage extensions
   419  		if !slices.Equal(parsedCert.ExtKeyUsage, []zX509.ExtKeyUsage{zX509.ExtKeyUsageServerAuth, zX509.ExtKeyUsageClientAuth}) {
   420  			problems = append(problems, "Certificate has incorrect key usage extensions")
   421  		}
   422  
   423  		for _, ext := range parsedCert.Extensions {
   424  			_, ok := allowedExtensions[ext.Id.String()]
   425  			if !ok {
   426  				problems = append(problems, fmt.Sprintf("Certificate contains an unexpected extension: %s", ext.Id))
   427  			}
   428  			expectedContent, ok := expectedExtensionContent[ext.Id.String()]
   429  			if ok {
   430  				if !bytes.Equal(ext.Value, expectedContent) {
   431  					problems = append(problems, fmt.Sprintf("Certificate extension %s contains unexpected content: has %x, expected %x", ext.Id, ext.Value, expectedContent))
   432  				}
   433  			}
   434  		}
   435  
   436  		// Check that the cert has a good key. Note that this does not perform
   437  		// checks which rely on external resources such as weak or blocked key
   438  		// lists, or the list of blocked keys in the database. This only performs
   439  		// static checks, such as against the RSA key size and the ECDSA curve.
   440  		p, err := x509.ParseCertificate(cert.DER)
   441  		if err != nil {
   442  			problems = append(problems, fmt.Sprintf("Couldn't parse stored certificate: %s", err))
   443  		}
   444  		err = c.kp.GoodKey(ctx, p.PublicKey)
   445  		if err != nil {
   446  			problems = append(problems, fmt.Sprintf("Key Policy isn't willing to issue for public key: %s", err))
   447  		}
   448  
   449  		if features.Enabled(features.CertCheckerRequiresCorrespondence) {
   450  			precertDER, err := c.getPrecert(ctx, cert.Serial)
   451  			if err != nil {
   452  				// Log and continue, since we want the problems slice to only contains
   453  				// problems with the cert itself.
   454  				c.logger.Errf("fetching linting precertificate for %s: %s", cert.Serial, err)
   455  				atomic.AddInt64(&c.issuedReport.DbErrs, 1)
   456  			} else {
   457  				err = precert.Correspond(precertDER, cert.DER)
   458  				if err != nil {
   459  					problems = append(problems,
   460  						fmt.Sprintf("Certificate does not correspond to precert for %s: %s", cert.Serial, err))
   461  				}
   462  			}
   463  		}
   464  
   465  		if features.Enabled(features.CertCheckerChecksValidations) {
   466  			err = c.checkValidations(ctx, cert, parsedCert.DNSNames)
   467  			if err != nil {
   468  				if features.Enabled(features.CertCheckerRequiresValidations) {
   469  					problems = append(problems, err.Error())
   470  				} else {
   471  					c.logger.Errf("Certificate %s %s: %s", cert.Serial, parsedCert.DNSNames, err)
   472  				}
   473  			}
   474  		}
   475  	}
   476  	return dnsNames, problems
   477  }
   478  
   479  type Config struct {
   480  	CertChecker struct {
   481  		DB cmd.DBConfig
   482  		cmd.HostnamePolicyConfig
   483  
   484  		Workers int `validate:"required,min=1"`
   485  		// Deprecated: this is ignored, and cert checker always checks both expired and unexpired.
   486  		UnexpiredOnly  bool
   487  		BadResultsOnly bool
   488  		CheckPeriod    config.Duration
   489  
   490  		// AcceptableValidityDurations is a list of durations which are
   491  		// acceptable for certificates we issue.
   492  		AcceptableValidityDurations []config.Duration
   493  
   494  		// GoodKey is an embedded config stanza for the goodkey library. If this
   495  		// is populated, the cert-checker will perform static checks against the
   496  		// public keys in the certs it checks.
   497  		GoodKey goodkey.Config
   498  
   499  		// IgnoredLints is a list of zlint names. Any lint results from a lint in
   500  		// the IgnoredLists list are ignored regardless of LintStatus level.
   501  		IgnoredLints []string
   502  
   503  		// CTLogListFile is the path to a JSON file on disk containing the set of
   504  		// all logs trusted by Chrome. The file must match the v3 log list schema:
   505  		// https://www.gstatic.com/ct/log_list/v3/log_list_schema.json
   506  		CTLogListFile string
   507  
   508  		Features map[string]bool
   509  	}
   510  	PA     cmd.PAConfig
   511  	Syslog cmd.SyslogConfig
   512  }
   513  
   514  func main() {
   515  	configFile := flag.String("config", "", "File path to the configuration file for this service")
   516  	flag.Parse()
   517  	if *configFile == "" {
   518  		flag.Usage()
   519  		os.Exit(1)
   520  	}
   521  
   522  	var config Config
   523  	err := cmd.ReadConfigFile(*configFile, &config)
   524  	cmd.FailOnError(err, "Reading JSON config file into config structure")
   525  
   526  	err = features.Set(config.CertChecker.Features)
   527  	cmd.FailOnError(err, "Failed to set feature flags")
   528  
   529  	syslogger, err := syslog.Dial("", "", syslog.LOG_INFO|syslog.LOG_LOCAL0, "")
   530  	cmd.FailOnError(err, "Failed to dial syslog")
   531  
   532  	syslogLevel := int(syslog.LOG_INFO)
   533  	if config.Syslog.SyslogLevel != 0 {
   534  		syslogLevel = config.Syslog.SyslogLevel
   535  	}
   536  	logger, err := blog.New(syslogger, config.Syslog.StdoutLevel, syslogLevel)
   537  	cmd.FailOnError(err, "Could not connect to Syslog")
   538  
   539  	err = blog.Set(logger)
   540  	cmd.FailOnError(err, "Failed to set audit logger")
   541  
   542  	logger.Info(cmd.VersionString())
   543  
   544  	acceptableValidityDurations := make(map[time.Duration]bool)
   545  	if len(config.CertChecker.AcceptableValidityDurations) > 0 {
   546  		for _, entry := range config.CertChecker.AcceptableValidityDurations {
   547  			acceptableValidityDurations[entry.Duration] = true
   548  		}
   549  	} else {
   550  		// For backwards compatibility, assume only a single valid validity
   551  		// period of exactly 90 days if none is configured.
   552  		ninetyDays := (time.Hour * 24) * 90
   553  		acceptableValidityDurations[ninetyDays] = true
   554  	}
   555  
   556  	// Validate PA config and set defaults if needed.
   557  	cmd.FailOnError(config.PA.CheckChallenges(), "Invalid PA configuration")
   558  
   559  	if config.CertChecker.GoodKey.WeakKeyFile != "" {
   560  		cmd.Fail("cert-checker does not support checking against weak key files")
   561  	}
   562  	if config.CertChecker.GoodKey.BlockedKeyFile != "" {
   563  		cmd.Fail("cert-checker does not support checking against blocked key files")
   564  	}
   565  	kp, err := sagoodkey.NewKeyPolicy(&config.CertChecker.GoodKey, nil)
   566  	cmd.FailOnError(err, "Unable to create key policy")
   567  
   568  	saDbMap, err := sa.InitWrappedDb(config.CertChecker.DB, prometheus.DefaultRegisterer, logger)
   569  	cmd.FailOnError(err, "While initializing dbMap")
   570  
   571  	checkerLatency := prometheus.NewHistogram(prometheus.HistogramOpts{
   572  		Name: "cert_checker_latency",
   573  		Help: "Histogram of latencies a cert-checker worker takes to complete a batch",
   574  	})
   575  	prometheus.DefaultRegisterer.MustRegister(checkerLatency)
   576  
   577  	pa, err := policy.New(config.PA.Challenges, logger)
   578  	cmd.FailOnError(err, "Failed to create PA")
   579  
   580  	err = pa.LoadHostnamePolicyFile(config.CertChecker.HostnamePolicyFile)
   581  	cmd.FailOnError(err, "Failed to load HostnamePolicyFile")
   582  
   583  	if config.CertChecker.CTLogListFile != "" {
   584  		err = loglist.InitLintList(config.CertChecker.CTLogListFile)
   585  		cmd.FailOnError(err, "Failed to load CT Log List")
   586  	}
   587  
   588  	checker := newChecker(
   589  		saDbMap,
   590  		cmd.Clock(),
   591  		pa,
   592  		kp,
   593  		config.CertChecker.CheckPeriod.Duration,
   594  		acceptableValidityDurations,
   595  		logger,
   596  	)
   597  	fmt.Fprintf(os.Stderr, "# Getting certificates issued in the last %s\n", config.CertChecker.CheckPeriod)
   598  
   599  	ignoredLintsMap := make(map[string]bool)
   600  	for _, name := range config.CertChecker.IgnoredLints {
   601  		ignoredLintsMap[name] = true
   602  	}
   603  
   604  	// Since we grab certificates in batches we don't want this to block, when it
   605  	// is finished it will close the certificate channel which allows the range
   606  	// loops in checker.processCerts to break
   607  	go func() {
   608  		err := checker.getCerts(context.TODO())
   609  		cmd.FailOnError(err, "Batch retrieval of certificates failed")
   610  	}()
   611  
   612  	fmt.Fprintf(os.Stderr, "# Processing certificates using %d workers\n", config.CertChecker.Workers)
   613  	wg := new(sync.WaitGroup)
   614  	for i := 0; i < config.CertChecker.Workers; i++ {
   615  		wg.Add(1)
   616  		go func() {
   617  			s := checker.clock.Now()
   618  			checker.processCerts(context.TODO(), wg, config.CertChecker.BadResultsOnly, ignoredLintsMap)
   619  			checkerLatency.Observe(checker.clock.Since(s).Seconds())
   620  		}()
   621  	}
   622  	wg.Wait()
   623  	fmt.Fprintf(
   624  		os.Stderr,
   625  		"# Finished processing certificates, report length: %d, good: %d, bad: %d\n",
   626  		len(checker.issuedReport.Entries),
   627  		checker.issuedReport.GoodCerts,
   628  		checker.issuedReport.BadCerts,
   629  	)
   630  	err = checker.issuedReport.dump()
   631  	cmd.FailOnError(err, "Failed to dump results: %s\n")
   632  }
   633  
   634  func init() {
   635  	cmd.RegisterCommand("cert-checker", main, &cmd.ConfigValidator{Config: &Config{}})
   636  }
   637  

View as plain text