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

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