github.com/letsencrypt/boulder@v0.20251208.0/issuance/crl.go (about) 1 package issuance 2 3 import ( 4 "crypto/rand" 5 "crypto/x509" 6 "fmt" 7 "math/big" 8 "time" 9 10 "github.com/zmap/zlint/v3/lint" 11 12 "github.com/letsencrypt/boulder/config" 13 "github.com/letsencrypt/boulder/crl/idp" 14 "github.com/letsencrypt/boulder/linter" 15 ) 16 17 type CRLProfileConfig struct { 18 ValidityInterval config.Duration 19 MaxBackdate config.Duration 20 21 // LintConfig is a path to a zlint config file, which can be used to control 22 // the behavior of zlint's "customizable lints". 23 LintConfig string 24 // IgnoredLints is a list of lint names that we know will fail for this 25 // profile, and which we know it is safe to ignore. 26 IgnoredLints []string 27 } 28 29 type CRLProfile struct { 30 validityInterval time.Duration 31 maxBackdate time.Duration 32 33 lints lint.Registry 34 } 35 36 func NewCRLProfile(config CRLProfileConfig) (*CRLProfile, error) { 37 lifetime := config.ValidityInterval.Duration 38 if lifetime >= 10*24*time.Hour { 39 return nil, fmt.Errorf("crl lifetime cannot be more than 10 days, got %q", lifetime) 40 } else if lifetime <= 0*time.Hour { 41 return nil, fmt.Errorf("crl lifetime must be positive, got %q", lifetime) 42 } 43 44 if config.MaxBackdate.Duration < 0 { 45 return nil, fmt.Errorf("crl max backdate must be non-negative, got %q", config.MaxBackdate) 46 } 47 48 reg, err := linter.NewRegistry(config.IgnoredLints) 49 if err != nil { 50 return nil, fmt.Errorf("creating lint registry: %w", err) 51 } 52 if config.LintConfig != "" { 53 lintconfig, err := lint.NewConfigFromFile(config.LintConfig) 54 if err != nil { 55 return nil, fmt.Errorf("loading zlint config file: %w", err) 56 } 57 reg.SetConfiguration(lintconfig) 58 } 59 60 return &CRLProfile{ 61 validityInterval: config.ValidityInterval.Duration, 62 maxBackdate: config.MaxBackdate.Duration, 63 lints: reg, 64 }, nil 65 } 66 67 type CRLRequest struct { 68 Number *big.Int 69 Shard int64 70 71 ThisUpdate time.Time 72 73 Entries []x509.RevocationListEntry 74 } 75 76 // crlURL combines the CRL URL base with a shard, and adds a suffix. 77 func (i *Issuer) crlURL(shard int) string { 78 return fmt.Sprintf("%s%d.crl", i.crlURLBase, shard) 79 } 80 81 func (i *Issuer) IssueCRL(prof *CRLProfile, req *CRLRequest) ([]byte, error) { 82 backdatedBy := i.clk.Now().Sub(req.ThisUpdate) 83 if backdatedBy > prof.maxBackdate { 84 return nil, fmt.Errorf("ThisUpdate is too far in the past (%s>%s)", backdatedBy, prof.maxBackdate) 85 } 86 if backdatedBy < 0 { 87 return nil, fmt.Errorf("ThisUpdate is in the future (%s>%s)", req.ThisUpdate, i.clk.Now()) 88 } 89 90 template := &x509.RevocationList{ 91 RevokedCertificateEntries: req.Entries, 92 Number: req.Number, 93 ThisUpdate: req.ThisUpdate, 94 NextUpdate: req.ThisUpdate.Add(-time.Second).Add(prof.validityInterval), 95 } 96 97 if i.crlURLBase == "" { 98 return nil, fmt.Errorf("CRL must contain an issuingDistributionPoint") 99 } 100 101 // Concat the base with the shard directly, since we require that the base 102 // end with a single trailing slash. 103 idp, err := idp.MakeUserCertsExt([]string{ 104 i.crlURL(int(req.Shard)), 105 }) 106 if err != nil { 107 return nil, fmt.Errorf("creating IDP extension: %w", err) 108 } 109 template.ExtraExtensions = append(template.ExtraExtensions, idp) 110 111 err = i.Linter.CheckCRL(template, prof.lints) 112 if err != nil { 113 return nil, err 114 } 115 116 crlBytes, err := x509.CreateRevocationList( 117 rand.Reader, 118 template, 119 i.Cert.Certificate, 120 i.Signer, 121 ) 122 if err != nil { 123 return nil, err 124 } 125 126 return crlBytes, nil 127 }