boulder/csr/csr.go

105 lines
3.5 KiB
Go

package csr
import (
"crypto"
"crypto/x509"
"errors"
"fmt"
"strings"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/goodkey"
)
// maxCNLength is the maximum length allowed for the common name as specified in RFC 5280
const maxCNLength = 64
// This map is used to detect algorithms in crypto/x509 that
// are no longer considered sufficiently strong.
// * No MD2, MD5, or SHA-1
// * No DSA
//
// SHA1WithRSA is allowed because there's still a fair bit of it
// out there, but we should try to remove it soon.
var badSignatureAlgorithms = map[x509.SignatureAlgorithm]bool{
x509.UnknownSignatureAlgorithm: true,
x509.MD2WithRSA: true,
x509.MD5WithRSA: true,
x509.DSAWithSHA1: true,
x509.DSAWithSHA256: true,
x509.ECDSAWithSHA1: true,
}
var (
invalidPubKey = errors.New("invalid public key in CSR")
unsupportedSigAlg = errors.New("signature algorithm not supported")
invalidSig = errors.New("invalid signature on CSR")
invalidEmailPresent = errors.New("CSR contains one or more email address fields")
invalidIPPresent = errors.New("CSR contains one or more IP address fields")
invalidNoDNS = errors.New("at least one DNS name is required")
)
// VerifyCSR checks the validity of a x509.CertificateRequest. Before doing checks it normalizes
// the CSR which lowers the case of DNS names and subject CN, and if forceCNFromSAN is true it
// will hoist a DNS name into the CN if it is empty.
func VerifyCSR(csr *x509.CertificateRequest, maxNames int, keyPolicy *goodkey.KeyPolicy, pa core.PolicyAuthority, forceCNFromSAN bool, regID int64) error {
normalizeCSR(csr, forceCNFromSAN)
key, ok := csr.PublicKey.(crypto.PublicKey)
if !ok {
return invalidPubKey
}
if err := keyPolicy.GoodKey(key); err != nil {
return fmt.Errorf("invalid public key in CSR: %s", err)
}
if badSignatureAlgorithms[csr.SignatureAlgorithm] {
// go1.6 provides a stringer for x509.SignatureAlgorithm but 1.5.x
// does not
return unsupportedSigAlg
}
if err := csr.CheckSignature(); err != nil {
return invalidSig
}
if len(csr.EmailAddresses) > 0 {
return invalidEmailPresent
}
if len(csr.IPAddresses) > 0 {
return invalidIPPresent
}
if len(csr.DNSNames) == 0 && csr.Subject.CommonName == "" {
return invalidNoDNS
}
if len(csr.Subject.CommonName) > maxCNLength {
return fmt.Errorf("CN was longer than %d bytes", maxCNLength)
}
if maxNames > 0 && len(csr.DNSNames) > maxNames {
return fmt.Errorf("CSR contains more than %d DNS names", maxNames)
}
badNames := []string{}
for _, name := range csr.DNSNames {
if err := pa.WillingToIssue(core.AcmeIdentifier{
Type: core.IdentifierDNS,
Value: name,
}); err != nil {
badNames = append(badNames, fmt.Sprintf("%q", name))
}
}
if len(badNames) > 0 {
return fmt.Errorf("policy forbids issuing for: %s", strings.Join(badNames, ", "))
}
return nil
}
// normalizeCSR deduplicates and lowers the case of dNSNames and the subject CN.
// If forceCNFromSAN is true it will also hoist a dNSName into the CN if it is empty.
func normalizeCSR(csr *x509.CertificateRequest, forceCNFromSAN bool) {
if forceCNFromSAN && csr.Subject.CommonName == "" {
if len(csr.DNSNames) > 0 {
csr.Subject.CommonName = csr.DNSNames[0]
}
} else if csr.Subject.CommonName != "" {
csr.DNSNames = append(csr.DNSNames, csr.Subject.CommonName)
}
csr.Subject.CommonName = strings.ToLower(csr.Subject.CommonName)
csr.DNSNames = core.UniqueLowerNames(csr.DNSNames)
}