github.com/letsencrypt/boulder@v0.20251208.0/linter/lints/rfc/lint_cert_via_pkimetal.go (about) 1 package rfc 2 3 import ( 4 "context" 5 "encoding/base64" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "slices" 12 "strings" 13 "time" 14 15 "github.com/zmap/zcrypto/x509" 16 "github.com/zmap/zlint/v3/lint" 17 "github.com/zmap/zlint/v3/util" 18 ) 19 20 // PKIMetalConfig and its execute method provide a shared basis for linting 21 // both certs and CRLs using PKIMetal. 22 type PKIMetalConfig struct { 23 Addr string `toml:"addr" comment:"The address where a pkilint REST API can be reached."` 24 Severity string `toml:"severity" comment:"The minimum severity of findings to report (meta, debug, info, notice, warning, error, bug, or fatal)."` 25 Timeout time.Duration `toml:"timeout" comment:"How long, in nanoseconds, to wait before giving up."` 26 IgnoreLints []string `toml:"ignore_lints" comment:"The unique Validator:Code IDs of lint findings which should be ignored."` 27 } 28 29 func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResult, error) { 30 timeout := pkim.Timeout 31 if timeout == 0 { 32 timeout = 100 * time.Millisecond 33 } 34 35 ctx, cancel := context.WithTimeout(context.Background(), timeout) 36 defer cancel() 37 38 apiURL, err := url.JoinPath(pkim.Addr, endpoint) 39 if err != nil { 40 return nil, fmt.Errorf("constructing pkimetal url: %w", err) 41 } 42 43 // reqForm matches PKIMetal's documented form-urlencoded request format. It 44 // does not include the "profile" field, as its default value ("autodetect") 45 // is good for our purposes. 46 // https://github.com/pkimetal/pkimetal/blob/578ac224a7ca3775af51b47fce16c95753d9ac8d/doc/openapi.yaml#L179-L194 47 reqForm := url.Values{} 48 reqForm.Set("b64input", base64.StdEncoding.EncodeToString(der)) 49 reqForm.Set("severity", pkim.Severity) 50 reqForm.Set("format", "json") 51 52 req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(reqForm.Encode())) 53 if err != nil { 54 return nil, fmt.Errorf("creating pkimetal request: %w", err) 55 } 56 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 57 req.Header.Add("Accept", "application/json") 58 59 resp, err := http.DefaultClient.Do(req) 60 if err != nil { 61 return nil, fmt.Errorf("making POST request to pkimetal API: %s (timeout %s)", err, timeout) 62 } 63 defer resp.Body.Close() 64 65 if resp.StatusCode != http.StatusOK { 66 return nil, fmt.Errorf("got status %d (%s) from pkimetal API", resp.StatusCode, resp.Status) 67 } 68 69 resJSON, err := io.ReadAll(resp.Body) 70 if err != nil { 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"` 82 } 83 84 var res []finding 85 err = json.Unmarshal(resJSON, &res) 86 if err != nil { 87 return nil, fmt.Errorf("parsing response from pkimetal API: %s", err) 88 } 89 90 var findings []string 91 for _, finding := range res { 92 var id string 93 if finding.Code != "" { 94 id = fmt.Sprintf("%s:%s", finding.Linter, finding.Code) 95 } else { 96 id = fmt.Sprintf("%s:%s", finding.Linter, strings.ReplaceAll(strings.ToLower(finding.Finding), " ", "_")) 97 } 98 if slices.Contains(pkim.IgnoreLints, id) { 99 continue 100 } 101 desc := fmt.Sprintf("%s from %s: %s", finding.Severity, id, finding.Finding) 102 findings = append(findings, desc) 103 } 104 105 if len(findings) != 0 { 106 // Group the findings by severity, for human readers. 107 slices.Sort(findings) 108 return &lint.LintResult{ 109 Status: lint.Error, 110 Details: fmt.Sprintf("got %d lint findings from pkimetal API: %s", len(findings), strings.Join(findings, "; ")), 111 }, nil 112 } 113 114 return &lint.LintResult{Status: lint.Pass}, nil 115 } 116 117 type certViaPKIMetal struct { 118 PKIMetalConfig 119 } 120 121 func init() { 122 lint.RegisterCertificateLint(&lint.CertificateLint{ 123 LintMetadata: lint.LintMetadata{ 124 Name: "e_pkimetal_lint_cabf_serverauth_cert", 125 Description: "Runs pkimetal's suite of cabf serverauth certificate lints", 126 Citation: "https://github.com/pkimetal/pkimetal", 127 Source: lint.Community, 128 EffectiveDate: util.CABEffectiveDate, 129 }, 130 Lint: NewCertViaPKIMetal, 131 }) 132 } 133 134 func NewCertViaPKIMetal() lint.CertificateLintInterface { 135 return &certViaPKIMetal{} 136 } 137 138 func (l *certViaPKIMetal) Configure() any { 139 return l 140 } 141 142 func (l *certViaPKIMetal) CheckApplies(c *x509.Certificate) bool { 143 // This lint applies to all certificates issued by Boulder, as long as it has 144 // been configured with an address to reach out to. If not, skip it. 145 return l.Addr != "" 146 } 147 148 func (l *certViaPKIMetal) Execute(c *x509.Certificate) *lint.LintResult { 149 res, err := l.execute("lintcert", c.Raw) 150 if err != nil { 151 return &lint.LintResult{ 152 Status: lint.Error, 153 Details: err.Error(), 154 } 155 } 156 157 return res 158 }