github.com/letsencrypt/boulder@v0.20251208.0/cmd/crl-checker/main.go (about) 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 }