1 package notmain
2
3 import (
4 "crypto/x509"
5 "encoding/json"
6 "flag"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "os"
12 "strings"
13 "time"
14
15 "github.com/letsencrypt/boulder/cmd"
16 "github.com/letsencrypt/boulder/core"
17 "github.com/letsencrypt/boulder/crl/checker"
18 )
19
20 func downloadShard(url string) (*x509.RevocationList, error) {
21 resp, err := http.Get(url)
22 if err != nil {
23 return nil, fmt.Errorf("downloading crl: %w", err)
24 }
25 if resp.StatusCode != http.StatusOK {
26 return nil, fmt.Errorf("downloading crl: http status %d", resp.StatusCode)
27 }
28
29 crlBytes, err := io.ReadAll(resp.Body)
30 if err != nil {
31 return nil, fmt.Errorf("reading CRL bytes: %w", err)
32 }
33
34 crl, err := x509.ParseRevocationList(crlBytes)
35 if err != nil {
36 return nil, fmt.Errorf("parsing CRL: %w", err)
37 }
38
39 return crl, nil
40 }
41
42 func main() {
43 urlFile := flag.String("crls", "", "path to a file containing a JSON Array of CRL URLs")
44 issuerFile := flag.String("issuer", "", "path to an issuer certificate on disk, required, '-' to disable validation")
45 ageLimitStr := flag.String("ageLimit", "168h", "maximum allowable age of a CRL shard")
46 emitRevoked := flag.Bool("emitRevoked", false, "emit revoked serial numbers on stdout, one per line, hex-encoded")
47 save := flag.Bool("save", false, "save CRLs to files named after the URL")
48 flag.Parse()
49
50 logger := cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 6, SyslogLevel: -1})
51 logger.Info(cmd.VersionString())
52
53 urlFileContents, err := os.ReadFile(*urlFile)
54 cmd.FailOnError(err, "Reading CRL URLs file")
55
56 var urls []string
57 err = json.Unmarshal(urlFileContents, &urls)
58 cmd.FailOnError(err, "Parsing JSON Array of CRL URLs")
59
60 if *issuerFile == "" {
61 cmd.Fail("-issuer is required, but may be '-' to disable validation")
62 }
63
64 var issuer *x509.Certificate
65 if *issuerFile != "-" {
66 issuer, err = core.LoadCert(*issuerFile)
67 cmd.FailOnError(err, "Loading issuer certificate")
68 } else {
69 logger.Warning("CRL signature validation disabled")
70 }
71
72 ageLimit, err := time.ParseDuration(*ageLimitStr)
73 cmd.FailOnError(err, "Parsing age limit")
74
75 errCount := 0
76 seenSerials := make(map[string]struct{})
77 totalBytes := 0
78 oldestTimestamp := time.Time{}
79 for _, u := range urls {
80 crl, err := downloadShard(u)
81 if err != nil {
82 errCount += 1
83 logger.Errf("fetching CRL %q failed: %s", u, err)
84 continue
85 }
86
87 if *save {
88 parsedURL, err := url.Parse(u)
89 if err != nil {
90 logger.Errf("parsing url: %s", err)
91 continue
92 }
93 filename := fmt.Sprintf("%s%s", parsedURL.Host, strings.ReplaceAll(parsedURL.Path, "/", "_"))
94 err = os.WriteFile(filename, crl.Raw, 0660)
95 if err != nil {
96 logger.Errf("writing file: %s", err)
97 continue
98 }
99 }
100
101 totalBytes += len(crl.Raw)
102
103 zcrl, err := x509.ParseRevocationList(crl.Raw)
104 if err != nil {
105 errCount += 1
106 logger.Errf("parsing CRL %q failed: %s", u, err)
107 continue
108 }
109
110 err = checker.Validate(zcrl, issuer, ageLimit)
111 if err != nil {
112 errCount += 1
113 logger.Errf("checking CRL %q failed: %s", u, err)
114 continue
115 }
116
117 if oldestTimestamp.IsZero() || crl.ThisUpdate.Before(oldestTimestamp) {
118 oldestTimestamp = crl.ThisUpdate
119 }
120
121 for _, c := range crl.RevokedCertificateEntries {
122 serial := core.SerialToString(c.SerialNumber)
123 if _, seen := seenSerials[serial]; seen {
124 errCount += 1
125 logger.Errf("serial seen in multiple shards: %s", serial)
126 continue
127 }
128 seenSerials[serial] = struct{}{}
129 }
130 }
131
132 if *emitRevoked {
133 for serial := range seenSerials {
134 fmt.Println(serial)
135 }
136 }
137
138 if errCount != 0 {
139 cmd.Fail(fmt.Sprintf("Encountered %d errors", errCount))
140 }
141
142 logger.AuditInfof(
143 "Validated %d CRLs, %d serials, %d bytes. Oldest CRL: %s",
144 len(urls), len(seenSerials), totalBytes, oldestTimestamp.Format(time.RFC3339))
145 }
146
147 func init() {
148 cmd.RegisterCommand("crl-checker", main, nil)
149 }
150
View as plain text