github.com/letsencrypt/boulder@v0.20251208.0/tools/crldps/main.go (about)

     1  // crldps generates the list of CRL Distribution Point URIs for a given issuing
     2  // CA and number of shards. The CRLDPs look like "http://x.c.lencr.org/n.crl",
     3  // where "x" is the lowercased Subject Common Name of the issuing CA (e.g. "r3")
     4  // and "n" is the one-indexed number of the shard. It fetches and validates all
     5  // of those shards. If it doesn't encounter any errors, it pretty-prints the
     6  // list of all CRLDPs for disclosure in CCADB.
     7  package main
     8  
     9  import (
    10  	"crypto/x509"
    11  	"encoding/json"
    12  	"flag"
    13  	"fmt"
    14  	"io"
    15  	"log"
    16  	"net/http"
    17  	"net/url"
    18  	"os"
    19  	"regexp"
    20  	"slices"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/letsencrypt/boulder/core"
    25  	"github.com/letsencrypt/boulder/crl/idp"
    26  )
    27  
    28  // Matches the ABNF definition of a <label> per RFC 1035's Preferred Name Syntax
    29  // https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.1
    30  var rfc1035label = regexp.MustCompile("^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$")
    31  
    32  func main() {
    33  	fs := flag.NewFlagSet("crldps", flag.ContinueOnError)
    34  	caPath := fs.String("ca", "", "path to an issuing intermediate CA certificate (required)")
    35  	numShards := fs.Int("shards", 0, "number of CRL shards issued by the CA (required)")
    36  	err := fs.Parse(os.Args[1:])
    37  	if err != nil || len(fs.Args()) != 0 || len(*caPath) == 0 || *numShards == 0 {
    38  		log.Println("Incorrect command line flags; usage:")
    39  		fs.PrintDefaults()
    40  		os.Exit(1)
    41  	}
    42  
    43  	issuer, err := core.LoadCert(*caPath)
    44  	if err != nil {
    45  		log.Fatalf("Failed to load issuer certificate from %q: %s", os.Args[1], err)
    46  	}
    47  
    48  	if len(issuer.Subject.CommonName) > 63 || !rfc1035label.MatchString(issuer.Subject.CommonName) {
    49  		log.Fatalf("Cannot construct CRLDP because issuer CN %q is not a valid domain label", issuer.Subject.CommonName)
    50  	}
    51  
    52  	client := http.Client{Timeout: 10 * time.Second}
    53  
    54  	var (
    55  		anyErr bool
    56  		crldps []string
    57  	)
    58  	for shard := range *numShards {
    59  		// We 1-index our CRL shards.
    60  		crldp := crldpString(issuer, shard+1)
    61  
    62  		err := fetchAndCheck(crldp, client, issuer)
    63  		if err != nil {
    64  			anyErr = true
    65  			log.Printf("Error processing crl %q: %x", crldp, err)
    66  			continue
    67  		}
    68  
    69  		crldps = append(crldps, crldp)
    70  	}
    71  
    72  	if anyErr {
    73  		log.Fatalf("Encountered one or more errors above; exiting")
    74  	}
    75  
    76  	// Do two final checks of the zeroth and one-past-the-end shards, to ensure
    77  	// the operator gave the correct number of shards and that the shard
    78  	// generation is configured correctly.
    79  	{
    80  		crldp := crldpString(issuer, 0)
    81  		resp, err := client.Get(crldp)
    82  		if err != nil {
    83  			log.Fatalf("Error checking for existence of zero shard %q: %s", crldp, err)
    84  		} else if resp.StatusCode != http.StatusNotFound {
    85  			log.Fatalf("Was unexpectedly able to fetch zero shard %q; please verify that the generated shards are one-indexed", crldp)
    86  		}
    87  	}
    88  	{
    89  		crldp := crldpString(issuer, *numShards+1)
    90  		resp, err := client.Get(crldp)
    91  		if err != nil {
    92  			log.Fatalf("Error checking for existence of higher-nunbered shard %q: %s", crldp, err)
    93  		} else if resp.StatusCode != http.StatusNotFound {
    94  			log.Fatalf("Was unexpectedly able to fetch higher-numbered shard %q; please verify that the -shards flag is correct", crldp)
    95  		}
    96  	}
    97  
    98  	out, err := json.MarshalIndent(crldps, "", "  ")
    99  	if err != nil {
   100  		log.Fatalf("Failed to marshal list of CRLDPs: %s", err)
   101  	}
   102  
   103  	fmt.Println(string(out))
   104  }
   105  
   106  func crldpString(issuer *x509.Certificate, shard int) string {
   107  	return (&url.URL{
   108  		Scheme: "http",
   109  		Host:   fmt.Sprintf("%s.c.lencr.org", strings.ToLower(issuer.Subject.CommonName)),
   110  		Path:   fmt.Sprintf("%d.crl", shard),
   111  	}).String()
   112  }
   113  
   114  func fetchAndCheck(crldp string, client http.Client, issuer *x509.Certificate) error {
   115  	resp, err := client.Get(crldp)
   116  	if err != nil {
   117  		return fmt.Errorf("error downloading crl: %s", err)
   118  	} else if resp.StatusCode != http.StatusOK {
   119  		return fmt.Errorf("unexpected status code while downloading crl: %s", http.StatusText(resp.StatusCode))
   120  	}
   121  	defer resp.Body.Close()
   122  
   123  	crlDer, err := io.ReadAll(resp.Body)
   124  	if err != nil {
   125  		return fmt.Errorf("error reading crl: %s", err)
   126  	}
   127  
   128  	crl, err := x509.ParseRevocationList(crlDer)
   129  	if err != nil {
   130  		return fmt.Errorf("error parsing crl: %s", err)
   131  	}
   132  
   133  	err = crl.CheckSignatureFrom(issuer)
   134  	if err != nil {
   135  		return fmt.Errorf("error validating crl signature: %s", err)
   136  	}
   137  
   138  	idps, err := idp.GetIDPURIs(crl.Extensions)
   139  	if err != nil {
   140  		return fmt.Errorf("error extracting IDPs: %s", err)
   141  	}
   142  
   143  	if !slices.Contains(idps, crldp) {
   144  		return fmt.Errorf("crl does not contain matching IDP")
   145  	}
   146  
   147  	return nil
   148  }