diff --git a/cmd/boulder/main.go b/cmd/boulder/main.go index b26f09099..5fd4618ad 100644 --- a/cmd/boulder/main.go +++ b/cmd/boulder/main.go @@ -19,6 +19,7 @@ import ( _ "github.com/letsencrypt/boulder/cmd/ceremony" _ "github.com/letsencrypt/boulder/cmd/cert-checker" _ "github.com/letsencrypt/boulder/cmd/contact-auditor" + _ "github.com/letsencrypt/boulder/cmd/crl-checker" _ "github.com/letsencrypt/boulder/cmd/crl-storer" _ "github.com/letsencrypt/boulder/cmd/crl-updater" _ "github.com/letsencrypt/boulder/cmd/expiration-mailer" diff --git a/cmd/crl-checker/main.go b/cmd/crl-checker/main.go new file mode 100644 index 000000000..d6338a98e --- /dev/null +++ b/cmd/crl-checker/main.go @@ -0,0 +1,81 @@ +package notmain + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "os" + + "github.com/letsencrypt/boulder/cmd" + "github.com/letsencrypt/boulder/crl/crl_x509" + "github.com/letsencrypt/boulder/issuance" + "github.com/letsencrypt/boulder/linter" + crlint "github.com/letsencrypt/boulder/linter/lints/crl" +) + +func validateShard(url string, issuer *issuance.Certificate) error { + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("downloading crl: %w", err) + } + + crlBytes, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("reading CRL bytes: %w", err) + } + + crl, err := crl_x509.ParseRevocationList(crlBytes) + if err != nil { + return fmt.Errorf("parsing CRL: %w", err) + } + + err = linter.ProcessResultSet(crlint.LintCRL(crl)) + if err != nil { + return fmt.Errorf("linting CRL: %w", err) + } + + err = crl.CheckSignatureFrom(issuer.Certificate) + if err != nil { + return fmt.Errorf("checking CRL signature: %w", err) + } + + return nil +} + +func main() { + urlFile := flag.String("crls", "", "path to a file containing a JSON Array of CRL URLs") + issuerFile := flag.String("issuer", "", "path to an issuer certificate on disk") + flag.Parse() + + logger := cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 6, SyslogLevel: -1}) + + urlFileContents, err := os.ReadFile(*urlFile) + cmd.FailOnError(err, "Reading CRL URLs file") + + var urls []string + err = json.Unmarshal(urlFileContents, &urls) + cmd.FailOnError(err, "Parsing JSON Array of CRL URLs") + + issuer, err := issuance.LoadCertificate(*issuerFile) + cmd.FailOnError(err, "Loading issuer certificate") + + errCount := 0 + for _, url := range urls { + err = validateShard(url, issuer) + if err != nil { + errCount += 1 + logger.Errf("CRL %q failed: %s\n", url, err) + } + } + + if errCount != 0 { + cmd.Fail(fmt.Sprintf("Encountered %d errors", errCount)) + } + logger.AuditInfo("All CRLs validated") +} + +func init() { + cmd.RegisterCommand("crl-checker", main) +} diff --git a/linter/linter.go b/linter/linter.go index c6c1fda38..eec2b06f8 100644 --- a/linter/linter.go +++ b/linter/linter.go @@ -76,7 +76,7 @@ func (l Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey) err return err } lintRes := zlint.LintCertificateEx(cert, l.registry) - return processResultSet(lintRes) + return ProcessResultSet(lintRes) } // CheckCRL signs the given RevocationList template using the Linter's fake @@ -88,7 +88,7 @@ func (l Linter) CheckCRL(tbs *crl_x509.RevocationList) error { return err } lintRes := crllints.LintCRL(crl) - return processResultSet(lintRes) + return ProcessResultSet(lintRes) } func makeSigner(realSigner crypto.Signer) (crypto.Signer, error) { @@ -190,7 +190,7 @@ func makeLintCert(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, issuer return lintCert, nil } -func processResultSet(lintRes *zlint.ResultSet) error { +func ProcessResultSet(lintRes *zlint.ResultSet) error { if lintRes.NoticesPresent || lintRes.WarningsPresent || lintRes.ErrorsPresent || lintRes.FatalsPresent { var failedLints []string for lintName, result := range lintRes.Results {