github.com/letsencrypt/boulder@v0.20251208.0/issuance/cert.go (about) 1 package issuance 2 3 import ( 4 "bytes" 5 "crypto" 6 "crypto/ecdsa" 7 "crypto/rand" 8 "crypto/rsa" 9 "crypto/x509" 10 "crypto/x509/pkix" 11 "encoding/asn1" 12 "encoding/json" 13 "errors" 14 "fmt" 15 "math/big" 16 "net" 17 "sync" 18 "time" 19 20 ct "github.com/google/certificate-transparency-go" 21 cttls "github.com/google/certificate-transparency-go/tls" 22 ctx509 "github.com/google/certificate-transparency-go/x509" 23 "github.com/jmhodges/clock" 24 "github.com/zmap/zlint/v3/lint" 25 26 "github.com/letsencrypt/boulder/cmd" 27 "github.com/letsencrypt/boulder/config" 28 "github.com/letsencrypt/boulder/linter" 29 "github.com/letsencrypt/boulder/precert" 30 ) 31 32 // ProfileConfig describes the certificate issuance constraints for all issuers. 33 type ProfileConfig struct { 34 // OmitCommonName causes the CN field to be excluded from the resulting 35 // certificate, regardless of its inclusion in the IssuanceRequest. 36 OmitCommonName bool 37 // OmitKeyEncipherment causes the keyEncipherment bit to be omitted from the 38 // Key Usage field of all certificates (instead of only from ECDSA certs). 39 OmitKeyEncipherment bool 40 // OmitClientAuth causes the id-kp-clientAuth OID (TLS Client Authentication) 41 // to be omitted from the EKU extension. 42 OmitClientAuth bool 43 // OmitSKID causes the Subject Key Identifier extension to be omitted. 44 OmitSKID bool 45 46 MaxValidityPeriod config.Duration 47 MaxValidityBackdate config.Duration 48 49 // LintConfig is a path to a zlint config file, which can be used to control 50 // the behavior of zlint's "customizable lints". 51 LintConfig string 52 // IgnoredLints is a list of lint names that we know will fail for this 53 // profile, and which we know it is safe to ignore. 54 IgnoredLints []string 55 } 56 57 // Profile is the validated structure created by reading in a ProfileConfig 58 type Profile struct { 59 omitCommonName bool 60 omitKeyEncipherment bool 61 omitClientAuth bool 62 omitSKID bool 63 64 maxBackdate time.Duration 65 maxValidity time.Duration 66 67 lints lint.Registry 68 } 69 70 // NewProfile converts the profile config into a usable profile. 71 func NewProfile(profileConfig ProfileConfig) (*Profile, error) { 72 // The Baseline Requirements, Section 7.1.2.7, says that the notBefore time 73 // must be "within 48 hours of the time of signing". We can be even stricter. 74 if profileConfig.MaxValidityBackdate.Duration >= 24*time.Hour { 75 return nil, fmt.Errorf("backdate %q is too large", profileConfig.MaxValidityBackdate.Duration) 76 } 77 78 // Our CP/CPS, Section 7.1, says that our Subscriber Certificates have a 79 // validity period of "up to 100 days". 80 if profileConfig.MaxValidityPeriod.Duration >= 100*24*time.Hour { 81 return nil, fmt.Errorf("validity period %q is too large", profileConfig.MaxValidityPeriod.Duration) 82 } 83 84 lints, err := linter.NewRegistry(profileConfig.IgnoredLints) 85 cmd.FailOnError(err, "Failed to create zlint registry") 86 if profileConfig.LintConfig != "" { 87 lintconfig, err := lint.NewConfigFromFile(profileConfig.LintConfig) 88 cmd.FailOnError(err, "Failed to load zlint config file") 89 lints.SetConfiguration(lintconfig) 90 } 91 92 sp := &Profile{ 93 omitCommonName: profileConfig.OmitCommonName, 94 omitKeyEncipherment: profileConfig.OmitKeyEncipherment, 95 omitClientAuth: profileConfig.OmitClientAuth, 96 omitSKID: profileConfig.OmitSKID, 97 maxBackdate: profileConfig.MaxValidityBackdate.Duration, 98 maxValidity: profileConfig.MaxValidityPeriod.Duration, 99 lints: lints, 100 } 101 102 return sp, nil 103 } 104 105 // GenerateValidity returns a notBefore/notAfter pair bracketing the input time, 106 // based on the profile's configured backdate and validity. 107 func (p *Profile) GenerateValidity(now time.Time) (time.Time, time.Time) { 108 // Don't use the full maxBackdate, to ensure that the actual backdate remains 109 // acceptable throughout the rest of the issuance process. 110 backdate := time.Duration(float64(p.maxBackdate.Nanoseconds()) * 0.9) 111 notBefore := now.Add(-1 * backdate).Truncate(time.Second) 112 // Subtract one second, because certificate validity periods are *inclusive* 113 // of their final second (Baseline Requirements, Section 1.6.1). 114 notAfter := notBefore.Add(p.maxValidity).Add(-1 * time.Second).Truncate(time.Second) 115 return notBefore, notAfter 116 } 117 118 // requestValid verifies the passed IssuanceRequest against the profile. If the 119 // request doesn't match the signing profile an error is returned. 120 func (i *Issuer) requestValid(clk clock.Clock, prof *Profile, req *IssuanceRequest) error { 121 switch req.PublicKey.PublicKey.(type) { 122 case *rsa.PublicKey, *ecdsa.PublicKey: 123 default: 124 return errors.New("unsupported public key type") 125 } 126 127 if len(req.precertDER) == 0 && !i.active { 128 return errors.New("inactive issuer cannot issue precert") 129 } 130 131 if len(req.SubjectKeyId) != 0 && len(req.SubjectKeyId) != 20 { 132 return errors.New("unexpected subject key ID length") 133 } 134 135 if req.IncludeCTPoison && req.sctList != nil { 136 return errors.New("cannot include both ct poison and sct list extensions") 137 } 138 139 // The validity period is calculated inclusive of the whole second represented 140 // by the notAfter timestamp. 141 validity := req.NotAfter.Add(time.Second).Sub(req.NotBefore) 142 if validity <= 0 { 143 return errors.New("NotAfter must be after NotBefore") 144 } 145 if validity > prof.maxValidity { 146 return fmt.Errorf("validity period is more than the maximum allowed period (%s>%s)", validity, prof.maxValidity) 147 } 148 backdatedBy := clk.Now().Sub(req.NotBefore) 149 if backdatedBy > prof.maxBackdate { 150 return fmt.Errorf("NotBefore is backdated more than the maximum allowed period (%s>%s)", backdatedBy, prof.maxBackdate) 151 } 152 if backdatedBy < 0 { 153 return errors.New("NotBefore is in the future") 154 } 155 156 // We use 19 here because a 20-byte serial could produce >20 octets when 157 // encoded in ASN.1. That happens when the first byte is >0x80. See 158 // https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/#integer-encoding 159 if len(req.Serial) > 19 || len(req.Serial) < 9 { 160 return errors.New("serial must be between 9 and 19 bytes") 161 } 162 163 return nil 164 } 165 166 // Baseline Requirements, Section 7.1.6.1: domain-validated 167 var domainValidatedOID = func() x509.OID { 168 x509OID, err := x509.OIDFromInts([]uint64{2, 23, 140, 1, 2, 1}) 169 if err != nil { 170 // This should never happen, as the OID is hardcoded. 171 panic(fmt.Errorf("failed to create OID using ints %v: %s", x509OID, err)) 172 } 173 return x509OID 174 }() 175 176 func (i *Issuer) generateTemplate() *x509.Certificate { 177 template := &x509.Certificate{ 178 SignatureAlgorithm: i.sigAlg, 179 IssuingCertificateURL: []string{i.issuerURL}, 180 BasicConstraintsValid: true, 181 // Baseline Requirements, Section 7.1.6.1: domain-validated 182 Policies: []x509.OID{domainValidatedOID}, 183 } 184 185 return template 186 } 187 188 var ctPoisonExt = pkix.Extension{ 189 // OID for CT poison, RFC 6962 (was never assigned a proper id-pe- name) 190 Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}, 191 Value: asn1.NullBytes, 192 Critical: true, 193 } 194 195 // OID for SCT list, RFC 6962 (was never assigned a proper id-pe- name) 196 var sctListOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2} 197 198 func generateSCTListExt(scts []ct.SignedCertificateTimestamp) (pkix.Extension, error) { 199 list := ctx509.SignedCertificateTimestampList{} 200 for _, sct := range scts { 201 sctBytes, err := cttls.Marshal(sct) 202 if err != nil { 203 return pkix.Extension{}, err 204 } 205 list.SCTList = append(list.SCTList, ctx509.SerializedSCT{Val: sctBytes}) 206 } 207 listBytes, err := cttls.Marshal(list) 208 if err != nil { 209 return pkix.Extension{}, err 210 } 211 extBytes, err := asn1.Marshal(listBytes) 212 if err != nil { 213 return pkix.Extension{}, err 214 } 215 return pkix.Extension{ 216 Id: sctListOID, 217 Value: extBytes, 218 }, nil 219 } 220 221 // MarshalablePublicKey is a wrapper for crypto.PublicKey with a custom JSON 222 // marshaller that encodes the public key as a DER-encoded SubjectPublicKeyInfo. 223 type MarshalablePublicKey struct { 224 crypto.PublicKey 225 } 226 227 func (pk MarshalablePublicKey) MarshalJSON() ([]byte, error) { 228 keyDER, err := x509.MarshalPKIXPublicKey(pk.PublicKey) 229 if err != nil { 230 return nil, err 231 } 232 return json.Marshal(keyDER) 233 } 234 235 type HexMarshalableBytes []byte 236 237 func (h HexMarshalableBytes) MarshalJSON() ([]byte, error) { 238 return json.Marshal(fmt.Sprintf("%x", h)) 239 } 240 241 // IssuanceRequest describes a certificate issuance request 242 // 243 // It can be marshaled as JSON for logging purposes, though note that sctList and precertDER 244 // will be omitted from the marshaled output because they are unexported. 245 type IssuanceRequest struct { 246 // PublicKey is of type MarshalablePublicKey so we can log an IssuanceRequest as a JSON object. 247 PublicKey MarshalablePublicKey 248 SubjectKeyId HexMarshalableBytes 249 250 Serial HexMarshalableBytes 251 252 NotBefore time.Time 253 NotAfter time.Time 254 255 CommonName string 256 DNSNames []string 257 IPAddresses []net.IP 258 259 IncludeCTPoison bool 260 261 // sctList is a list of SCTs to include in a final certificate. 262 // If it is non-empty, PrecertDER must also be non-empty. 263 sctList []ct.SignedCertificateTimestamp 264 // precertDER is the encoded bytes of the precertificate that a 265 // final certificate is expected to correspond to. If it is non-empty, 266 // SCTList must also be non-empty. 267 precertDER []byte 268 } 269 270 // An issuanceToken represents an assertion that Issuer.Lint has generated 271 // a linting certificate for a given input and run the linter over it with no 272 // errors. The token may be redeemed (at most once) to sign a certificate or 273 // precertificate with the same Issuer's private key, containing the same 274 // contents that were linted. 275 type issuanceToken struct { 276 mu sync.Mutex 277 template *x509.Certificate 278 pubKey MarshalablePublicKey 279 // A pointer to the issuer that created this token. This token may only 280 // be redeemed by the same issuer. 281 issuer *Issuer 282 } 283 284 // Prepare combines the given profile and request with the Issuer's information 285 // to create a template certificate. It then generates a linting certificate 286 // from that template and runs the linter over it. If successful, returns both 287 // the linting certificate (which can be stored) and an issuanceToken. The 288 // issuanceToken can be used to sign a matching certificate with this Issuer's 289 // private key. 290 func (i *Issuer) Prepare(prof *Profile, req *IssuanceRequest) ([]byte, *issuanceToken, error) { 291 // check request is valid according to the issuance profile 292 err := i.requestValid(i.clk, prof, req) 293 if err != nil { 294 return nil, nil, err 295 } 296 297 // generate template from the issuer's data 298 template := i.generateTemplate() 299 300 ekus := []x509.ExtKeyUsage{ 301 x509.ExtKeyUsageServerAuth, 302 x509.ExtKeyUsageClientAuth, 303 } 304 if prof.omitClientAuth { 305 ekus = []x509.ExtKeyUsage{ 306 x509.ExtKeyUsageServerAuth, 307 } 308 } 309 template.ExtKeyUsage = ekus 310 311 // populate template from the issuance request 312 template.NotBefore, template.NotAfter = req.NotBefore, req.NotAfter 313 template.SerialNumber = big.NewInt(0).SetBytes(req.Serial) 314 if req.CommonName != "" && !prof.omitCommonName { 315 template.Subject.CommonName = req.CommonName 316 } 317 template.DNSNames = req.DNSNames 318 template.IPAddresses = req.IPAddresses 319 320 switch req.PublicKey.PublicKey.(type) { 321 case *rsa.PublicKey: 322 if prof.omitKeyEncipherment { 323 template.KeyUsage = x509.KeyUsageDigitalSignature 324 } else { 325 template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment 326 } 327 case *ecdsa.PublicKey: 328 template.KeyUsage = x509.KeyUsageDigitalSignature 329 } 330 331 if !prof.omitSKID { 332 template.SubjectKeyId = req.SubjectKeyId 333 } 334 335 if req.IncludeCTPoison { 336 template.ExtraExtensions = append(template.ExtraExtensions, ctPoisonExt) 337 } else if len(req.sctList) > 0 { 338 if len(req.precertDER) == 0 { 339 return nil, nil, errors.New("inconsistent request contains sctList but no precertDER") 340 } 341 sctListExt, err := generateSCTListExt(req.sctList) 342 if err != nil { 343 return nil, nil, err 344 } 345 template.ExtraExtensions = append(template.ExtraExtensions, sctListExt) 346 } else { 347 return nil, nil, errors.New("invalid request contains neither sctList nor precertDER") 348 } 349 350 // Pick a CRL shard based on the serial number modulo the number of shards. 351 // This gives us random distribution that is nonetheless consistent between 352 // precert and cert. 353 shardZeroBased := big.NewInt(0).Mod(template.SerialNumber, big.NewInt(int64(i.crlShards))) 354 shard := int(shardZeroBased.Int64()) + 1 355 template.CRLDistributionPoints = []string{i.crlURL(shard)} 356 357 // check that the tbsCertificate is properly formed by signing it 358 // with a throwaway key and then linting it using zlint 359 lintCertBytes, err := i.Linter.Check(template, req.PublicKey.PublicKey, prof.lints) 360 if err != nil { 361 return nil, nil, fmt.Errorf("tbsCertificate linting failed: %w", err) 362 } 363 364 if len(req.precertDER) > 0 { 365 err = precert.Correspond(req.precertDER, lintCertBytes) 366 if err != nil { 367 return nil, nil, fmt.Errorf("precert does not correspond to linted final cert: %w", err) 368 } 369 } 370 371 token := &issuanceToken{sync.Mutex{}, template, req.PublicKey, i} 372 return lintCertBytes, token, nil 373 } 374 375 // Issue performs a real issuance using an issuanceToken resulting from a 376 // previous call to Prepare(). Call this at most once per token. Calls after 377 // the first will receive an error. 378 func (i *Issuer) Issue(token *issuanceToken) ([]byte, error) { 379 if token == nil { 380 return nil, errors.New("nil issuanceToken") 381 } 382 token.mu.Lock() 383 defer token.mu.Unlock() 384 if token.template == nil { 385 return nil, errors.New("issuance token already redeemed") 386 } 387 template := token.template 388 token.template = nil 389 390 if token.issuer != i { 391 return nil, errors.New("tried to redeem issuance token with the wrong issuer") 392 } 393 394 return x509.CreateCertificate(rand.Reader, template, i.Cert.Certificate, token.pubKey.PublicKey, i.Signer) 395 } 396 397 // containsCTPoison returns true if the provided set of extensions includes 398 // an entry whose OID and value both match the expected values for the CT 399 // Poison extension. 400 func containsCTPoison(extensions []pkix.Extension) bool { 401 for _, ext := range extensions { 402 if ext.Id.Equal(ctPoisonExt.Id) && bytes.Equal(ext.Value, asn1.NullBytes) { 403 return true 404 } 405 } 406 return false 407 } 408 409 // RequestFromPrecert constructs a final certificate IssuanceRequest matching 410 // the provided precertificate. It returns an error if the precertificate doesn't 411 // contain the CT poison extension. 412 func RequestFromPrecert(precert *x509.Certificate, scts []ct.SignedCertificateTimestamp) (*IssuanceRequest, error) { 413 if !containsCTPoison(precert.Extensions) { 414 return nil, errors.New("provided certificate doesn't contain the CT poison extension") 415 } 416 return &IssuanceRequest{ 417 PublicKey: MarshalablePublicKey{precert.PublicKey}, 418 SubjectKeyId: precert.SubjectKeyId, 419 Serial: precert.SerialNumber.Bytes(), 420 NotBefore: precert.NotBefore, 421 NotAfter: precert.NotAfter, 422 CommonName: precert.Subject.CommonName, 423 DNSNames: precert.DNSNames, 424 IPAddresses: precert.IPAddresses, 425 sctList: scts, 426 precertDER: precert.Raw, 427 }, nil 428 }