github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/swarmkit/ca/external.go (about) 1 package ca 2 3 import ( 4 "bytes" 5 "context" 6 cryptorand "crypto/rand" 7 "crypto/tls" 8 "crypto/x509" 9 "encoding/hex" 10 "encoding/json" 11 "encoding/pem" 12 "io" 13 "io/ioutil" 14 "net/http" 15 "sync" 16 "time" 17 18 "github.com/cloudflare/cfssl/api" 19 "github.com/cloudflare/cfssl/config" 20 "github.com/cloudflare/cfssl/csr" 21 "github.com/cloudflare/cfssl/signer" 22 "github.com/docker/swarmkit/log" 23 "github.com/pkg/errors" 24 "github.com/sirupsen/logrus" 25 "golang.org/x/net/context/ctxhttp" 26 ) 27 28 const ( 29 // ExternalCrossSignProfile is the profile that we will be sending cross-signing CSR sign requests with 30 ExternalCrossSignProfile = "CA" 31 32 // CertificateMaxSize is the maximum expected size of a certificate. 33 // While there is no specced upper limit to the size of an x509 certificate in PEM format, 34 // one with a ridiculous RSA key size (16384) and 26 256-character DNS SAN fields is about 14k. 35 // While there is no upper limit on the length of certificate chains, long chains are impractical. 36 // To be conservative, and to also account for external CA certificate responses in JSON format 37 // from CFSSL, we'll set the max to be 256KiB. 38 CertificateMaxSize int64 = 256 << 10 39 ) 40 41 // ErrNoExternalCAURLs is an error used it indicate that an ExternalCA is 42 // configured with no URLs to which it can proxy certificate signing requests. 43 var ErrNoExternalCAURLs = errors.New("no external CA URLs") 44 45 // ExternalCA is able to make certificate signing requests to one of a list 46 // remote CFSSL API endpoints. 47 type ExternalCA struct { 48 ExternalRequestTimeout time.Duration 49 50 mu sync.Mutex 51 intermediates []byte 52 urls []string 53 client *http.Client 54 } 55 56 // NewExternalCATLSConfig takes a TLS certificate and root pool and returns a TLS config that can be updated 57 // without killing existing connections 58 func NewExternalCATLSConfig(certs []tls.Certificate, rootPool *x509.CertPool) *tls.Config { 59 return &tls.Config{ 60 Certificates: certs, 61 RootCAs: rootPool, 62 MinVersion: tls.VersionTLS12, 63 } 64 } 65 66 // NewExternalCA creates a new ExternalCA which uses the given tlsConfig to 67 // authenticate to any of the given URLS of CFSSL API endpoints. 68 func NewExternalCA(intermediates []byte, tlsConfig *tls.Config, urls ...string) *ExternalCA { 69 return &ExternalCA{ 70 ExternalRequestTimeout: 5 * time.Second, 71 intermediates: intermediates, 72 urls: urls, 73 client: &http.Client{ 74 Transport: &http.Transport{ 75 TLSClientConfig: tlsConfig, 76 }, 77 }, 78 } 79 } 80 81 // UpdateTLSConfig updates the HTTP Client for this ExternalCA by creating 82 // a new client which uses the given tlsConfig. 83 func (eca *ExternalCA) UpdateTLSConfig(tlsConfig *tls.Config) { 84 eca.mu.Lock() 85 defer eca.mu.Unlock() 86 87 eca.client = &http.Client{ 88 Transport: &http.Transport{ 89 TLSClientConfig: tlsConfig, 90 }, 91 } 92 } 93 94 // UpdateURLs updates the list of CSR API endpoints by setting it to the given urls. 95 func (eca *ExternalCA) UpdateURLs(urls ...string) { 96 eca.mu.Lock() 97 defer eca.mu.Unlock() 98 99 eca.urls = urls 100 } 101 102 // Sign signs a new certificate by proxying the given certificate signing 103 // request to an external CFSSL API server. 104 func (eca *ExternalCA) Sign(ctx context.Context, req signer.SignRequest) (cert []byte, err error) { 105 // Get the current HTTP client and list of URLs in a small critical 106 // section. We will use these to make certificate signing requests. 107 eca.mu.Lock() 108 urls := eca.urls 109 client := eca.client 110 intermediates := eca.intermediates 111 eca.mu.Unlock() 112 113 if len(urls) == 0 { 114 return nil, ErrNoExternalCAURLs 115 } 116 117 csrJSON, err := json.Marshal(req) 118 if err != nil { 119 return nil, errors.Wrap(err, "unable to JSON-encode CFSSL signing request") 120 } 121 122 // Try each configured proxy URL. Return after the first success. If 123 // all fail then the last error will be returned. 124 for _, url := range urls { 125 requestCtx, cancel := context.WithTimeout(ctx, eca.ExternalRequestTimeout) 126 cert, err = makeExternalSignRequest(requestCtx, client, url, csrJSON) 127 cancel() 128 if err == nil { 129 return append(cert, intermediates...), err 130 } 131 log.G(ctx).Debugf("unable to proxy certificate signing request to %s: %s", url, err) 132 } 133 134 return nil, err 135 } 136 137 // CrossSignRootCA takes a RootCA object, generates a CA CSR, sends a signing request with the CA CSR to the external 138 // CFSSL API server in order to obtain a cross-signed root 139 func (eca *ExternalCA) CrossSignRootCA(ctx context.Context, rca RootCA) ([]byte, error) { 140 // ExtractCertificateRequest generates a new key request, and we want to continue to use the old 141 // key. However, ExtractCertificateRequest will also convert the pkix.Name to csr.Name, which we 142 // need in order to generate a signing request 143 rcaSigner, err := rca.Signer() 144 if err != nil { 145 return nil, err 146 } 147 rootCert := rcaSigner.parsedCert 148 cfCSRObj := csr.ExtractCertificateRequest(rootCert) 149 150 der, err := x509.CreateCertificateRequest(cryptorand.Reader, &x509.CertificateRequest{ 151 RawSubjectPublicKeyInfo: rootCert.RawSubjectPublicKeyInfo, 152 RawSubject: rootCert.RawSubject, 153 PublicKeyAlgorithm: rootCert.PublicKeyAlgorithm, 154 Subject: rootCert.Subject, 155 Extensions: rootCert.Extensions, 156 DNSNames: rootCert.DNSNames, 157 EmailAddresses: rootCert.EmailAddresses, 158 IPAddresses: rootCert.IPAddresses, 159 }, rcaSigner.cryptoSigner) 160 if err != nil { 161 return nil, err 162 } 163 req := signer.SignRequest{ 164 Request: string(pem.EncodeToMemory(&pem.Block{ 165 Type: "CERTIFICATE REQUEST", 166 Bytes: der, 167 })), 168 Subject: &signer.Subject{ 169 CN: rootCert.Subject.CommonName, 170 Names: cfCSRObj.Names, 171 }, 172 Profile: ExternalCrossSignProfile, 173 } 174 // cfssl actually ignores non subject alt name extensions in the CSR, so we have to add the CA extension in the signing 175 // request as well 176 for _, ext := range rootCert.Extensions { 177 if ext.Id.Equal(BasicConstraintsOID) { 178 req.Extensions = append(req.Extensions, signer.Extension{ 179 ID: config.OID(ext.Id), 180 Critical: ext.Critical, 181 Value: hex.EncodeToString(ext.Value), 182 }) 183 } 184 } 185 return eca.Sign(ctx, req) 186 } 187 188 func makeExternalSignRequest(ctx context.Context, client *http.Client, url string, csrJSON []byte) (cert []byte, err error) { 189 resp, err := ctxhttp.Post(ctx, client, url, "application/json", bytes.NewReader(csrJSON)) 190 if err != nil { 191 return nil, recoverableErr{err: errors.Wrap(err, "unable to perform certificate signing request")} 192 } 193 defer resp.Body.Close() 194 195 b := io.LimitReader(resp.Body, CertificateMaxSize) 196 body, err := ioutil.ReadAll(b) 197 if err != nil { 198 return nil, recoverableErr{err: errors.Wrap(err, "unable to read CSR response body")} 199 } 200 201 if resp.StatusCode != http.StatusOK { 202 return nil, recoverableErr{err: errors.Errorf("unexpected status code in CSR response: %d - %s", resp.StatusCode, string(body))} 203 } 204 205 var apiResponse api.Response 206 if err := json.Unmarshal(body, &apiResponse); err != nil { 207 logrus.Debugf("unable to JSON-parse CFSSL API response body: %s", string(body)) 208 return nil, recoverableErr{err: errors.Wrap(err, "unable to parse JSON response")} 209 } 210 211 if !apiResponse.Success || apiResponse.Result == nil { 212 if len(apiResponse.Errors) > 0 { 213 return nil, errors.Errorf("response errors: %v", apiResponse.Errors) 214 } 215 216 return nil, errors.New("certificate signing request failed") 217 } 218 219 result, ok := apiResponse.Result.(map[string]interface{}) 220 if !ok { 221 return nil, errors.Errorf("invalid result type: %T", apiResponse.Result) 222 } 223 224 certPEM, ok := result["certificate"].(string) 225 if !ok { 226 return nil, errors.Errorf("invalid result certificate field type: %T", result["certificate"]) 227 } 228 229 return []byte(certPEM), nil 230 }