// Package csr implements certificate requests for CFSSL. package csr import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" "errors" "net" "net/mail" "strings" cferr "github.com/cloudflare/cfssl/errors" "github.com/cloudflare/cfssl/helpers" "github.com/cloudflare/cfssl/log" ) const ( curveP256 = 256 curveP384 = 384 curveP521 = 521 ) // A Name contains the SubjectInfo fields. type Name struct { C string // Country ST string // State L string // Locality O string // OrganisationName OU string // OrganisationalUnitName SerialNumber string } // A KeyRequest is a generic request for a new key. type KeyRequest interface { Algo() string Size() int Generate() (crypto.PrivateKey, error) SigAlgo() x509.SignatureAlgorithm } // A BasicKeyRequest contains the algorithm and key size for a new private key. type BasicKeyRequest struct { A string `json:"algo"` S int `json:"size"` } // NewBasicKeyRequest returns a default BasicKeyRequest. func NewBasicKeyRequest() *BasicKeyRequest { return &BasicKeyRequest{"ecdsa", curveP256} } // Algo returns the requested key algorithm represented as a string. func (kr *BasicKeyRequest) Algo() string { return kr.A } // Size returns the requested key size. func (kr *BasicKeyRequest) Size() int { return kr.S } // Generate generates a key as specified in the request. Currently, // only ECDSA and RSA are supported. func (kr *BasicKeyRequest) Generate() (crypto.PrivateKey, error) { log.Debugf("generate key from request: algo=%s, size=%d", kr.Algo(), kr.Size()) switch kr.Algo() { case "rsa": if kr.Size() < 2048 { return nil, errors.New("RSA key is too weak") } if kr.Size() > 8192 { return nil, errors.New("RSA key size too large") } return rsa.GenerateKey(rand.Reader, kr.Size()) case "ecdsa": var curve elliptic.Curve switch kr.Size() { case curveP256: curve = elliptic.P256() case curveP384: curve = elliptic.P384() case curveP521: curve = elliptic.P521() default: return nil, errors.New("invalid curve") } return ecdsa.GenerateKey(curve, rand.Reader) default: return nil, errors.New("invalid algorithm") } } // SigAlgo returns an appropriate X.509 signature algorithm given the // key request's type and size. func (kr *BasicKeyRequest) SigAlgo() x509.SignatureAlgorithm { switch kr.Algo() { case "rsa": switch { case kr.Size() >= 4096: return x509.SHA512WithRSA case kr.Size() >= 3072: return x509.SHA384WithRSA case kr.Size() >= 2048: return x509.SHA256WithRSA default: return x509.SHA1WithRSA } case "ecdsa": switch kr.Size() { case curveP521: return x509.ECDSAWithSHA512 case curveP384: return x509.ECDSAWithSHA384 case curveP256: return x509.ECDSAWithSHA256 default: return x509.ECDSAWithSHA1 } default: return x509.UnknownSignatureAlgorithm } } // CAConfig is a section used in the requests initialising a new CA. type CAConfig struct { PathLength int `json:"pathlen"` PathLenZero bool `json:"pathlenzero"` Expiry string `json:"expiry"` } // A CertificateRequest encapsulates the API interface to the // certificate request functionality. type CertificateRequest struct { CN string Names []Name `json:"names"` Hosts []string `json:"hosts"` KeyRequest KeyRequest `json:"key,omitempty"` CA *CAConfig `json:"ca,omitempty"` SerialNumber string `json:"serialnumber,omitempty"` } // New returns a new, empty CertificateRequest with a // BasicKeyRequest. func New() *CertificateRequest { return &CertificateRequest{ KeyRequest: NewBasicKeyRequest(), } } // appendIf appends to a if s is not an empty string. func appendIf(s string, a *[]string) { if s != "" { *a = append(*a, s) } } // Name returns the PKIX name for the request. func (cr *CertificateRequest) Name() pkix.Name { var name pkix.Name name.CommonName = cr.CN for _, n := range cr.Names { appendIf(n.C, &name.Country) appendIf(n.ST, &name.Province) appendIf(n.L, &name.Locality) appendIf(n.O, &name.Organization) appendIf(n.OU, &name.OrganizationalUnit) } name.SerialNumber = cr.SerialNumber return name } // BasicConstraints CSR information RFC 5280, 4.2.1.9 type BasicConstraints struct { IsCA bool `asn1:"optional"` MaxPathLen int `asn1:"optional,default:-1"` } // ParseRequest takes a certificate request and generates a key and // CSR from it. It does no validation -- caveat emptor. It will, // however, fail if the key request is not valid (i.e., an unsupported // curve or RSA key size). The lack of validation was specifically // chosen to allow the end user to define a policy and validate the // request appropriately before calling this function. func ParseRequest(req *CertificateRequest) (csr, key []byte, err error) { log.Info("received CSR") if req.KeyRequest == nil { req.KeyRequest = NewBasicKeyRequest() } log.Infof("generating key: %s-%d", req.KeyRequest.Algo(), req.KeyRequest.Size()) priv, err := req.KeyRequest.Generate() if err != nil { err = cferr.Wrap(cferr.PrivateKeyError, cferr.GenerationFailed, err) return } switch priv := priv.(type) { case *rsa.PrivateKey: key = x509.MarshalPKCS1PrivateKey(priv) block := pem.Block{ Type: "RSA PRIVATE KEY", Bytes: key, } key = pem.EncodeToMemory(&block) case *ecdsa.PrivateKey: key, err = x509.MarshalECPrivateKey(priv) if err != nil { err = cferr.Wrap(cferr.PrivateKeyError, cferr.Unknown, err) return } block := pem.Block{ Type: "EC PRIVATE KEY", Bytes: key, } key = pem.EncodeToMemory(&block) default: panic("Generate should have failed to produce a valid key.") } csr, err = Generate(priv.(crypto.Signer), req) if err != nil { log.Errorf("failed to generate a CSR: %v", err) err = cferr.Wrap(cferr.CSRError, cferr.BadRequest, err) } return } // ExtractCertificateRequest extracts a CertificateRequest from // x509.Certificate. It is aimed to used for generating a new certificate // from an existing certificate. For a root certificate, the CA expiry // length is calculated as the duration between cert.NotAfter and cert.NotBefore. func ExtractCertificateRequest(cert *x509.Certificate) *CertificateRequest { req := New() req.CN = cert.Subject.CommonName req.Names = getNames(cert.Subject) req.Hosts = getHosts(cert) req.SerialNumber = cert.Subject.SerialNumber if cert.IsCA { req.CA = new(CAConfig) // CA expiry length is calculated based on the input cert // issue date and expiry date. req.CA.Expiry = cert.NotAfter.Sub(cert.NotBefore).String() req.CA.PathLength = cert.MaxPathLen req.CA.PathLenZero = cert.MaxPathLenZero } return req } func getHosts(cert *x509.Certificate) []string { var hosts []string for _, ip := range cert.IPAddresses { hosts = append(hosts, ip.String()) } for _, dns := range cert.DNSNames { hosts = append(hosts, dns) } for _, email := range cert.EmailAddresses { hosts = append(hosts, email) } return hosts } // getNames returns an array of Names from the certificate // It onnly cares about Country, Organization, OrganizationalUnit, Locality, Province func getNames(sub pkix.Name) []Name { // anonymous func for finding the max of a list of interger max := func(v1 int, vn ...int) (max int) { max = v1 for i := 0; i < len(vn); i++ { if vn[i] > max { max = vn[i] } } return max } nc := len(sub.Country) norg := len(sub.Organization) nou := len(sub.OrganizationalUnit) nl := len(sub.Locality) np := len(sub.Province) n := max(nc, norg, nou, nl, np) names := make([]Name, n) for i := range names { if i < nc { names[i].C = sub.Country[i] } if i < norg { names[i].O = sub.Organization[i] } if i < nou { names[i].OU = sub.OrganizationalUnit[i] } if i < nl { names[i].L = sub.Locality[i] } if i < np { names[i].ST = sub.Province[i] } } return names } // A Generator is responsible for validating certificate requests. type Generator struct { Validator func(*CertificateRequest) error } // ProcessRequest validates and processes the incoming request. It is // a wrapper around a validator and the ParseRequest function. func (g *Generator) ProcessRequest(req *CertificateRequest) (csr, key []byte, err error) { log.Info("generate received request") err = g.Validator(req) if err != nil { log.Warningf("invalid request: %v", err) return } csr, key, err = ParseRequest(req) if err != nil { return nil, nil, err } return } // IsNameEmpty returns true if the name has no identifying information in it. func IsNameEmpty(n Name) bool { empty := func(s string) bool { return strings.TrimSpace(s) == "" } if empty(n.C) && empty(n.ST) && empty(n.L) && empty(n.O) && empty(n.OU) { return true } return false } // Regenerate uses the provided CSR as a template for signing a new // CSR using priv. func Regenerate(priv crypto.Signer, csr []byte) ([]byte, error) { req, extra, err := helpers.ParseCSR(csr) if err != nil { return nil, err } else if len(extra) > 0 { return nil, errors.New("csr: trailing data in certificate request") } return x509.CreateCertificateRequest(rand.Reader, req, priv) } // Generate creates a new CSR from a CertificateRequest structure and // an existing key. The KeyRequest field is ignored. func Generate(priv crypto.Signer, req *CertificateRequest) (csr []byte, err error) { sigAlgo := helpers.SignerAlgo(priv) if sigAlgo == x509.UnknownSignatureAlgorithm { return nil, cferr.New(cferr.PrivateKeyError, cferr.Unavailable) } var tpl = x509.CertificateRequest{ Subject: req.Name(), SignatureAlgorithm: sigAlgo, } for i := range req.Hosts { if ip := net.ParseIP(req.Hosts[i]); ip != nil { tpl.IPAddresses = append(tpl.IPAddresses, ip) } else if email, err := mail.ParseAddress(req.Hosts[i]); err == nil && email != nil { tpl.EmailAddresses = append(tpl.EmailAddresses, email.Address) } else { tpl.DNSNames = append(tpl.DNSNames, req.Hosts[i]) } } if req.CA != nil { err = appendCAInfoToCSR(req.CA, &tpl) if err != nil { err = cferr.Wrap(cferr.CSRError, cferr.GenerationFailed, err) return } } csr, err = x509.CreateCertificateRequest(rand.Reader, &tpl, priv) if err != nil { log.Errorf("failed to generate a CSR: %v", err) err = cferr.Wrap(cferr.CSRError, cferr.BadRequest, err) return } block := pem.Block{ Type: "CERTIFICATE REQUEST", Bytes: csr, } log.Info("encoded CSR") csr = pem.EncodeToMemory(&block) return } // appendCAInfoToCSR appends CAConfig BasicConstraint extension to a CSR func appendCAInfoToCSR(reqConf *CAConfig, csr *x509.CertificateRequest) error { pathlen := reqConf.PathLength if pathlen == 0 && !reqConf.PathLenZero { pathlen = -1 } val, err := asn1.Marshal(BasicConstraints{true, pathlen}) if err != nil { return err } csr.ExtraExtensions = []pkix.Extension{ { Id: asn1.ObjectIdentifier{2, 5, 29, 19}, Value: val, Critical: true, }, } return nil }