150 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			150 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			Go
		
	
	
	
package notmain
 | 
						|
 | 
						|
import (
 | 
						|
	"crypto/x509"
 | 
						|
	"encoding/json"
 | 
						|
	"flag"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
	"os"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/letsencrypt/boulder/cmd"
 | 
						|
	"github.com/letsencrypt/boulder/core"
 | 
						|
	"github.com/letsencrypt/boulder/crl/checker"
 | 
						|
)
 | 
						|
 | 
						|
func downloadShard(url string) (*x509.RevocationList, error) {
 | 
						|
	resp, err := http.Get(url)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("downloading crl: %w", err)
 | 
						|
	}
 | 
						|
	if resp.StatusCode != http.StatusOK {
 | 
						|
		return nil, fmt.Errorf("downloading crl: http status %d", resp.StatusCode)
 | 
						|
	}
 | 
						|
 | 
						|
	crlBytes, err := io.ReadAll(resp.Body)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("reading CRL bytes: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	crl, err := x509.ParseRevocationList(crlBytes)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("parsing CRL: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	return crl, 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, required, '-' to disable validation")
 | 
						|
	ageLimitStr := flag.String("ageLimit", "168h", "maximum allowable age of a CRL shard")
 | 
						|
	emitRevoked := flag.Bool("emitRevoked", false, "emit revoked serial numbers on stdout, one per line, hex-encoded")
 | 
						|
	save := flag.Bool("save", false, "save CRLs to files named after the URL")
 | 
						|
	flag.Parse()
 | 
						|
 | 
						|
	logger := cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 6, SyslogLevel: -1})
 | 
						|
	logger.Info(cmd.VersionString())
 | 
						|
 | 
						|
	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")
 | 
						|
 | 
						|
	if *issuerFile == "" {
 | 
						|
		cmd.Fail("-issuer is required, but may be '-' to disable validation")
 | 
						|
	}
 | 
						|
 | 
						|
	var issuer *x509.Certificate
 | 
						|
	if *issuerFile != "-" {
 | 
						|
		issuer, err = core.LoadCert(*issuerFile)
 | 
						|
		cmd.FailOnError(err, "Loading issuer certificate")
 | 
						|
	} else {
 | 
						|
		logger.Warning("CRL signature validation disabled")
 | 
						|
	}
 | 
						|
 | 
						|
	ageLimit, err := time.ParseDuration(*ageLimitStr)
 | 
						|
	cmd.FailOnError(err, "Parsing age limit")
 | 
						|
 | 
						|
	errCount := 0
 | 
						|
	seenSerials := make(map[string]struct{})
 | 
						|
	totalBytes := 0
 | 
						|
	oldestTimestamp := time.Time{}
 | 
						|
	for _, u := range urls {
 | 
						|
		crl, err := downloadShard(u)
 | 
						|
		if err != nil {
 | 
						|
			errCount += 1
 | 
						|
			logger.Errf("fetching CRL %q failed: %s", u, err)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		if *save {
 | 
						|
			parsedURL, err := url.Parse(u)
 | 
						|
			if err != nil {
 | 
						|
				logger.Errf("parsing url: %s", err)
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			filename := fmt.Sprintf("%s%s", parsedURL.Host, strings.ReplaceAll(parsedURL.Path, "/", "_"))
 | 
						|
			err = os.WriteFile(filename, crl.Raw, 0660)
 | 
						|
			if err != nil {
 | 
						|
				logger.Errf("writing file: %s", err)
 | 
						|
				continue
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		totalBytes += len(crl.Raw)
 | 
						|
 | 
						|
		zcrl, err := x509.ParseRevocationList(crl.Raw)
 | 
						|
		if err != nil {
 | 
						|
			errCount += 1
 | 
						|
			logger.Errf("parsing CRL %q failed: %s", u, err)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		err = checker.Validate(zcrl, issuer, ageLimit)
 | 
						|
		if err != nil {
 | 
						|
			errCount += 1
 | 
						|
			logger.Errf("checking CRL %q failed: %s", u, err)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		if oldestTimestamp.IsZero() || crl.ThisUpdate.Before(oldestTimestamp) {
 | 
						|
			oldestTimestamp = crl.ThisUpdate
 | 
						|
		}
 | 
						|
 | 
						|
		for _, c := range crl.RevokedCertificateEntries {
 | 
						|
			serial := core.SerialToString(c.SerialNumber)
 | 
						|
			if _, seen := seenSerials[serial]; seen {
 | 
						|
				errCount += 1
 | 
						|
				logger.Errf("serial seen in multiple shards: %s", serial)
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			seenSerials[serial] = struct{}{}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if *emitRevoked {
 | 
						|
		for serial := range seenSerials {
 | 
						|
			fmt.Println(serial)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if errCount != 0 {
 | 
						|
		cmd.Fail(fmt.Sprintf("Encountered %d errors", errCount))
 | 
						|
	}
 | 
						|
 | 
						|
	logger.AuditInfof(
 | 
						|
		"Validated %d CRLs, %d serials, %d bytes. Oldest CRL: %s",
 | 
						|
		len(urls), len(seenSerials), totalBytes, oldestTimestamp.Format(time.RFC3339))
 | 
						|
}
 | 
						|
 | 
						|
func init() {
 | 
						|
	cmd.RegisterCommand("crl-checker", main, nil)
 | 
						|
}
 |