5 cryptorand "crypto/rand"
16 "github.com/Sirupsen/logrus"
17 "github.com/cloudflare/cfssl/api"
18 "github.com/cloudflare/cfssl/config"
19 "github.com/cloudflare/cfssl/csr"
20 "github.com/cloudflare/cfssl/signer"
21 "github.com/docker/swarmkit/log"
22 "github.com/pkg/errors"
23 "golang.org/x/net/context"
24 "golang.org/x/net/context/ctxhttp"
27 // ExternalCrossSignProfile is the profile that we will be sending cross-signing CSR sign requests with
28 const ExternalCrossSignProfile = "CA"
30 // ErrNoExternalCAURLs is an error used it indicate that an ExternalCA is
31 // configured with no URLs to which it can proxy certificate signing requests.
32 var ErrNoExternalCAURLs = errors.New("no external CA URLs")
34 // ExternalCA is able to make certificate signing requests to one of a list
35 // remote CFSSL API endpoints.
36 type ExternalCA struct {
37 ExternalRequestTimeout time.Duration
45 // NewExternalCA creates a new ExternalCA which uses the given tlsConfig to
46 // authenticate to any of the given URLS of CFSSL API endpoints.
47 func NewExternalCA(rootCA *RootCA, tlsConfig *tls.Config, urls ...string) *ExternalCA {
49 ExternalRequestTimeout: 5 * time.Second,
53 Transport: &http.Transport{
54 TLSClientConfig: tlsConfig,
60 // Copy returns a copy of the external CA that can be updated independently
61 func (eca *ExternalCA) Copy() *ExternalCA {
66 ExternalRequestTimeout: eca.ExternalRequestTimeout,
73 // UpdateTLSConfig updates the HTTP Client for this ExternalCA by creating
74 // a new client which uses the given tlsConfig.
75 func (eca *ExternalCA) UpdateTLSConfig(tlsConfig *tls.Config) {
79 eca.client = &http.Client{
80 Transport: &http.Transport{
81 TLSClientConfig: tlsConfig,
86 // UpdateURLs updates the list of CSR API endpoints by setting it to the given urls.
87 func (eca *ExternalCA) UpdateURLs(urls ...string) {
94 // UpdateRootCA changes the root CA used to append intermediates
95 func (eca *ExternalCA) UpdateRootCA(rca *RootCA) {
101 // Sign signs a new certificate by proxying the given certificate signing
102 // request to an external CFSSL API server.
103 func (eca *ExternalCA) Sign(ctx context.Context, req signer.SignRequest) (cert []byte, err error) {
104 // Get the current HTTP client and list of URLs in a small critical
105 // section. We will use these to make certificate signing requests.
109 intermediates := eca.rootCA.Intermediates
113 return nil, ErrNoExternalCAURLs
116 csrJSON, err := json.Marshal(req)
118 return nil, errors.Wrap(err, "unable to JSON-encode CFSSL signing request")
121 // Try each configured proxy URL. Return after the first success. If
122 // all fail then the last error will be returned.
123 for _, url := range urls {
124 requestCtx, cancel := context.WithTimeout(ctx, eca.ExternalRequestTimeout)
125 cert, err = makeExternalSignRequest(requestCtx, client, url, csrJSON)
128 return append(cert, intermediates...), err
130 log.G(ctx).Debugf("unable to proxy certificate signing request to %s: %s", url, err)
136 // CrossSignRootCA takes a RootCA object, generates a CA CSR, sends a signing request with the CA CSR to the external
137 // CFSSL API server in order to obtain a cross-signed root
138 func (eca *ExternalCA) CrossSignRootCA(ctx context.Context, rca RootCA) ([]byte, error) {
139 // ExtractCertificateRequest generates a new key request, and we want to continue to use the old
140 // key. However, ExtractCertificateRequest will also convert the pkix.Name to csr.Name, which we
141 // need in order to generate a signing request
142 rcaSigner, err := rca.Signer()
146 rootCert := rcaSigner.parsedCert
147 cfCSRObj := csr.ExtractCertificateRequest(rootCert)
149 der, err := x509.CreateCertificateRequest(cryptorand.Reader, &x509.CertificateRequest{
150 RawSubjectPublicKeyInfo: rootCert.RawSubjectPublicKeyInfo,
151 RawSubject: rootCert.RawSubject,
152 PublicKeyAlgorithm: rootCert.PublicKeyAlgorithm,
153 Subject: rootCert.Subject,
154 Extensions: rootCert.Extensions,
155 DNSNames: rootCert.DNSNames,
156 EmailAddresses: rootCert.EmailAddresses,
157 IPAddresses: rootCert.IPAddresses,
158 }, rcaSigner.cryptoSigner)
162 req := signer.SignRequest{
163 Request: string(pem.EncodeToMemory(&pem.Block{
164 Type: "CERTIFICATE REQUEST",
167 Subject: &signer.Subject{
168 CN: rootCert.Subject.CommonName,
169 Names: cfCSRObj.Names,
171 Profile: ExternalCrossSignProfile,
173 // cfssl actually ignores non subject alt name extensions in the CSR, so we have to add the CA extension in the signing
175 for _, ext := range rootCert.Extensions {
176 if ext.Id.Equal(BasicConstraintsOID) {
177 req.Extensions = append(req.Extensions, signer.Extension{
178 ID: config.OID(ext.Id),
179 Critical: ext.Critical,
180 Value: hex.EncodeToString(ext.Value),
184 return eca.Sign(ctx, req)
187 func makeExternalSignRequest(ctx context.Context, client *http.Client, url string, csrJSON []byte) (cert []byte, err error) {
188 resp, err := ctxhttp.Post(ctx, client, url, "application/json", bytes.NewReader(csrJSON))
190 return nil, recoverableErr{err: errors.Wrap(err, "unable to perform certificate signing request")}
192 defer resp.Body.Close()
194 body, err := ioutil.ReadAll(resp.Body)
196 return nil, recoverableErr{err: errors.Wrap(err, "unable to read CSR response body")}
199 if resp.StatusCode != http.StatusOK {
200 return nil, recoverableErr{err: errors.Errorf("unexpected status code in CSR response: %d - %s", resp.StatusCode, string(body))}
203 var apiResponse api.Response
204 if err := json.Unmarshal(body, &apiResponse); err != nil {
205 logrus.Debugf("unable to JSON-parse CFSSL API response body: %s", string(body))
206 return nil, recoverableErr{err: errors.Wrap(err, "unable to parse JSON response")}
209 if !apiResponse.Success || apiResponse.Result == nil {
210 if len(apiResponse.Errors) > 0 {
211 return nil, errors.Errorf("response errors: %v", apiResponse.Errors)
214 return nil, errors.New("certificate signing request failed")
217 result, ok := apiResponse.Result.(map[string]interface{})
219 return nil, errors.Errorf("invalid result type: %T", apiResponse.Result)
222 certPEM, ok := result["certificate"].(string)
224 return nil, errors.Errorf("invalid result certificate field type: %T", result["certificate"])
227 return []byte(certPEM), nil