2016-06-30 13:34:48 -07:00
|
|
|
package ca
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2017-03-10 11:15:44 -08:00
|
|
|
cryptorand "crypto/rand"
|
2016-06-30 13:34:48 -07:00
|
|
|
"crypto/tls"
|
2017-03-10 11:15:44 -08:00
|
|
|
"crypto/x509"
|
|
|
|
"encoding/hex"
|
2016-06-30 13:34:48 -07:00
|
|
|
"encoding/json"
|
2017-03-10 11:15:44 -08:00
|
|
|
"encoding/pem"
|
2016-06-30 13:34:48 -07:00
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"sync"
|
2017-03-30 16:12:33 -07:00
|
|
|
"time"
|
2016-06-30 13:34:48 -07:00
|
|
|
|
2016-11-02 19:43:27 +01:00
|
|
|
"github.com/Sirupsen/logrus"
|
2016-06-30 13:34:48 -07:00
|
|
|
"github.com/cloudflare/cfssl/api"
|
2017-03-10 11:15:44 -08:00
|
|
|
"github.com/cloudflare/cfssl/config"
|
|
|
|
"github.com/cloudflare/cfssl/csr"
|
2016-06-30 13:34:48 -07:00
|
|
|
"github.com/cloudflare/cfssl/signer"
|
2017-06-12 10:27:11 -07:00
|
|
|
"github.com/docker/swarmkit/log"
|
2016-09-26 23:48:16 -07:00
|
|
|
"github.com/pkg/errors"
|
2016-11-18 12:41:16 -08:00
|
|
|
"golang.org/x/net/context"
|
|
|
|
"golang.org/x/net/context/ctxhttp"
|
2016-06-30 13:34:48 -07:00
|
|
|
)
|
|
|
|
|
2017-06-12 10:27:11 -07:00
|
|
|
// ExternalCrossSignProfile is the profile that we will be sending cross-signing CSR sign requests with
|
|
|
|
const ExternalCrossSignProfile = "CA"
|
|
|
|
|
2016-06-30 13:34:48 -07:00
|
|
|
// ErrNoExternalCAURLs is an error used it indicate that an ExternalCA is
|
|
|
|
// configured with no URLs to which it can proxy certificate signing requests.
|
|
|
|
var ErrNoExternalCAURLs = errors.New("no external CA URLs")
|
|
|
|
|
|
|
|
// ExternalCA is able to make certificate signing requests to one of a list
|
|
|
|
// remote CFSSL API endpoints.
|
|
|
|
type ExternalCA struct {
|
2017-03-30 16:12:33 -07:00
|
|
|
ExternalRequestTimeout time.Duration
|
|
|
|
|
2016-06-30 13:34:48 -07:00
|
|
|
mu sync.Mutex
|
|
|
|
rootCA *RootCA
|
|
|
|
urls []string
|
|
|
|
client *http.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewExternalCA creates a new ExternalCA which uses the given tlsConfig to
|
|
|
|
// authenticate to any of the given URLS of CFSSL API endpoints.
|
|
|
|
func NewExternalCA(rootCA *RootCA, tlsConfig *tls.Config, urls ...string) *ExternalCA {
|
|
|
|
return &ExternalCA{
|
2017-03-30 16:12:33 -07:00
|
|
|
ExternalRequestTimeout: 5 * time.Second,
|
|
|
|
rootCA: rootCA,
|
|
|
|
urls: urls,
|
2016-06-30 13:34:48 -07:00
|
|
|
client: &http.Client{
|
|
|
|
Transport: &http.Transport{
|
|
|
|
TLSClientConfig: tlsConfig,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-30 16:12:33 -07:00
|
|
|
// Copy returns a copy of the external CA that can be updated independently
|
|
|
|
func (eca *ExternalCA) Copy() *ExternalCA {
|
|
|
|
eca.mu.Lock()
|
|
|
|
defer eca.mu.Unlock()
|
|
|
|
|
|
|
|
return &ExternalCA{
|
|
|
|
ExternalRequestTimeout: eca.ExternalRequestTimeout,
|
|
|
|
rootCA: eca.rootCA,
|
|
|
|
urls: eca.urls,
|
|
|
|
client: eca.client,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-30 13:34:48 -07:00
|
|
|
// UpdateTLSConfig updates the HTTP Client for this ExternalCA by creating
|
|
|
|
// a new client which uses the given tlsConfig.
|
|
|
|
func (eca *ExternalCA) UpdateTLSConfig(tlsConfig *tls.Config) {
|
|
|
|
eca.mu.Lock()
|
|
|
|
defer eca.mu.Unlock()
|
|
|
|
|
|
|
|
eca.client = &http.Client{
|
|
|
|
Transport: &http.Transport{
|
|
|
|
TLSClientConfig: tlsConfig,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-12 10:27:11 -07:00
|
|
|
// UpdateURLs updates the list of CSR API endpoints by setting it to the given urls.
|
2016-06-30 13:34:48 -07:00
|
|
|
func (eca *ExternalCA) UpdateURLs(urls ...string) {
|
|
|
|
eca.mu.Lock()
|
|
|
|
defer eca.mu.Unlock()
|
|
|
|
|
|
|
|
eca.urls = urls
|
|
|
|
}
|
|
|
|
|
2017-06-12 10:27:11 -07:00
|
|
|
// UpdateRootCA changes the root CA used to append intermediates
|
|
|
|
func (eca *ExternalCA) UpdateRootCA(rca *RootCA) {
|
|
|
|
eca.mu.Lock()
|
|
|
|
eca.rootCA = rca
|
|
|
|
eca.mu.Unlock()
|
|
|
|
}
|
|
|
|
|
2016-06-30 13:34:48 -07:00
|
|
|
// Sign signs a new certificate by proxying the given certificate signing
|
|
|
|
// request to an external CFSSL API server.
|
2016-11-18 12:41:16 -08:00
|
|
|
func (eca *ExternalCA) Sign(ctx context.Context, req signer.SignRequest) (cert []byte, err error) {
|
2016-06-30 13:34:48 -07:00
|
|
|
// Get the current HTTP client and list of URLs in a small critical
|
|
|
|
// section. We will use these to make certificate signing requests.
|
|
|
|
eca.mu.Lock()
|
|
|
|
urls := eca.urls
|
|
|
|
client := eca.client
|
2017-06-12 10:27:11 -07:00
|
|
|
intermediates := eca.rootCA.Intermediates
|
2016-06-30 13:34:48 -07:00
|
|
|
eca.mu.Unlock()
|
|
|
|
|
|
|
|
if len(urls) == 0 {
|
|
|
|
return nil, ErrNoExternalCAURLs
|
|
|
|
}
|
|
|
|
|
|
|
|
csrJSON, err := json.Marshal(req)
|
|
|
|
if err != nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.Wrap(err, "unable to JSON-encode CFSSL signing request")
|
2016-06-30 13:34:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Try each configured proxy URL. Return after the first success. If
|
|
|
|
// all fail then the last error will be returned.
|
|
|
|
for _, url := range urls {
|
2017-03-30 16:12:33 -07:00
|
|
|
requestCtx, cancel := context.WithTimeout(ctx, eca.ExternalRequestTimeout)
|
|
|
|
cert, err = makeExternalSignRequest(requestCtx, client, url, csrJSON)
|
|
|
|
cancel()
|
2016-06-30 13:34:48 -07:00
|
|
|
if err == nil {
|
2017-06-12 10:27:11 -07:00
|
|
|
return append(cert, intermediates...), err
|
2016-06-30 13:34:48 -07:00
|
|
|
}
|
2017-06-12 10:27:11 -07:00
|
|
|
log.G(ctx).Debugf("unable to proxy certificate signing request to %s: %s", url, err)
|
2016-06-30 13:34:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-03-10 11:15:44 -08:00
|
|
|
// CrossSignRootCA takes a RootCA object, generates a CA CSR, sends a signing request with the CA CSR to the external
|
|
|
|
// CFSSL API server in order to obtain a cross-signed root
|
|
|
|
func (eca *ExternalCA) CrossSignRootCA(ctx context.Context, rca RootCA) ([]byte, error) {
|
|
|
|
// ExtractCertificateRequest generates a new key request, and we want to continue to use the old
|
|
|
|
// key. However, ExtractCertificateRequest will also convert the pkix.Name to csr.Name, which we
|
|
|
|
// need in order to generate a signing request
|
2017-03-28 11:51:33 -07:00
|
|
|
rcaSigner, err := rca.Signer()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
rootCert := rcaSigner.parsedCert
|
2017-03-10 11:15:44 -08:00
|
|
|
cfCSRObj := csr.ExtractCertificateRequest(rootCert)
|
|
|
|
|
|
|
|
der, err := x509.CreateCertificateRequest(cryptorand.Reader, &x509.CertificateRequest{
|
|
|
|
RawSubjectPublicKeyInfo: rootCert.RawSubjectPublicKeyInfo,
|
|
|
|
RawSubject: rootCert.RawSubject,
|
|
|
|
PublicKeyAlgorithm: rootCert.PublicKeyAlgorithm,
|
|
|
|
Subject: rootCert.Subject,
|
|
|
|
Extensions: rootCert.Extensions,
|
|
|
|
DNSNames: rootCert.DNSNames,
|
|
|
|
EmailAddresses: rootCert.EmailAddresses,
|
|
|
|
IPAddresses: rootCert.IPAddresses,
|
2017-03-28 11:51:33 -07:00
|
|
|
}, rcaSigner.cryptoSigner)
|
2017-03-10 11:15:44 -08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
req := signer.SignRequest{
|
|
|
|
Request: string(pem.EncodeToMemory(&pem.Block{
|
|
|
|
Type: "CERTIFICATE REQUEST",
|
|
|
|
Bytes: der,
|
|
|
|
})),
|
|
|
|
Subject: &signer.Subject{
|
|
|
|
CN: rootCert.Subject.CommonName,
|
|
|
|
Names: cfCSRObj.Names,
|
|
|
|
},
|
2017-06-12 10:27:11 -07:00
|
|
|
Profile: ExternalCrossSignProfile,
|
2017-03-10 11:15:44 -08:00
|
|
|
}
|
|
|
|
// cfssl actually ignores non subject alt name extensions in the CSR, so we have to add the CA extension in the signing
|
|
|
|
// request as well
|
|
|
|
for _, ext := range rootCert.Extensions {
|
|
|
|
if ext.Id.Equal(BasicConstraintsOID) {
|
|
|
|
req.Extensions = append(req.Extensions, signer.Extension{
|
|
|
|
ID: config.OID(ext.Id),
|
|
|
|
Critical: ext.Critical,
|
|
|
|
Value: hex.EncodeToString(ext.Value),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return eca.Sign(ctx, req)
|
|
|
|
}
|
|
|
|
|
2016-11-18 12:41:16 -08:00
|
|
|
func makeExternalSignRequest(ctx context.Context, client *http.Client, url string, csrJSON []byte) (cert []byte, err error) {
|
|
|
|
resp, err := ctxhttp.Post(ctx, client, url, "application/json", bytes.NewReader(csrJSON))
|
2016-06-30 13:34:48 -07:00
|
|
|
if err != nil {
|
2016-10-20 11:26:04 -07:00
|
|
|
return nil, recoverableErr{err: errors.Wrap(err, "unable to perform certificate signing request")}
|
2016-06-30 13:34:48 -07:00
|
|
|
}
|
2017-01-19 17:18:22 -08:00
|
|
|
defer resp.Body.Close()
|
2016-06-30 13:34:48 -07:00
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
2016-10-20 11:26:04 -07:00
|
|
|
return nil, recoverableErr{err: errors.Wrap(err, "unable to read CSR response body")}
|
2016-06-30 13:34:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
2016-10-20 11:26:04 -07:00
|
|
|
return nil, recoverableErr{err: errors.Errorf("unexpected status code in CSR response: %d - %s", resp.StatusCode, string(body))}
|
2016-06-30 13:34:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
var apiResponse api.Response
|
|
|
|
if err := json.Unmarshal(body, &apiResponse); err != nil {
|
2016-11-02 19:43:27 +01:00
|
|
|
logrus.Debugf("unable to JSON-parse CFSSL API response body: %s", string(body))
|
2016-10-20 11:26:04 -07:00
|
|
|
return nil, recoverableErr{err: errors.Wrap(err, "unable to parse JSON response")}
|
2016-06-30 13:34:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if !apiResponse.Success || apiResponse.Result == nil {
|
|
|
|
if len(apiResponse.Errors) > 0 {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.Errorf("response errors: %v", apiResponse.Errors)
|
2016-06-30 13:34:48 -07:00
|
|
|
}
|
|
|
|
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.New("certificate signing request failed")
|
2016-06-30 13:34:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
result, ok := apiResponse.Result.(map[string]interface{})
|
|
|
|
if !ok {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.Errorf("invalid result type: %T", apiResponse.Result)
|
2016-06-30 13:34:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
certPEM, ok := result["certificate"].(string)
|
|
|
|
if !ok {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.Errorf("invalid result certificate field type: %T", result["certificate"])
|
2016-06-30 13:34:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return []byte(certPEM), nil
|
|
|
|
}
|