boulder/csr/csr.go

135 lines
4.4 KiB
Go

package csr
import (
"context"
"crypto"
"crypto/x509"
"errors"
"strings"
"github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/goodkey"
"github.com/letsencrypt/boulder/identifier"
)
// maxCNLength is the maximum length allowed for the common name as specified in RFC 5280
const maxCNLength = 64
// This map is used to decide which CSR signing algorithms we consider
// strong enough to use. Significantly the missing algorithms are:
// * No algorithms using MD2, MD5, or SHA-1
// * No DSA algorithms
var goodSignatureAlgorithms = map[x509.SignatureAlgorithm]bool{
x509.SHA256WithRSA: true,
x509.SHA384WithRSA: true,
x509.SHA512WithRSA: true,
x509.ECDSAWithSHA256: true,
x509.ECDSAWithSHA384: true,
x509.ECDSAWithSHA512: true,
}
var (
invalidPubKey = berrors.BadCSRError("invalid public key in CSR")
unsupportedSigAlg = berrors.BadCSRError("signature algorithm not supported")
invalidSig = berrors.BadCSRError("invalid signature on CSR")
invalidEmailPresent = berrors.BadCSRError("CSR contains one or more email address fields")
invalidIPPresent = berrors.BadCSRError("CSR contains one or more IP address fields")
invalidNoIdent = berrors.BadCSRError("at least one identifier is required")
)
// VerifyCSR checks the validity of a x509.CertificateRequest. It uses
// NamesFromCSR to normalize the DNS names before checking whether we'll issue
// for them.
func VerifyCSR(ctx context.Context, csr *x509.CertificateRequest, maxNames int, keyPolicy *goodkey.KeyPolicy, pa core.PolicyAuthority) error {
key, ok := csr.PublicKey.(crypto.PublicKey)
if !ok {
return invalidPubKey
}
err := keyPolicy.GoodKey(ctx, key)
if err != nil {
if errors.Is(err, goodkey.ErrBadKey) {
return berrors.BadCSRError("invalid public key in CSR: %s", err)
}
return berrors.InternalServerError("error checking key validity: %s", err)
}
if !goodSignatureAlgorithms[csr.SignatureAlgorithm] {
return unsupportedSigAlg
}
err = csr.CheckSignature()
if err != nil {
return invalidSig
}
if len(csr.EmailAddresses) > 0 {
return invalidEmailPresent
}
if len(csr.IPAddresses) > 0 {
return invalidIPPresent
}
// NamesFromCSR also performs normalization, returning values that may not
// match the literal CSR contents.
names := NamesFromCSR(csr)
if len(names.SANs) == 0 && names.CN == "" {
return invalidNoIdent
}
if len(names.CN) > maxCNLength {
return berrors.BadCSRError("CN was longer than %d bytes", maxCNLength)
}
if len(names.SANs) > maxNames {
return berrors.BadCSRError("CSR contains more than %d DNS names", maxNames)
}
err = pa.WillingToIssue(identifier.NewDNSSlice(names.SANs))
if err != nil {
return err
}
return nil
}
type names struct {
SANs []string
CN string
}
// NamesFromCSR deduplicates and lower-cases the Subject Common Name and Subject
// Alternative Names from the CSR. If a CN was provided, it will be used if it
// is short enough, otherwise there will be no CN. If no CN was provided, the CN
// will be the first SAN that is short enough, which is done only for backwards
// compatibility with prior Let's Encrypt behaviour. The resulting SANs will
// always include the original CN, if any.
//
// TODO(#7311): For callers that don't care about CNs, use identifier.FromCSR.
// For the rest, either revise the names struct to hold identifiers instead of
// strings, or add an ipSANs field (and rename SANs to dnsSANs).
func NamesFromCSR(csr *x509.CertificateRequest) names {
// Produce a new "sans" slice with the same memory address as csr.DNSNames
// but force a new allocation if an append happens so that we don't
// accidentally mutate the underlying csr.DNSNames array.
sans := csr.DNSNames[0:len(csr.DNSNames):len(csr.DNSNames)]
if csr.Subject.CommonName != "" {
sans = append(sans, csr.Subject.CommonName)
}
if len(csr.Subject.CommonName) > maxCNLength {
return names{SANs: core.UniqueLowerNames(sans)}
}
if csr.Subject.CommonName != "" {
return names{SANs: core.UniqueLowerNames(sans), CN: strings.ToLower(csr.Subject.CommonName)}
}
// If there's no CN already, but we want to set one, promote the first SAN
// which is shorter than the maximum acceptable CN length (if any).
for _, name := range sans {
if len(name) <= maxCNLength {
return names{SANs: core.UniqueLowerNames(sans), CN: strings.ToLower(name)}
}
}
return names{SANs: core.UniqueLowerNames(sans)}
}