Skip to content

Commit 6071bed

Browse files
authored
Use PKIMetal to lint CRLs in CI (#8061)
Add a new custom lint which sends CRLs to PKIMetal, and configure it to run in our integration test environment. Factor out most of the code used to talk to the PKIMetal API so that it can be shared by the two custom lints which do so. Add the ability to configure lints to the CRLProfileConfig, so that zlint knows where to load the necessary custom config from.
1 parent d045b38 commit 6071bed

File tree

7 files changed

+157
-71
lines changed

7 files changed

+157
-71
lines changed

issuance/crl.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ import (
1717
type CRLProfileConfig struct {
1818
ValidityInterval config.Duration
1919
MaxBackdate config.Duration
20+
21+
// LintConfig is a path to a zlint config file, which can be used to control
22+
// the behavior of zlint's "customizable lints".
23+
LintConfig string
24+
// IgnoredLints is a list of lint names that we know will fail for this
25+
// profile, and which we know it is safe to ignore.
26+
IgnoredLints []string
2027
}
2128

2229
type CRLProfile struct {
@@ -38,10 +45,17 @@ func NewCRLProfile(config CRLProfileConfig) (*CRLProfile, error) {
3845
return nil, fmt.Errorf("crl max backdate must be non-negative, got %q", config.MaxBackdate)
3946
}
4047

41-
reg, err := linter.NewRegistry(nil)
48+
reg, err := linter.NewRegistry(config.IgnoredLints)
4249
if err != nil {
4350
return nil, fmt.Errorf("creating lint registry: %w", err)
4451
}
52+
if config.LintConfig != "" {
53+
lintconfig, err := lint.NewConfigFromFile(config.LintConfig)
54+
if err != nil {
55+
return nil, fmt.Errorf("loading zlint config file: %w", err)
56+
}
57+
reg.SetConfiguration(lintconfig)
58+
}
4559

4660
return &CRLProfile{
4761
validityInterval: config.ValidityInterval.Duration,

linter/lints/rfc/lint_cert_via_pkimetal.go

Lines changed: 76 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -17,115 +17,80 @@ import (
1717
"github.com/zmap/zlint/v3/util"
1818
)
1919

20-
type certViaPKIMetal struct {
20+
// PKIMetalConfig and its execute method provide a shared basis for linting
21+
// both certs and CRLs using PKIMetal.
22+
type PKIMetalConfig struct {
2123
Addr string `toml:"addr" comment:"The address where a pkilint REST API can be reached."`
2224
Severity string `toml:"severity" comment:"The minimum severity of findings to report (meta, debug, info, notice, warning, error, bug, or fatal)."`
2325
Timeout time.Duration `toml:"timeout" comment:"How long, in nanoseconds, to wait before giving up."`
2426
IgnoreLints []string `toml:"ignore_lints" comment:"The unique Validator:Code IDs of lint findings which should be ignored."`
2527
}
2628

27-
func init() {
28-
lint.RegisterCertificateLint(&lint.CertificateLint{
29-
LintMetadata: lint.LintMetadata{
30-
Name: "e_pkimetal_lint_cabf_serverauth_cert",
31-
Description: "Runs pkimetal's suite of cabf serverauth certificate lints",
32-
Citation: "https://github.com/pkimetal/pkimetal",
33-
Source: lint.Community,
34-
EffectiveDate: util.CABEffectiveDate,
35-
},
36-
Lint: NewCertViaPKIMetal,
37-
})
38-
}
39-
40-
func NewCertViaPKIMetal() lint.CertificateLintInterface {
41-
return &certViaPKIMetal{}
42-
}
43-
44-
func (l *certViaPKIMetal) Configure() any {
45-
return l
46-
}
47-
48-
func (l *certViaPKIMetal) CheckApplies(c *x509.Certificate) bool {
49-
// This lint applies to all certificates issued by Boulder, as long as it has
50-
// been configured with an address to reach out to. If not, skip it.
51-
return l.Addr != ""
52-
}
53-
54-
// finding matches the repeated portion of PKIMetal's documented JSON response.
55-
// https://github.com/pkimetal/pkimetal/blob/578ac224a7ca3775af51b47fce16c95753d9ac8d/doc/openapi.yaml#L201-L221
56-
type finding struct {
57-
Linter string `json:"linter"`
58-
Finding string `json:"finding"`
59-
Severity string `json:"severity"`
60-
Code string `json:"code"`
61-
Field string `json:"field"`
62-
}
63-
64-
func (l *certViaPKIMetal) Execute(c *x509.Certificate) *lint.LintResult {
65-
timeout := l.Timeout
29+
func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResult, error) {
30+
timeout := pkim.Timeout
6631
if timeout == 0 {
6732
timeout = 100 * time.Millisecond
6833
}
6934

7035
ctx, cancel := context.WithTimeout(context.Background(), timeout)
7136
defer cancel()
7237

38+
apiURL, err := url.JoinPath(pkim.Addr, endpoint)
39+
if err != nil {
40+
return nil, fmt.Errorf("constructing pkimetal url: %w", err)
41+
}
42+
7343
// reqForm matches PKIMetal's documented form-urlencoded request format. It
74-
// does not include the "format" or "profile" fields, as their default values
75-
// ("json" and "autodetect", respectively) are good for our purposes.
44+
// does not include the "profile" field, as its default value ("autodetect")
45+
// is good for our purposes.
7646
// https://github.com/pkimetal/pkimetal/blob/578ac224a7ca3775af51b47fce16c95753d9ac8d/doc/openapi.yaml#L179-L194
7747
reqForm := url.Values{}
78-
reqForm.Set("b64input", base64.StdEncoding.EncodeToString(c.Raw))
79-
reqForm.Set("severity", l.Severity)
48+
reqForm.Set("b64input", base64.StdEncoding.EncodeToString(der))
49+
reqForm.Set("severity", pkim.Severity)
50+
reqForm.Set("format", "json")
8051

81-
url := fmt.Sprintf("%s/lintcert", l.Addr)
82-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(reqForm.Encode()))
52+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(reqForm.Encode()))
8353
if err != nil {
84-
return &lint.LintResult{
85-
Status: lint.Error,
86-
Details: fmt.Sprintf("creating pkimetal request: %s", err),
87-
}
54+
return nil, fmt.Errorf("creating pkimetal request: %w", err)
8855
}
8956
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
9057
req.Header.Add("Accept", "application/json")
9158

9259
resp, err := http.DefaultClient.Do(req)
9360
if err != nil {
94-
return &lint.LintResult{
95-
Status: lint.Error,
96-
Details: fmt.Sprintf("making POST request to pkimetal API: %s (timeout %s)", err, timeout),
97-
}
61+
return nil, fmt.Errorf("making POST request to pkimetal API: %s (timeout %s)", err, timeout)
9862
}
9963
defer resp.Body.Close()
10064

10165
if resp.StatusCode != http.StatusOK {
102-
return &lint.LintResult{
103-
Status: lint.Error,
104-
Details: fmt.Sprintf("got status %d (%s) from pkimetal API", resp.StatusCode, resp.Status),
105-
}
66+
return nil, fmt.Errorf("got status %d (%s) from pkimetal API", resp.StatusCode, resp.Status)
10667
}
10768

10869
resJSON, err := io.ReadAll(resp.Body)
10970
if err != nil {
110-
return &lint.LintResult{
111-
Status: lint.Error,
112-
Details: fmt.Sprintf("reading response from pkimetal API: %s", err),
113-
}
71+
return nil, fmt.Errorf("reading response from pkimetal API: %s", err)
72+
}
73+
74+
// finding matches the repeated portion of PKIMetal's documented JSON response.
75+
// https://github.com/pkimetal/pkimetal/blob/578ac224a7ca3775af51b47fce16c95753d9ac8d/doc/openapi.yaml#L201-L221
76+
type finding struct {
77+
Linter string `json:"linter"`
78+
Finding string `json:"finding"`
79+
Severity string `json:"severity"`
80+
Code string `json:"code"`
81+
Field string `json:"field"`
11482
}
11583

11684
var res []finding
11785
err = json.Unmarshal(resJSON, &res)
11886
if err != nil {
119-
return &lint.LintResult{
120-
Status: lint.Error,
121-
Details: fmt.Sprintf("parsing response from pkimetal API: %s", err),
122-
}
87+
return nil, fmt.Errorf("parsing response from pkimetal API: %s", err)
12388
}
12489

12590
var findings []string
12691
for _, finding := range res {
12792
id := fmt.Sprintf("%s:%s", finding.Linter, finding.Code)
128-
if slices.Contains(l.IgnoreLints, id) {
93+
if slices.Contains(pkim.IgnoreLints, id) {
12994
continue
13095
}
13196
desc := fmt.Sprintf("%s from %s at %s", finding.Severity, id, finding.Field)
@@ -141,8 +106,51 @@ func (l *certViaPKIMetal) Execute(c *x509.Certificate) *lint.LintResult {
141106
return &lint.LintResult{
142107
Status: lint.Error,
143108
Details: fmt.Sprintf("got %d lint findings from pkimetal API: %s", len(findings), strings.Join(findings, "; ")),
109+
}, nil
110+
}
111+
112+
return &lint.LintResult{Status: lint.Pass}, nil
113+
}
114+
115+
type certViaPKIMetal struct {
116+
PKIMetalConfig
117+
}
118+
119+
func init() {
120+
lint.RegisterCertificateLint(&lint.CertificateLint{
121+
LintMetadata: lint.LintMetadata{
122+
Name: "e_pkimetal_lint_cabf_serverauth_cert",
123+
Description: "Runs pkimetal's suite of cabf serverauth certificate lints",
124+
Citation: "https://github.com/pkimetal/pkimetal",
125+
Source: lint.Community,
126+
EffectiveDate: util.CABEffectiveDate,
127+
},
128+
Lint: NewCertViaPKIMetal,
129+
})
130+
}
131+
132+
func NewCertViaPKIMetal() lint.CertificateLintInterface {
133+
return &certViaPKIMetal{}
134+
}
135+
136+
func (l *certViaPKIMetal) Configure() any {
137+
return l
138+
}
139+
140+
func (l *certViaPKIMetal) CheckApplies(c *x509.Certificate) bool {
141+
// This lint applies to all certificates issued by Boulder, as long as it has
142+
// been configured with an address to reach out to. If not, skip it.
143+
return l.Addr != ""
144+
}
145+
146+
func (l *certViaPKIMetal) Execute(c *x509.Certificate) *lint.LintResult {
147+
res, err := l.execute("lintcert", c.Raw)
148+
if err != nil {
149+
return &lint.LintResult{
150+
Status: lint.Error,
151+
Details: err.Error(),
144152
}
145153
}
146154

147-
return &lint.LintResult{Status: lint.Pass}
155+
return res
148156
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package rfc
2+
3+
import (
4+
"github.com/zmap/zcrypto/x509"
5+
"github.com/zmap/zlint/v3/lint"
6+
"github.com/zmap/zlint/v3/util"
7+
)
8+
9+
type crlViaPKIMetal struct {
10+
PKIMetalConfig
11+
}
12+
13+
func init() {
14+
lint.RegisterRevocationListLint(&lint.RevocationListLint{
15+
LintMetadata: lint.LintMetadata{
16+
Name: "e_pkimetal_lint_cabf_serverauth_crl",
17+
Description: "Runs pkimetal's suite of cabf serverauth CRL lints",
18+
Citation: "https://github.com/pkimetal/pkimetal",
19+
Source: lint.Community,
20+
EffectiveDate: util.CABEffectiveDate,
21+
},
22+
Lint: NewCrlViaPKIMetal,
23+
})
24+
}
25+
26+
func NewCrlViaPKIMetal() lint.RevocationListLintInterface {
27+
return &crlViaPKIMetal{}
28+
}
29+
30+
func (l *crlViaPKIMetal) Configure() any {
31+
return l
32+
}
33+
34+
func (l *crlViaPKIMetal) CheckApplies(c *x509.RevocationList) bool {
35+
// This lint applies to all CRLs issued by Boulder, as long as it has
36+
// been configured with an address to reach out to. If not, skip it.
37+
return l.Addr != ""
38+
}
39+
40+
func (l *crlViaPKIMetal) Execute(c *x509.RevocationList) *lint.LintResult {
41+
res, err := l.execute("lintcrl", c.Raw)
42+
if err != nil {
43+
return &lint.LintResult{
44+
Status: lint.Error,
45+
Details: err.Error(),
46+
}
47+
}
48+
49+
return res
50+
}

test/config-next/ca.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@
118118
},
119119
"crlProfile": {
120120
"validityInterval": "216h",
121-
"maxBackdate": "1h5m"
121+
"maxBackdate": "1h5m",
122+
"lintConfig": "test/config-next/zlint.toml"
122123
},
123124
"issuers": [
124125
{

test/config-next/zlint.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,9 @@ ignore_lints = [
1616
# and "shortlived" profiles.
1717
"pkilint:cabf.serverauth.subscriber_rsa_digitalsignature_and_keyencipherment_present",
1818
]
19+
20+
[e_pkimetal_lint_cabf_serverauth_crl]
21+
addr = "http://10.77.77.9:8080"
22+
severity = "notice"
23+
timeout = 2000000000 # 2 seconds
24+
ignore_lints = []

test/config/ca.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@
9595
},
9696
"crlProfile": {
9797
"validityInterval": "216h",
98-
"maxBackdate": "1h5m"
98+
"maxBackdate": "1h5m",
99+
"lintConfig": "test/config/zlint.toml"
99100
},
100101
"issuers": [
101102
{

test/config/zlint.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,9 @@ ignore_lints = [
1616
# and "shortlived" profiles.
1717
"pkilint:cabf.serverauth.subscriber_rsa_digitalsignature_and_keyencipherment_present",
1818
]
19+
20+
[e_pkimetal_lint_cabf_serverauth_crl]
21+
addr = "http://10.77.77.9:8080"
22+
severity = "notice"
23+
timeout = 2000000000 # 2 seconds
24+
ignore_lints = []

0 commit comments

Comments
 (0)