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.
This commit is contained in:
parent
d045b387ef
commit
6071bedb52
|
@ -17,6 +17,13 @@ import (
|
|||
type CRLProfileConfig struct {
|
||||
ValidityInterval config.Duration
|
||||
MaxBackdate config.Duration
|
||||
|
||||
// LintConfig is a path to a zlint config file, which can be used to control
|
||||
// the behavior of zlint's "customizable lints".
|
||||
LintConfig string
|
||||
// IgnoredLints is a list of lint names that we know will fail for this
|
||||
// profile, and which we know it is safe to ignore.
|
||||
IgnoredLints []string
|
||||
}
|
||||
|
||||
type CRLProfile struct {
|
||||
|
@ -38,10 +45,17 @@ func NewCRLProfile(config CRLProfileConfig) (*CRLProfile, error) {
|
|||
return nil, fmt.Errorf("crl max backdate must be non-negative, got %q", config.MaxBackdate)
|
||||
}
|
||||
|
||||
reg, err := linter.NewRegistry(nil)
|
||||
reg, err := linter.NewRegistry(config.IgnoredLints)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating lint registry: %w", err)
|
||||
}
|
||||
if config.LintConfig != "" {
|
||||
lintconfig, err := lint.NewConfigFromFile(config.LintConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading zlint config file: %w", err)
|
||||
}
|
||||
reg.SetConfiguration(lintconfig)
|
||||
}
|
||||
|
||||
return &CRLProfile{
|
||||
validityInterval: config.ValidityInterval.Duration,
|
||||
|
|
|
@ -17,13 +17,105 @@ import (
|
|||
"github.com/zmap/zlint/v3/util"
|
||||
)
|
||||
|
||||
type certViaPKIMetal struct {
|
||||
// PKIMetalConfig and its execute method provide a shared basis for linting
|
||||
// both certs and CRLs using PKIMetal.
|
||||
type PKIMetalConfig struct {
|
||||
Addr string `toml:"addr" comment:"The address where a pkilint REST API can be reached."`
|
||||
Severity string `toml:"severity" comment:"The minimum severity of findings to report (meta, debug, info, notice, warning, error, bug, or fatal)."`
|
||||
Timeout time.Duration `toml:"timeout" comment:"How long, in nanoseconds, to wait before giving up."`
|
||||
IgnoreLints []string `toml:"ignore_lints" comment:"The unique Validator:Code IDs of lint findings which should be ignored."`
|
||||
}
|
||||
|
||||
func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResult, error) {
|
||||
timeout := pkim.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = 100 * time.Millisecond
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
apiURL, err := url.JoinPath(pkim.Addr, endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("constructing pkimetal url: %w", err)
|
||||
}
|
||||
|
||||
// reqForm matches PKIMetal's documented form-urlencoded request format. It
|
||||
// does not include the "profile" field, as its default value ("autodetect")
|
||||
// is good for our purposes.
|
||||
// https://github.com/pkimetal/pkimetal/blob/578ac224a7ca3775af51b47fce16c95753d9ac8d/doc/openapi.yaml#L179-L194
|
||||
reqForm := url.Values{}
|
||||
reqForm.Set("b64input", base64.StdEncoding.EncodeToString(der))
|
||||
reqForm.Set("severity", pkim.Severity)
|
||||
reqForm.Set("format", "json")
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(reqForm.Encode()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating pkimetal request: %w", err)
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Accept", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making POST request to pkimetal API: %s (timeout %s)", err, timeout)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("got status %d (%s) from pkimetal API", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
resJSON, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading response from pkimetal API: %s", err)
|
||||
}
|
||||
|
||||
// finding matches the repeated portion of PKIMetal's documented JSON response.
|
||||
// https://github.com/pkimetal/pkimetal/blob/578ac224a7ca3775af51b47fce16c95753d9ac8d/doc/openapi.yaml#L201-L221
|
||||
type finding struct {
|
||||
Linter string `json:"linter"`
|
||||
Finding string `json:"finding"`
|
||||
Severity string `json:"severity"`
|
||||
Code string `json:"code"`
|
||||
Field string `json:"field"`
|
||||
}
|
||||
|
||||
var res []finding
|
||||
err = json.Unmarshal(resJSON, &res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing response from pkimetal API: %s", err)
|
||||
}
|
||||
|
||||
var findings []string
|
||||
for _, finding := range res {
|
||||
id := fmt.Sprintf("%s:%s", finding.Linter, finding.Code)
|
||||
if slices.Contains(pkim.IgnoreLints, id) {
|
||||
continue
|
||||
}
|
||||
desc := fmt.Sprintf("%s from %s at %s", finding.Severity, id, finding.Field)
|
||||
if finding.Finding != "" {
|
||||
desc = fmt.Sprintf("%s: %s", desc, finding.Finding)
|
||||
}
|
||||
findings = append(findings, desc)
|
||||
}
|
||||
|
||||
if len(findings) != 0 {
|
||||
// Group the findings by severity, for human readers.
|
||||
slices.Sort(findings)
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: fmt.Sprintf("got %d lint findings from pkimetal API: %s", len(findings), strings.Join(findings, "; ")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &lint.LintResult{Status: lint.Pass}, nil
|
||||
}
|
||||
|
||||
type certViaPKIMetal struct {
|
||||
PKIMetalConfig
|
||||
}
|
||||
|
||||
func init() {
|
||||
lint.RegisterCertificateLint(&lint.CertificateLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
|
@ -51,98 +143,14 @@ func (l *certViaPKIMetal) CheckApplies(c *x509.Certificate) bool {
|
|||
return l.Addr != ""
|
||||
}
|
||||
|
||||
// finding matches the repeated portion of PKIMetal's documented JSON response.
|
||||
// https://github.com/pkimetal/pkimetal/blob/578ac224a7ca3775af51b47fce16c95753d9ac8d/doc/openapi.yaml#L201-L221
|
||||
type finding struct {
|
||||
Linter string `json:"linter"`
|
||||
Finding string `json:"finding"`
|
||||
Severity string `json:"severity"`
|
||||
Code string `json:"code"`
|
||||
Field string `json:"field"`
|
||||
}
|
||||
|
||||
func (l *certViaPKIMetal) Execute(c *x509.Certificate) *lint.LintResult {
|
||||
timeout := l.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = 100 * time.Millisecond
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// reqForm matches PKIMetal's documented form-urlencoded request format. It
|
||||
// does not include the "format" or "profile" fields, as their default values
|
||||
// ("json" and "autodetect", respectively) are good for our purposes.
|
||||
// https://github.com/pkimetal/pkimetal/blob/578ac224a7ca3775af51b47fce16c95753d9ac8d/doc/openapi.yaml#L179-L194
|
||||
reqForm := url.Values{}
|
||||
reqForm.Set("b64input", base64.StdEncoding.EncodeToString(c.Raw))
|
||||
reqForm.Set("severity", l.Severity)
|
||||
|
||||
url := fmt.Sprintf("%s/lintcert", l.Addr)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(reqForm.Encode()))
|
||||
res, err := l.execute("lintcert", c.Raw)
|
||||
if err != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: fmt.Sprintf("creating pkimetal request: %s", err),
|
||||
}
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Accept", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: fmt.Sprintf("making POST request to pkimetal API: %s (timeout %s)", err, timeout),
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: fmt.Sprintf("got status %d (%s) from pkimetal API", resp.StatusCode, resp.Status),
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
resJSON, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: fmt.Sprintf("reading response from pkimetal API: %s", err),
|
||||
}
|
||||
}
|
||||
|
||||
var res []finding
|
||||
err = json.Unmarshal(resJSON, &res)
|
||||
if err != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: fmt.Sprintf("parsing response from pkimetal API: %s", err),
|
||||
}
|
||||
}
|
||||
|
||||
var findings []string
|
||||
for _, finding := range res {
|
||||
id := fmt.Sprintf("%s:%s", finding.Linter, finding.Code)
|
||||
if slices.Contains(l.IgnoreLints, id) {
|
||||
continue
|
||||
}
|
||||
desc := fmt.Sprintf("%s from %s at %s", finding.Severity, id, finding.Field)
|
||||
if finding.Finding != "" {
|
||||
desc = fmt.Sprintf("%s: %s", desc, finding.Finding)
|
||||
}
|
||||
findings = append(findings, desc)
|
||||
}
|
||||
|
||||
if len(findings) != 0 {
|
||||
// Group the findings by severity, for human readers.
|
||||
slices.Sort(findings)
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: fmt.Sprintf("got %d lint findings from pkimetal API: %s", len(findings), strings.Join(findings, "; ")),
|
||||
}
|
||||
}
|
||||
|
||||
return &lint.LintResult{Status: lint.Pass}
|
||||
return res
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package rfc
|
||||
|
||||
import (
|
||||
"github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v3/lint"
|
||||
"github.com/zmap/zlint/v3/util"
|
||||
)
|
||||
|
||||
type crlViaPKIMetal struct {
|
||||
PKIMetalConfig
|
||||
}
|
||||
|
||||
func init() {
|
||||
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
||||
LintMetadata: lint.LintMetadata{
|
||||
Name: "e_pkimetal_lint_cabf_serverauth_crl",
|
||||
Description: "Runs pkimetal's suite of cabf serverauth CRL lints",
|
||||
Citation: "https://github.com/pkimetal/pkimetal",
|
||||
Source: lint.Community,
|
||||
EffectiveDate: util.CABEffectiveDate,
|
||||
},
|
||||
Lint: NewCrlViaPKIMetal,
|
||||
})
|
||||
}
|
||||
|
||||
func NewCrlViaPKIMetal() lint.RevocationListLintInterface {
|
||||
return &crlViaPKIMetal{}
|
||||
}
|
||||
|
||||
func (l *crlViaPKIMetal) Configure() any {
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *crlViaPKIMetal) CheckApplies(c *x509.RevocationList) bool {
|
||||
// This lint applies to all CRLs issued by Boulder, as long as it has
|
||||
// been configured with an address to reach out to. If not, skip it.
|
||||
return l.Addr != ""
|
||||
}
|
||||
|
||||
func (l *crlViaPKIMetal) Execute(c *x509.RevocationList) *lint.LintResult {
|
||||
res, err := l.execute("lintcrl", c.Raw)
|
||||
if err != nil {
|
||||
return &lint.LintResult{
|
||||
Status: lint.Error,
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
|
@ -118,7 +118,8 @@
|
|||
},
|
||||
"crlProfile": {
|
||||
"validityInterval": "216h",
|
||||
"maxBackdate": "1h5m"
|
||||
"maxBackdate": "1h5m",
|
||||
"lintConfig": "test/config-next/zlint.toml"
|
||||
},
|
||||
"issuers": [
|
||||
{
|
||||
|
|
|
@ -16,3 +16,9 @@ ignore_lints = [
|
|||
# and "shortlived" profiles.
|
||||
"pkilint:cabf.serverauth.subscriber_rsa_digitalsignature_and_keyencipherment_present",
|
||||
]
|
||||
|
||||
[e_pkimetal_lint_cabf_serverauth_crl]
|
||||
addr = "http://10.77.77.9:8080"
|
||||
severity = "notice"
|
||||
timeout = 2000000000 # 2 seconds
|
||||
ignore_lints = []
|
||||
|
|
|
@ -95,7 +95,8 @@
|
|||
},
|
||||
"crlProfile": {
|
||||
"validityInterval": "216h",
|
||||
"maxBackdate": "1h5m"
|
||||
"maxBackdate": "1h5m",
|
||||
"lintConfig": "test/config/zlint.toml"
|
||||
},
|
||||
"issuers": [
|
||||
{
|
||||
|
|
|
@ -16,3 +16,9 @@ ignore_lints = [
|
|||
# and "shortlived" profiles.
|
||||
"pkilint:cabf.serverauth.subscriber_rsa_digitalsignature_and_keyencipherment_present",
|
||||
]
|
||||
|
||||
[e_pkimetal_lint_cabf_serverauth_crl]
|
||||
addr = "http://10.77.77.9:8080"
|
||||
severity = "notice"
|
||||
timeout = 2000000000 # 2 seconds
|
||||
ignore_lints = []
|
||||
|
|
Loading…
Reference in New Issue