11package rfc
22
33import (
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-
154 "github.com/zmap/zcrypto/x509"
165 "github.com/zmap/zlint/v3/lint"
176 "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- }
687
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- }
8+ "github.com/letsencrypt/boulder/linter/pkimetal"
9+ )
11610
11711type certViaPKIMetal struct {
118- PKIMetalConfig
12+ pkimetal. Client
11913}
12014
12115func init () {
@@ -136,17 +30,17 @@ func NewCertViaPKIMetal() lint.CertificateLintInterface {
13630}
13731
13832func (l * certViaPKIMetal ) Configure () any {
139- return l
33+ return & l . Config
14034}
14135
14236func (l * certViaPKIMetal ) CheckApplies (c * x509.Certificate ) bool {
14337 // 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 != ""
38+ // been configured with a socket to reach out to. If not, skip it.
39+ return l .Enabled ()
14640}
14741
14842func (l * certViaPKIMetal ) Execute (c * x509.Certificate ) * lint.LintResult {
149- res , err := l .execute ("lintcert" , c .Raw )
43+ res , err := l .Client . Execute ("lintcert" , c .Raw )
15044 if err != nil {
15145 return & lint.LintResult {
15246 Status : lint .Error ,
0 commit comments