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 }