| // Package signer implements certificate signature functionality for CFSSL. |
| package signer |
| |
| import ( |
| "crypto" |
| "crypto/ecdsa" |
| "crypto/elliptic" |
| "crypto/rsa" |
| "crypto/sha1" |
| "crypto/x509" |
| "crypto/x509/pkix" |
| "encoding/asn1" |
| "errors" |
| "math/big" |
| "net/http" |
| "strings" |
| "time" |
| |
| "github.com/cloudflare/cfssl/certdb" |
| "github.com/cloudflare/cfssl/config" |
| "github.com/cloudflare/cfssl/csr" |
| cferr "github.com/cloudflare/cfssl/errors" |
| "github.com/cloudflare/cfssl/info" |
| ) |
| |
| // Subject contains the information that should be used to override the |
| // subject information when signing a certificate. |
| type Subject struct { |
| CN string |
| Names []csr.Name `json:"names"` |
| SerialNumber string |
| } |
| |
| // Extension represents a raw extension to be included in the certificate. The |
| // "value" field must be hex encoded. |
| type Extension struct { |
| ID config.OID `json:"id"` |
| Critical bool `json:"critical"` |
| Value string `json:"value"` |
| } |
| |
| // SignRequest stores a signature request, which contains the hostname, |
| // the CSR, optional subject information, and the signature profile. |
| // |
| // Extensions provided in the signRequest are copied into the certificate, as |
| // long as they are in the ExtensionWhitelist for the signer's policy. |
| // Extensions requested in the CSR are ignored, except for those processed by |
| // ParseCertificateRequest (mainly subjectAltName). |
| type SignRequest struct { |
| Hosts []string `json:"hosts"` |
| Request string `json:"certificate_request"` |
| Subject *Subject `json:"subject,omitempty"` |
| Profile string `json:"profile"` |
| CRLOverride string `json:"crl_override"` |
| Label string `json:"label"` |
| Serial *big.Int `json:"serial,omitempty"` |
| Extensions []Extension `json:"extensions,omitempty"` |
| // If provided, NotBefore will be used without modification (except |
| // for canonicalization) as the value of the notBefore field of the |
| // certificate. In particular no backdating adjustment will be made |
| // when NotBefore is provided. |
| NotBefore time.Time |
| // If provided, NotAfter will be used without modification (except |
| // for canonicalization) as the value of the notAfter field of the |
| // certificate. |
| NotAfter time.Time |
| // If ReturnPrecert is true a certificate with the CT poison extension |
| // will be returned from the Signer instead of attempting to retrieve |
| // SCTs and populate the tbsCert with them itself. This precert can then |
| // be passed to SignFromPrecert with the SCTs in order to create a |
| // valid certificate. |
| ReturnPrecert bool |
| } |
| |
| // 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 subject. |
| func (s *Subject) Name() pkix.Name { |
| var name pkix.Name |
| name.CommonName = s.CN |
| |
| for _, n := range s.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 = s.SerialNumber |
| return name |
| } |
| |
| // SplitHosts takes a comma-spearated list of hosts and returns a slice |
| // with the hosts split |
| func SplitHosts(hostList string) []string { |
| if hostList == "" { |
| return nil |
| } |
| |
| return strings.Split(hostList, ",") |
| } |
| |
| // A Signer contains a CA's certificate and private key for signing |
| // certificates, a Signing policy to refer to and a SignatureAlgorithm. |
| type Signer interface { |
| Info(info.Req) (*info.Resp, error) |
| Policy() *config.Signing |
| SetDBAccessor(certdb.Accessor) |
| GetDBAccessor() certdb.Accessor |
| SetPolicy(*config.Signing) |
| SigAlgo() x509.SignatureAlgorithm |
| Sign(req SignRequest) (cert []byte, err error) |
| SetReqModifier(func(*http.Request, []byte)) |
| } |
| |
| // Profile gets the specific profile from the signer |
| func Profile(s Signer, profile string) (*config.SigningProfile, error) { |
| var p *config.SigningProfile |
| policy := s.Policy() |
| if policy != nil && policy.Profiles != nil && profile != "" { |
| p = policy.Profiles[profile] |
| } |
| |
| if p == nil && policy != nil { |
| p = policy.Default |
| } |
| |
| if p == nil { |
| return nil, cferr.Wrap(cferr.APIClientError, cferr.ClientHTTPError, errors.New("profile must not be nil")) |
| } |
| return p, nil |
| } |
| |
| // DefaultSigAlgo returns an appropriate X.509 signature algorithm given |
| // the CA's private key. |
| func DefaultSigAlgo(priv crypto.Signer) x509.SignatureAlgorithm { |
| pub := priv.Public() |
| switch pub := pub.(type) { |
| case *rsa.PublicKey: |
| keySize := pub.N.BitLen() |
| switch { |
| case keySize >= 4096: |
| return x509.SHA512WithRSA |
| case keySize >= 3072: |
| return x509.SHA384WithRSA |
| case keySize >= 2048: |
| return x509.SHA256WithRSA |
| default: |
| return x509.SHA1WithRSA |
| } |
| case *ecdsa.PublicKey: |
| switch pub.Curve { |
| case elliptic.P256(): |
| return x509.ECDSAWithSHA256 |
| case elliptic.P384(): |
| return x509.ECDSAWithSHA384 |
| case elliptic.P521(): |
| return x509.ECDSAWithSHA512 |
| default: |
| return x509.ECDSAWithSHA1 |
| } |
| default: |
| return x509.UnknownSignatureAlgorithm |
| } |
| } |
| |
| // ParseCertificateRequest takes an incoming certificate request and |
| // builds a certificate template from it. |
| func ParseCertificateRequest(s Signer, csrBytes []byte) (template *x509.Certificate, err error) { |
| csrv, err := x509.ParseCertificateRequest(csrBytes) |
| if err != nil { |
| err = cferr.Wrap(cferr.CSRError, cferr.ParseFailed, err) |
| return |
| } |
| |
| err = csrv.CheckSignature() |
| if err != nil { |
| err = cferr.Wrap(cferr.CSRError, cferr.KeyMismatch, err) |
| return |
| } |
| |
| template = &x509.Certificate{ |
| Subject: csrv.Subject, |
| PublicKeyAlgorithm: csrv.PublicKeyAlgorithm, |
| PublicKey: csrv.PublicKey, |
| SignatureAlgorithm: s.SigAlgo(), |
| DNSNames: csrv.DNSNames, |
| IPAddresses: csrv.IPAddresses, |
| EmailAddresses: csrv.EmailAddresses, |
| } |
| |
| for _, val := range csrv.Extensions { |
| // Check the CSR for the X.509 BasicConstraints (RFC 5280, 4.2.1.9) |
| // extension and append to template if necessary |
| if val.Id.Equal(asn1.ObjectIdentifier{2, 5, 29, 19}) { |
| var constraints csr.BasicConstraints |
| var rest []byte |
| |
| if rest, err = asn1.Unmarshal(val.Value, &constraints); err != nil { |
| return nil, cferr.Wrap(cferr.CSRError, cferr.ParseFailed, err) |
| } else if len(rest) != 0 { |
| return nil, cferr.Wrap(cferr.CSRError, cferr.ParseFailed, errors.New("x509: trailing data after X.509 BasicConstraints")) |
| } |
| |
| template.BasicConstraintsValid = true |
| template.IsCA = constraints.IsCA |
| template.MaxPathLen = constraints.MaxPathLen |
| template.MaxPathLenZero = template.MaxPathLen == 0 |
| } |
| } |
| |
| return |
| } |
| |
| type subjectPublicKeyInfo struct { |
| Algorithm pkix.AlgorithmIdentifier |
| SubjectPublicKey asn1.BitString |
| } |
| |
| // ComputeSKI derives an SKI from the certificate's public key in a |
| // standard manner. This is done by computing the SHA-1 digest of the |
| // SubjectPublicKeyInfo component of the certificate. |
| func ComputeSKI(template *x509.Certificate) ([]byte, error) { |
| pub := template.PublicKey |
| encodedPub, err := x509.MarshalPKIXPublicKey(pub) |
| if err != nil { |
| return nil, err |
| } |
| |
| var subPKI subjectPublicKeyInfo |
| _, err = asn1.Unmarshal(encodedPub, &subPKI) |
| if err != nil { |
| return nil, err |
| } |
| |
| pubHash := sha1.Sum(subPKI.SubjectPublicKey.Bytes) |
| return pubHash[:], nil |
| } |
| |
| // FillTemplate is a utility function that tries to load as much of |
| // the certificate template as possible from the profiles and current |
| // template. It fills in the key uses, expiration, revocation URLs |
| // and SKI. |
| func FillTemplate(template *x509.Certificate, defaultProfile, profile *config.SigningProfile, notBefore time.Time, notAfter time.Time) error { |
| ski, err := ComputeSKI(template) |
| if err != nil { |
| return err |
| } |
| |
| var ( |
| eku []x509.ExtKeyUsage |
| ku x509.KeyUsage |
| backdate time.Duration |
| expiry time.Duration |
| crlURL, ocspURL string |
| issuerURL = profile.IssuerURL |
| ) |
| |
| // The third value returned from Usages is a list of unknown key usages. |
| // This should be used when validating the profile at load, and isn't used |
| // here. |
| ku, eku, _ = profile.Usages() |
| if profile.IssuerURL == nil { |
| issuerURL = defaultProfile.IssuerURL |
| } |
| |
| if ku == 0 && len(eku) == 0 { |
| return cferr.New(cferr.PolicyError, cferr.NoKeyUsages) |
| } |
| |
| if expiry = profile.Expiry; expiry == 0 { |
| expiry = defaultProfile.Expiry |
| } |
| |
| if crlURL = profile.CRL; crlURL == "" { |
| crlURL = defaultProfile.CRL |
| } |
| if ocspURL = profile.OCSP; ocspURL == "" { |
| ocspURL = defaultProfile.OCSP |
| } |
| |
| if notBefore.IsZero() { |
| if !profile.NotBefore.IsZero() { |
| notBefore = profile.NotBefore |
| } else { |
| if backdate = profile.Backdate; backdate == 0 { |
| backdate = -5 * time.Minute |
| } else { |
| backdate = -1 * profile.Backdate |
| } |
| notBefore = time.Now().Round(time.Minute).Add(backdate) |
| } |
| } |
| notBefore = notBefore.UTC() |
| |
| if notAfter.IsZero() { |
| if !profile.NotAfter.IsZero() { |
| notAfter = profile.NotAfter |
| } else { |
| notAfter = notBefore.Add(expiry) |
| } |
| } |
| notAfter = notAfter.UTC() |
| |
| template.NotBefore = notBefore |
| template.NotAfter = notAfter |
| template.KeyUsage = ku |
| template.ExtKeyUsage = eku |
| template.BasicConstraintsValid = true |
| template.IsCA = profile.CAConstraint.IsCA |
| if template.IsCA { |
| template.MaxPathLen = profile.CAConstraint.MaxPathLen |
| if template.MaxPathLen == 0 { |
| template.MaxPathLenZero = profile.CAConstraint.MaxPathLenZero |
| } |
| template.DNSNames = nil |
| template.EmailAddresses = nil |
| } |
| template.SubjectKeyId = ski |
| |
| if ocspURL != "" { |
| template.OCSPServer = []string{ocspURL} |
| } |
| if crlURL != "" { |
| template.CRLDistributionPoints = []string{crlURL} |
| } |
| |
| if len(issuerURL) != 0 { |
| template.IssuingCertificateURL = issuerURL |
| } |
| if len(profile.Policies) != 0 { |
| err = addPolicies(template, profile.Policies) |
| if err != nil { |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, err) |
| } |
| } |
| if profile.OCSPNoCheck { |
| ocspNoCheckExtension := pkix.Extension{ |
| Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 48, 1, 5}, |
| Critical: false, |
| Value: []byte{0x05, 0x00}, |
| } |
| template.ExtraExtensions = append(template.ExtraExtensions, ocspNoCheckExtension) |
| } |
| |
| return nil |
| } |
| |
| type policyInformation struct { |
| PolicyIdentifier asn1.ObjectIdentifier |
| Qualifiers []interface{} `asn1:"tag:optional,omitempty"` |
| } |
| |
| type cpsPolicyQualifier struct { |
| PolicyQualifierID asn1.ObjectIdentifier |
| Qualifier string `asn1:"tag:optional,ia5"` |
| } |
| |
| type userNotice struct { |
| ExplicitText string `asn1:"tag:optional,utf8"` |
| } |
| type userNoticePolicyQualifier struct { |
| PolicyQualifierID asn1.ObjectIdentifier |
| Qualifier userNotice |
| } |
| |
| var ( |
| // Per https://tools.ietf.org/html/rfc3280.html#page-106, this represents: |
| // iso(1) identified-organization(3) dod(6) internet(1) security(5) |
| // mechanisms(5) pkix(7) id-qt(2) id-qt-cps(1) |
| iDQTCertificationPracticeStatement = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 2, 1} |
| // iso(1) identified-organization(3) dod(6) internet(1) security(5) |
| // mechanisms(5) pkix(7) id-qt(2) id-qt-unotice(2) |
| iDQTUserNotice = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 2, 2} |
| |
| // CTPoisonOID is the object ID of the critical poison extension for precertificates |
| // https://tools.ietf.org/html/rfc6962#page-9 |
| CTPoisonOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3} |
| |
| // SCTListOID is the object ID for the Signed Certificate Timestamp certificate extension |
| // https://tools.ietf.org/html/rfc6962#page-14 |
| SCTListOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2} |
| ) |
| |
| // addPolicies adds Certificate Policies and optional Policy Qualifiers to a |
| // certificate, based on the input config. Go's x509 library allows setting |
| // Certificate Policies easily, but does not support nested Policy Qualifiers |
| // under those policies. So we need to construct the ASN.1 structure ourselves. |
| func addPolicies(template *x509.Certificate, policies []config.CertificatePolicy) error { |
| asn1PolicyList := []policyInformation{} |
| |
| for _, policy := range policies { |
| pi := policyInformation{ |
| // The PolicyIdentifier is an OID assigned to a given issuer. |
| PolicyIdentifier: asn1.ObjectIdentifier(policy.ID), |
| } |
| for _, qualifier := range policy.Qualifiers { |
| switch qualifier.Type { |
| case "id-qt-unotice": |
| pi.Qualifiers = append(pi.Qualifiers, |
| userNoticePolicyQualifier{ |
| PolicyQualifierID: iDQTUserNotice, |
| Qualifier: userNotice{ |
| ExplicitText: qualifier.Value, |
| }, |
| }) |
| case "id-qt-cps": |
| pi.Qualifiers = append(pi.Qualifiers, |
| cpsPolicyQualifier{ |
| PolicyQualifierID: iDQTCertificationPracticeStatement, |
| Qualifier: qualifier.Value, |
| }) |
| default: |
| return errors.New("Invalid qualifier type in Policies " + qualifier.Type) |
| } |
| } |
| asn1PolicyList = append(asn1PolicyList, pi) |
| } |
| |
| asn1Bytes, err := asn1.Marshal(asn1PolicyList) |
| if err != nil { |
| return err |
| } |
| |
| template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{ |
| Id: asn1.ObjectIdentifier{2, 5, 29, 32}, |
| Critical: false, |
| Value: asn1Bytes, |
| }) |
| return nil |
| } |