437 lines
13 KiB
Go
437 lines
13 KiB
Go
|
// 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
|
||
|
}
|