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 }