...

Source file src/github.com/letsencrypt/boulder/cmd/crl-checker/main.go

Documentation: github.com/letsencrypt/boulder/cmd/crl-checker

     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