boulder/issuance/cert.go

512 lines
18 KiB
Go

package issuance
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/json"
"errors"
"fmt"
"math/big"
"sync"
"time"
ct "github.com/google/certificate-transparency-go"
cttls "github.com/google/certificate-transparency-go/tls"
ctx509 "github.com/google/certificate-transparency-go/x509"
"github.com/jmhodges/clock"
"github.com/zmap/zlint/v3/lint"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/config"
"github.com/letsencrypt/boulder/linter"
"github.com/letsencrypt/boulder/precert"
)
// ProfileConfig describes the certificate issuance constraints for all issuers.
//
// This struct gets hashed in the CA to allow matching up precert and final cert
// issuance by the exact profile config. We compute the hash over an ASN.1 encoding
// because ASN.1 encoding has a canonical form and can omit optional fields (which
// allows for gracefully adding new fields without changing the hash of existing
// profile configs). This struct does not get embedded into any certs, CRLs, or
// other objects, and does not get signed; it's only used internally.
//
// Note: even though these fields have encoding instructions (tag:N), they will
// be encoded in the order they appear in the struct, so do not reorder them.
type ProfileConfig struct {
// AllowMustStaple, when false, causes all IssuanceRequests which specify the
// OCSP Must Staple extension to be rejected.
AllowMustStaple bool `asn1:"tag:1,optional"`
// OmitCommonName causes the CN field to be excluded from the resulting
// certificate, regardless of its inclusion in the IssuanceRequest.
OmitCommonName bool `asn1:"tag:2,optional"`
// OmitKeyEncipherment causes the keyEncipherment bit to be omitted from the
// Key Usage field of all certificates (instead of only from ECDSA certs).
OmitKeyEncipherment bool `asn1:"tag:3,optional"`
// OmitClientAuth causes the id-kp-clientAuth OID (TLS Client Authentication)
// to be omitted from the EKU extension.
OmitClientAuth bool `asn1:"tag:4,optional"`
// OmitSKID causes the Subject Key Identifier extension to be omitted.
OmitSKID bool `asn1:"tag:5,optional"`
// IncludeCRLDistributionPoints causes the CRLDistributionPoints extension to
// be added to all certificates issued by this profile.
IncludeCRLDistributionPoints bool `asn1:"tag:6,optional"`
MaxValidityPeriod config.Duration `asn1:"tag:7,optional"`
MaxValidityBackdate config.Duration `asn1:"tag:8,optional"`
// LintConfig is a path to a zlint config file, which can be used to control
// the behavior of zlint's "customizable lints".
LintConfig string `asn1:"tag:9,optional"`
// IgnoredLints is a list of lint names that we know will fail for this
// profile, and which we know it is safe to ignore.
IgnoredLints []string `asn1:"tag:10,optional"`
}
func (pcn ProfileConfig) hash() ([32]byte, error) {
encodedBytes, err := asn1.Marshal(pcn)
if err != nil {
return [32]byte{}, err
}
return sha256.Sum256(encodedBytes), nil
}
// PolicyConfig describes a policy
type PolicyConfig struct {
OID string `validate:"required"`
}
// Profile is the validated structure created by reading in ProfileConfigs and IssuerConfigs
type Profile struct {
allowMustStaple bool
omitCommonName bool
omitKeyEncipherment bool
omitClientAuth bool
omitSKID bool
includeCRLDistributionPoints bool
maxBackdate time.Duration
maxValidity time.Duration
lints lint.Registry
hash [32]byte
}
// NewProfile converts the profile config into a usable profile.
func NewProfile(profileConfig *ProfileConfig) (*Profile, error) {
// The Baseline Requirements, Section 7.1.2.7, says that the notBefore time
// must be "within 48 hours of the time of signing". We can be even stricter.
if profileConfig.MaxValidityBackdate.Duration >= 24*time.Hour {
return nil, fmt.Errorf("backdate %q is too large", profileConfig.MaxValidityBackdate.Duration)
}
// Our CP/CPS, Section 7.1, says that our Subscriber Certificates have a
// validity period of "up to 100 days".
if profileConfig.MaxValidityPeriod.Duration >= 100*24*time.Hour {
return nil, fmt.Errorf("validity period %q is too large", profileConfig.MaxValidityPeriod.Duration)
}
lints, err := linter.NewRegistry(profileConfig.IgnoredLints)
cmd.FailOnError(err, "Failed to create zlint registry")
if profileConfig.LintConfig != "" {
lintconfig, err := lint.NewConfigFromFile(profileConfig.LintConfig)
cmd.FailOnError(err, "Failed to load zlint config file")
lints.SetConfiguration(lintconfig)
}
hash, err := profileConfig.hash()
if err != nil {
return nil, err
}
sp := &Profile{
allowMustStaple: profileConfig.AllowMustStaple,
omitCommonName: profileConfig.OmitCommonName,
omitKeyEncipherment: profileConfig.OmitKeyEncipherment,
omitClientAuth: profileConfig.OmitClientAuth,
omitSKID: profileConfig.OmitSKID,
includeCRLDistributionPoints: profileConfig.IncludeCRLDistributionPoints,
maxBackdate: profileConfig.MaxValidityBackdate.Duration,
maxValidity: profileConfig.MaxValidityPeriod.Duration,
lints: lints,
hash: hash,
}
return sp, nil
}
func (p *Profile) Hash() [32]byte {
return p.hash
}
// GenerateValidity returns a notBefore/notAfter pair bracketing the input time,
// based on the profile's configured backdate and validity.
func (p *Profile) GenerateValidity(now time.Time) (time.Time, time.Time) {
// Don't use the full maxBackdate, to ensure that the actual backdate remains
// acceptable throughout the rest of the issuance process.
backdate := time.Duration(float64(p.maxBackdate.Nanoseconds()) * 0.9)
notBefore := now.Add(-1 * backdate)
// Subtract one second, because certificate validity periods are *inclusive*
// of their final second (Baseline Requirements, Section 1.6.1).
notAfter := notBefore.Add(p.maxValidity).Add(-1 * time.Second)
return notBefore, notAfter
}
// requestValid verifies the passed IssuanceRequest against the profile. If the
// request doesn't match the signing profile an error is returned.
func (i *Issuer) requestValid(clk clock.Clock, prof *Profile, req *IssuanceRequest) error {
switch req.PublicKey.PublicKey.(type) {
case *rsa.PublicKey, *ecdsa.PublicKey:
default:
return errors.New("unsupported public key type")
}
if len(req.precertDER) == 0 && !i.active {
return errors.New("inactive issuer cannot issue precert")
}
if len(req.SubjectKeyId) != 0 && len(req.SubjectKeyId) != 20 {
return errors.New("unexpected subject key ID length")
}
if !prof.allowMustStaple && req.IncludeMustStaple {
return errors.New("must-staple extension cannot be included")
}
if req.IncludeCTPoison && req.sctList != nil {
return errors.New("cannot include both ct poison and sct list extensions")
}
// The validity period is calculated inclusive of the whole second represented
// by the notAfter timestamp.
validity := req.NotAfter.Add(time.Second).Sub(req.NotBefore)
if validity <= 0 {
return errors.New("NotAfter must be after NotBefore")
}
if validity > prof.maxValidity {
return fmt.Errorf("validity period is more than the maximum allowed period (%s>%s)", validity, prof.maxValidity)
}
backdatedBy := clk.Now().Sub(req.NotBefore)
if backdatedBy > prof.maxBackdate {
return fmt.Errorf("NotBefore is backdated more than the maximum allowed period (%s>%s)", backdatedBy, prof.maxBackdate)
}
if backdatedBy < 0 {
return errors.New("NotBefore is in the future")
}
// We use 19 here because a 20-byte serial could produce >20 octets when
// encoded in ASN.1. That happens when the first byte is >0x80. See
// https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/#integer-encoding
if len(req.Serial) > 19 || len(req.Serial) < 9 {
return errors.New("serial must be between 9 and 19 bytes")
}
return nil
}
// Baseline Requirements, Section 7.1.6.1: domain-validated
var domainValidatedOID = func() x509.OID {
x509OID, err := x509.OIDFromInts([]uint64{2, 23, 140, 1, 2, 1})
if err != nil {
// This should never happen, as the OID is hardcoded.
panic(fmt.Errorf("failed to create OID using ints %v: %s", x509OID, err))
}
return x509OID
}()
func (i *Issuer) generateTemplate() *x509.Certificate {
template := &x509.Certificate{
SignatureAlgorithm: i.sigAlg,
OCSPServer: []string{i.ocspURL},
IssuingCertificateURL: []string{i.issuerURL},
BasicConstraintsValid: true,
// Baseline Requirements, Section 7.1.6.1: domain-validated
Policies: []x509.OID{domainValidatedOID},
}
return template
}
var ctPoisonExt = pkix.Extension{
// OID for CT poison, RFC 6962 (was never assigned a proper id-pe- name)
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3},
Value: asn1.NullBytes,
Critical: true,
}
// OID for SCT list, RFC 6962 (was never assigned a proper id-pe- name)
var sctListOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}
func generateSCTListExt(scts []ct.SignedCertificateTimestamp) (pkix.Extension, error) {
list := ctx509.SignedCertificateTimestampList{}
for _, sct := range scts {
sctBytes, err := cttls.Marshal(sct)
if err != nil {
return pkix.Extension{}, err
}
list.SCTList = append(list.SCTList, ctx509.SerializedSCT{Val: sctBytes})
}
listBytes, err := cttls.Marshal(list)
if err != nil {
return pkix.Extension{}, err
}
extBytes, err := asn1.Marshal(listBytes)
if err != nil {
return pkix.Extension{}, err
}
return pkix.Extension{
Id: sctListOID,
Value: extBytes,
}, nil
}
var mustStapleExt = pkix.Extension{
// RFC 7633: id-pe-tlsfeature OBJECT IDENTIFIER ::= { id-pe 24 }
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24},
// ASN.1 encoding of:
// SEQUENCE
// INTEGER 5
// where "5" is the status_request feature (RFC 6066)
Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05},
}
// MarshalablePublicKey is a wrapper for crypto.PublicKey with a custom JSON
// marshaller that encodes the public key as a DER-encoded SubjectPublicKeyInfo.
type MarshalablePublicKey struct {
crypto.PublicKey
}
func (pk MarshalablePublicKey) MarshalJSON() ([]byte, error) {
keyDER, err := x509.MarshalPKIXPublicKey(pk.PublicKey)
if err != nil {
return nil, err
}
return json.Marshal(keyDER)
}
type HexMarshalableBytes []byte
func (h HexMarshalableBytes) MarshalJSON() ([]byte, error) {
return json.Marshal(fmt.Sprintf("%x", h))
}
// IssuanceRequest describes a certificate issuance request
//
// It can be marshaled as JSON for logging purposes, though note that sctList and precertDER
// will be omitted from the marshaled output because they are unexported.
type IssuanceRequest struct {
// PublicKey is of type MarshalablePublicKey so we can log an IssuanceRequest as a JSON object.
PublicKey MarshalablePublicKey
SubjectKeyId HexMarshalableBytes
Serial HexMarshalableBytes
NotBefore time.Time
NotAfter time.Time
CommonName string
DNSNames []string
IncludeMustStaple bool
IncludeCTPoison bool
// sctList is a list of SCTs to include in a final certificate.
// If it is non-empty, PrecertDER must also be non-empty.
sctList []ct.SignedCertificateTimestamp
// precertDER is the encoded bytes of the precertificate that a
// final certificate is expected to correspond to. If it is non-empty,
// SCTList must also be non-empty.
precertDER []byte
}
// An issuanceToken represents an assertion that Issuer.Lint has generated
// a linting certificate for a given input and run the linter over it with no
// errors. The token may be redeemed (at most once) to sign a certificate or
// precertificate with the same Issuer's private key, containing the same
// contents that were linted.
type issuanceToken struct {
mu sync.Mutex
template *x509.Certificate
pubKey MarshalablePublicKey
// A pointer to the issuer that created this token. This token may only
// be redeemed by the same issuer.
issuer *Issuer
}
// Prepare combines the given profile and request with the Issuer's information
// to create a template certificate. It then generates a linting certificate
// from that template and runs the linter over it. If successful, returns both
// the linting certificate (which can be stored) and an issuanceToken. The
// issuanceToken can be used to sign a matching certificate with this Issuer's
// private key.
func (i *Issuer) Prepare(prof *Profile, req *IssuanceRequest) ([]byte, *issuanceToken, error) {
// check request is valid according to the issuance profile
err := i.requestValid(i.clk, prof, req)
if err != nil {
return nil, nil, err
}
// generate template from the issuer's data
template := i.generateTemplate()
ekus := []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
}
if prof.omitClientAuth {
ekus = []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
}
}
template.ExtKeyUsage = ekus
// populate template from the issuance request
template.NotBefore, template.NotAfter = req.NotBefore, req.NotAfter
template.SerialNumber = big.NewInt(0).SetBytes(req.Serial)
if req.CommonName != "" && !prof.omitCommonName {
template.Subject.CommonName = req.CommonName
}
template.DNSNames = req.DNSNames
switch req.PublicKey.PublicKey.(type) {
case *rsa.PublicKey:
if prof.omitKeyEncipherment {
template.KeyUsage = x509.KeyUsageDigitalSignature
} else {
template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
}
case *ecdsa.PublicKey:
template.KeyUsage = x509.KeyUsageDigitalSignature
}
if !prof.omitSKID {
template.SubjectKeyId = req.SubjectKeyId
}
if req.IncludeCTPoison {
template.ExtraExtensions = append(template.ExtraExtensions, ctPoisonExt)
} else if len(req.sctList) > 0 {
if len(req.precertDER) == 0 {
return nil, nil, errors.New("inconsistent request contains sctList but no precertDER")
}
sctListExt, err := generateSCTListExt(req.sctList)
if err != nil {
return nil, nil, err
}
template.ExtraExtensions = append(template.ExtraExtensions, sctListExt)
} else {
return nil, nil, errors.New("invalid request contains neither sctList nor precertDER")
}
// If explicit CRL sharding is enabled, pick a shard based on the serial number
// modulus the number of shards. This gives us random distribution that is
// nonetheless consistent between precert and cert.
if prof.includeCRLDistributionPoints {
if i.crlShards <= 0 {
return nil, nil, errors.New("IncludeCRLDistributionPoints was set but CRLShards was not set")
}
shardZeroBased := big.NewInt(0).Mod(template.SerialNumber, big.NewInt(int64(i.crlShards)))
shard := int(shardZeroBased.Int64()) + 1
url := i.crlURL(shard)
template.CRLDistributionPoints = []string{url}
}
if req.IncludeMustStaple {
template.ExtraExtensions = append(template.ExtraExtensions, mustStapleExt)
}
// check that the tbsCertificate is properly formed by signing it
// with a throwaway key and then linting it using zlint
lintCertBytes, err := i.Linter.Check(template, req.PublicKey.PublicKey, prof.lints)
if err != nil {
return nil, nil, fmt.Errorf("tbsCertificate linting failed: %w", err)
}
if len(req.precertDER) > 0 {
err = precert.Correspond(req.precertDER, lintCertBytes)
if err != nil {
return nil, nil, fmt.Errorf("precert does not correspond to linted final cert: %w", err)
}
}
token := &issuanceToken{sync.Mutex{}, template, req.PublicKey, i}
return lintCertBytes, token, nil
}
// Issue performs a real issuance using an issuanceToken resulting from a
// previous call to Prepare(). Call this at most once per token. Calls after
// the first will receive an error.
func (i *Issuer) Issue(token *issuanceToken) ([]byte, error) {
if token == nil {
return nil, errors.New("nil issuanceToken")
}
token.mu.Lock()
defer token.mu.Unlock()
if token.template == nil {
return nil, errors.New("issuance token already redeemed")
}
template := token.template
token.template = nil
if token.issuer != i {
return nil, errors.New("tried to redeem issuance token with the wrong issuer")
}
return x509.CreateCertificate(rand.Reader, template, i.Cert.Certificate, token.pubKey.PublicKey, i.Signer)
}
// ContainsMustStaple returns true if the provided set of extensions includes
// an entry whose OID and value both match the expected values for the OCSP
// Must-Staple (a.k.a. id-pe-tlsFeature) extension.
func ContainsMustStaple(extensions []pkix.Extension) bool {
for _, ext := range extensions {
if ext.Id.Equal(mustStapleExt.Id) && bytes.Equal(ext.Value, mustStapleExt.Value) {
return true
}
}
return false
}
// containsCTPoison returns true if the provided set of extensions includes
// an entry whose OID and value both match the expected values for the CT
// Poison extension.
func containsCTPoison(extensions []pkix.Extension) bool {
for _, ext := range extensions {
if ext.Id.Equal(ctPoisonExt.Id) && bytes.Equal(ext.Value, asn1.NullBytes) {
return true
}
}
return false
}
// RequestFromPrecert constructs a final certificate IssuanceRequest matching
// the provided precertificate. It returns an error if the precertificate doesn't
// contain the CT poison extension.
func RequestFromPrecert(precert *x509.Certificate, scts []ct.SignedCertificateTimestamp) (*IssuanceRequest, error) {
if !containsCTPoison(precert.Extensions) {
return nil, errors.New("provided certificate doesn't contain the CT poison extension")
}
return &IssuanceRequest{
PublicKey: MarshalablePublicKey{precert.PublicKey},
SubjectKeyId: precert.SubjectKeyId,
Serial: precert.SerialNumber.Bytes(),
NotBefore: precert.NotBefore,
NotAfter: precert.NotAfter,
CommonName: precert.Subject.CommonName,
DNSNames: precert.DNSNames,
IncludeMustStaple: ContainsMustStaple(precert.Extensions),
sctList: scts,
precertDER: precert.Raw,
}, nil
}