Skip to content

Commit 767c5d1

Browse files
authored
Improve how cert-checker runs lints (#8063)
Give cert-checker the ability to load zlint configs, so that it can be configured to talk to PKIMetal in CI and hopefully in staging/production in the future. Also update how cert-checker executes lints, so that it uses a real lint registry instead of using the global registry and passing around a dictionary of lints to filter out of the results. Fixes #7786
1 parent 5889d6a commit 767c5d1

File tree

3 files changed

+51
-36
lines changed

3 files changed

+51
-36
lines changed

cmd/cert-checker/main.go

+22-12
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import (
2929
"github.com/letsencrypt/boulder/features"
3030
"github.com/letsencrypt/boulder/goodkey"
3131
"github.com/letsencrypt/boulder/goodkey/sagoodkey"
32-
_ "github.com/letsencrypt/boulder/linter"
32+
"github.com/letsencrypt/boulder/linter"
3333
blog "github.com/letsencrypt/boulder/log"
3434
"github.com/letsencrypt/boulder/policy"
3535
"github.com/letsencrypt/boulder/precert"
@@ -105,6 +105,7 @@ type certChecker struct {
105105
issuedReport report
106106
checkPeriod time.Duration
107107
acceptableValidityDurations map[time.Duration]bool
108+
lints lint.Registry
108109
logger blog.Logger
109110
}
110111

@@ -114,6 +115,7 @@ func newChecker(saDbMap certDB,
114115
kp goodkey.KeyPolicy,
115116
period time.Duration,
116117
avd map[time.Duration]bool,
118+
lints lint.Registry,
117119
logger blog.Logger,
118120
) certChecker {
119121
precertGetter := func(ctx context.Context, serial string) ([]byte, error) {
@@ -134,6 +136,7 @@ func newChecker(saDbMap certDB,
134136
issuedReport: report{Entries: make(map[string]reportEntry)},
135137
checkPeriod: period,
136138
acceptableValidityDurations: avd,
139+
lints: lints,
137140
logger: logger,
138141
}
139142
}
@@ -252,9 +255,9 @@ func (c *certChecker) getCerts(ctx context.Context) error {
252255
return nil
253256
}
254257

255-
func (c *certChecker) processCerts(ctx context.Context, wg *sync.WaitGroup, badResultsOnly bool, ignoredLints map[string]bool) {
258+
func (c *certChecker) processCerts(ctx context.Context, wg *sync.WaitGroup, badResultsOnly bool) {
256259
for cert := range c.certs {
257-
dnsNames, problems := c.checkCert(ctx, cert, ignoredLints)
260+
dnsNames, problems := c.checkCert(ctx, cert)
258261
valid := len(problems) == 0
259262
c.rMu.Lock()
260263
if !badResultsOnly || (badResultsOnly && !valid) {
@@ -330,7 +333,7 @@ func (c *certChecker) checkValidations(ctx context.Context, cert core.Certificat
330333
}
331334

332335
// checkCert returns a list of DNS names in the certificate and a list of problems with the certificate.
333-
func (c *certChecker) checkCert(ctx context.Context, cert core.Certificate, ignoredLints map[string]bool) ([]string, []string) {
336+
func (c *certChecker) checkCert(ctx context.Context, cert core.Certificate) ([]string, []string) {
334337
var dnsNames []string
335338
var problems []string
336339

@@ -345,9 +348,9 @@ func (c *certChecker) checkCert(ctx context.Context, cert core.Certificate, igno
345348
} else {
346349
dnsNames = parsedCert.DNSNames
347350
// Run zlint checks.
348-
results := zlint.LintCertificate(parsedCert)
351+
results := zlint.LintCertificateEx(parsedCert, c.lints)
349352
for name, res := range results.Results {
350-
if ignoredLints[name] || res.Status <= lint.Pass {
353+
if res.Status <= lint.Pass {
351354
continue
352355
}
353356
prob := fmt.Sprintf("zlint %s: %s", res.Status, name)
@@ -502,6 +505,9 @@ type Config struct {
502505
// public keys in the certs it checks.
503506
GoodKey goodkey.Config
504507

508+
// LintConfig is a path to a zlint config file, which can be used to control
509+
// the behavior of zlint's "customizable lints".
510+
LintConfig string
505511
// IgnoredLints is a list of zlint names. Any lint results from a lint in
506512
// the IgnoredLists list are ignored regardless of LintStatus level.
507513
IgnoredLints []string
@@ -572,22 +578,26 @@ func main() {
572578
cmd.FailOnError(err, "Failed to load CT Log List")
573579
}
574580

581+
lints, err := linter.NewRegistry(config.CertChecker.IgnoredLints)
582+
cmd.FailOnError(err, "Failed to create zlint registry")
583+
if config.CertChecker.LintConfig != "" {
584+
lintconfig, err := lint.NewConfigFromFile(config.CertChecker.LintConfig)
585+
cmd.FailOnError(err, "Failed to load zlint config file")
586+
lints.SetConfiguration(lintconfig)
587+
}
588+
575589
checker := newChecker(
576590
saDbMap,
577591
cmd.Clock(),
578592
pa,
579593
kp,
580594
config.CertChecker.CheckPeriod.Duration,
581595
acceptableValidityDurations,
596+
lints,
582597
logger,
583598
)
584599
fmt.Fprintf(os.Stderr, "# Getting certificates issued in the last %s\n", config.CertChecker.CheckPeriod)
585600

586-
ignoredLintsMap := make(map[string]bool)
587-
for _, name := range config.CertChecker.IgnoredLints {
588-
ignoredLintsMap[name] = true
589-
}
590-
591601
// Since we grab certificates in batches we don't want this to block, when it
592602
// is finished it will close the certificate channel which allows the range
593603
// loops in checker.processCerts to break
@@ -602,7 +612,7 @@ func main() {
602612
wg.Add(1)
603613
go func() {
604614
s := checker.clock.Now()
605-
checker.processCerts(context.TODO(), wg, config.CertChecker.BadResultsOnly, ignoredLintsMap)
615+
checker.processCerts(context.TODO(), wg, config.CertChecker.BadResultsOnly)
606616
checkerLatency.Observe(checker.clock.Since(s).Seconds())
607617
}()
608618
}

cmd/cert-checker/main_test.go

+28-24
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/letsencrypt/boulder/ctpolicy/loglist"
3131
"github.com/letsencrypt/boulder/goodkey"
3232
"github.com/letsencrypt/boulder/goodkey/sagoodkey"
33+
"github.com/letsencrypt/boulder/linter"
3334
blog "github.com/letsencrypt/boulder/log"
3435
"github.com/letsencrypt/boulder/metrics"
3536
"github.com/letsencrypt/boulder/policy"
@@ -65,7 +66,7 @@ func init() {
6566
}
6667

6768
func BenchmarkCheckCert(b *testing.B) {
68-
checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
69+
checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
6970
testKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
7071
expiry := time.Now().AddDate(0, 0, 1)
7172
serial := big.NewInt(1337)
@@ -87,7 +88,7 @@ func BenchmarkCheckCert(b *testing.B) {
8788
}
8889
b.ResetTimer()
8990
for range b.N {
90-
checker.checkCert(context.Background(), cert, nil)
91+
checker.checkCert(context.Background(), cert)
9192
}
9293
}
9394

@@ -101,7 +102,7 @@ func TestCheckWildcardCert(t *testing.T) {
101102

102103
testKey, _ := rsa.GenerateKey(rand.Reader, 2048)
103104
fc := clock.NewFake()
104-
checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, blog.NewMock())
105+
checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
105106
issued := checker.clock.Now().Add(-time.Minute)
106107
goodExpiry := issued.Add(testValidityDuration - time.Second)
107108
serial := big.NewInt(1337)
@@ -131,7 +132,7 @@ func TestCheckWildcardCert(t *testing.T) {
131132
Issued: parsed.NotBefore,
132133
DER: wildcardCertDer,
133134
}
134-
_, problems := checker.checkCert(context.Background(), cert, nil)
135+
_, problems := checker.checkCert(context.Background(), cert)
135136
for _, p := range problems {
136137
t.Error(p)
137138
}
@@ -144,7 +145,7 @@ func TestCheckCertReturnsDNSNames(t *testing.T) {
144145
defer func() {
145146
saCleanup()
146147
}()
147-
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
148+
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
148149

149150
certPEM, err := os.ReadFile("testdata/quite_invalid.pem")
150151
if err != nil {
@@ -164,7 +165,7 @@ func TestCheckCertReturnsDNSNames(t *testing.T) {
164165
DER: block.Bytes,
165166
}
166167

167-
names, problems := checker.checkCert(context.Background(), cert, nil)
168+
names, problems := checker.checkCert(context.Background(), cert)
168169
if !slices.Equal(names, []string{"quite_invalid.com", "al--so--wr--ong.com"}) {
169170
t.Errorf("didn't get expected DNS names. other problems: %s", strings.Join(problems, "\n"))
170171
}
@@ -211,7 +212,7 @@ func TestCheckCert(t *testing.T) {
211212
t.Run(tc.name, func(t *testing.T) {
212213
testKey, _ := tc.key.genKey()
213214

214-
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
215+
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
215216

216217
// Create a RFC 7633 OCSP Must Staple Extension.
217218
// OID 1.3.6.1.5.5.7.1.24
@@ -268,7 +269,7 @@ func TestCheckCert(t *testing.T) {
268269
Expires: goodExpiry.AddDate(0, 0, 2), // Expiration doesn't match
269270
}
270271

271-
_, problems := checker.checkCert(context.Background(), cert, nil)
272+
_, problems := checker.checkCert(context.Background(), cert)
272273

273274
problemsMap := map[string]int{
274275
"Stored digest doesn't match certificate digest": 1,
@@ -295,7 +296,7 @@ func TestCheckCert(t *testing.T) {
295296

296297
// Same settings as above, but the stored serial number in the DB is invalid.
297298
cert.Serial = "not valid"
298-
_, problems = checker.checkCert(context.Background(), cert, nil)
299+
_, problems = checker.checkCert(context.Background(), cert)
299300
foundInvalidSerialProblem := false
300301
for _, p := range problems {
301302
if p == "Stored serial is invalid" {
@@ -320,7 +321,7 @@ func TestCheckCert(t *testing.T) {
320321
cert.DER = goodCertDer
321322
cert.Expires = parsed.NotAfter
322323
cert.Issued = parsed.NotBefore
323-
_, problems = checker.checkCert(context.Background(), cert, nil)
324+
_, problems = checker.checkCert(context.Background(), cert)
324325
test.AssertEquals(t, len(problems), 0)
325326
})
326327
}
@@ -332,7 +333,7 @@ func TestGetAndProcessCerts(t *testing.T) {
332333
fc := clock.NewFake()
333334
fc.Set(fc.Now().Add(time.Hour))
334335

335-
checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, blog.NewMock())
336+
checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
336337
sa, err := sa.NewSQLStorageAuthority(saDbMap, saDbMap, nil, 1, 0, fc, blog.NewMock(), metrics.NoopRegisterer)
337338
test.AssertNotError(t, err, "Couldn't create SA to insert certificates")
338339
saCleanUp := test.ResetBoulderTestDatabase(t)
@@ -371,7 +372,7 @@ func TestGetAndProcessCerts(t *testing.T) {
371372
test.AssertEquals(t, len(checker.certs), 5)
372373
wg := new(sync.WaitGroup)
373374
wg.Add(1)
374-
checker.processCerts(context.Background(), wg, false, nil)
375+
checker.processCerts(context.Background(), wg, false)
375376
test.AssertEquals(t, checker.issuedReport.BadCerts, int64(5))
376377
test.AssertEquals(t, len(checker.issuedReport.Entries), 5)
377378
}
@@ -426,7 +427,7 @@ func (db mismatchedCountDB) SelectOne(_ context.Context, _ interface{}, _ string
426427
func TestGetCertsEmptyResults(t *testing.T) {
427428
saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
428429
test.AssertNotError(t, err, "Couldn't connect to database")
429-
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
430+
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
430431
checker.dbMap = mismatchedCountDB{}
431432

432433
batchSize = 3
@@ -452,7 +453,7 @@ func (db emptyDB) SelectNullInt(_ context.Context, _ string, _ ...interface{}) (
452453
// expected if the DB finds no certificates to match the SELECT query and
453454
// should return an error.
454455
func TestGetCertsNullResults(t *testing.T) {
455-
checker := newChecker(emptyDB{}, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
456+
checker := newChecker(emptyDB{}, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
456457

457458
err := checker.getCerts(context.Background())
458459
test.AssertError(t, err, "Should have gotten error from empty DB")
@@ -496,7 +497,7 @@ func TestGetCertsLate(t *testing.T) {
496497
clk := clock.NewFake()
497498
db := &lateDB{issuedTime: clk.Now().Add(-time.Hour)}
498499
checkPeriod := 24 * time.Hour
499-
checker := newChecker(db, clk, pa, kp, checkPeriod, testValidityDurations, blog.NewMock())
500+
checker := newChecker(db, clk, pa, kp, checkPeriod, testValidityDurations, nil, blog.NewMock())
500501

501502
err := checker.getCerts(context.Background())
502503
test.AssertNotError(t, err, "getting certs")
@@ -581,7 +582,7 @@ func TestIgnoredLint(t *testing.T) {
581582
err = loglist.InitLintList("../../test/ct-test-srv/log_list.json")
582583
test.AssertNotError(t, err, "failed to load ct log list")
583584
testKey, _ := rsa.GenerateKey(rand.Reader, 2048)
584-
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
585+
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
585586
serial := big.NewInt(1337)
586587

587588
x509OID, err := x509.OIDFromInts([]uint64{1, 2, 3})
@@ -643,23 +644,26 @@ func TestIgnoredLint(t *testing.T) {
643644

644645
// Check the certificate with a nil ignore map. This should return the
645646
// expected zlint problems.
646-
_, problems := checker.checkCert(context.Background(), cert, nil)
647+
_, problems := checker.checkCert(context.Background(), cert)
647648
slices.Sort(problems)
648649
test.AssertDeepEquals(t, problems, expectedProblems)
649650

650651
// Check the certificate again with an ignore map that excludes the affected
651652
// lints. This should return no problems.
652-
_, problems = checker.checkCert(context.Background(), cert, map[string]bool{
653-
"w_subject_common_name_included": true,
654-
"w_ext_subject_key_identifier_not_recommended_subscriber": true,
655-
"w_ct_sct_policy_count_unsatisfied": true,
656-
"e_scts_from_same_operator": true,
653+
lints, err := linter.NewRegistry([]string{
654+
"w_subject_common_name_included",
655+
"w_ext_subject_key_identifier_not_recommended_subscriber",
656+
"w_ct_sct_policy_count_unsatisfied",
657+
"e_scts_from_same_operator",
657658
})
659+
test.AssertNotError(t, err, "creating test lint registry")
660+
checker.lints = lints
661+
_, problems = checker.checkCert(context.Background(), cert)
658662
test.AssertEquals(t, len(problems), 0)
659663
}
660664

661665
func TestPrecertCorrespond(t *testing.T) {
662-
checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
666+
checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
663667
checker.getPrecert = func(_ context.Context, _ string) ([]byte, error) {
664668
return []byte("hello"), nil
665669
}
@@ -682,7 +686,7 @@ func TestPrecertCorrespond(t *testing.T) {
682686
Issued: time.Now(),
683687
Expires: expiry,
684688
}
685-
_, problems := checker.checkCert(context.Background(), cert, nil)
689+
_, problems := checker.checkCert(context.Background(), cert)
686690
if len(problems) == 0 {
687691
t.Errorf("expected precert correspondence problem")
688692
}

test/config-next/cert-checker.json

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"acceptableValidityDurations": [
1313
"7776000s"
1414
],
15+
"lintConfig": "test/config-next/zlint.toml",
1516
"ignoredLints": [
1617
"w_subject_common_name_included",
1718
"w_ext_subject_key_identifier_missing_sub_cert",

0 commit comments

Comments
 (0)