package sshgen import ( "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/json" "encoding/pem" "fmt" "io" "io/ioutil" "net/http" "net/url" "time" "github.com/coreos/go-oidc/jose" homedir "github.com/mitchellh/go-homedir" "github.com/pkg/errors" gossh "golang.org/x/crypto/ssh" "github.com/cloudflare/cloudflared/config" cfpath "github.com/cloudflare/cloudflared/token" ) const ( signEndpoint = "/cdn-cgi/access/cert_sign" keyName = "cf_key" ) // signPayload represents the request body sent to the sign handler API type signPayload struct { PublicKey string `json:"public_key"` JWT string `json:"jwt"` Issuer string `json:"issuer"` } // signResponse represents the response body from the sign handler API type signResponse struct { KeyID string `json:"id"` Certificate string `json:"certificate"` ExpiresAt time.Time `json:"expires_at"` } // ErrorResponse struct stores error information after any error-prone function type errorResponse struct { Status int `json:"status"` Message string `json:"message"` } var mockRequest func(url, contentType string, body io.Reader) (*http.Response, error) = nil // GenerateShortLivedCertificate generates and stores a keypair for short lived certs func GenerateShortLivedCertificate(appURL *url.URL, token string) error { fullName, err := cfpath.GenerateSSHCertFilePathFromURL(appURL, keyName) if err != nil { return err } cert, err := handleCertificateGeneration(token, fullName) if err != nil { return err } name := fullName + "-cert.pub" if err := writeKey(name, []byte(cert)); err != nil { return err } return nil } // handleCertificateGeneration takes a JWT and uses it build a signPayload // to send to the Sign endpoint with the public key from the keypair it generated func handleCertificateGeneration(token, fullName string) (string, error) { pub, err := generateKeyPair(fullName) if err != nil { return "", err } return SignCert(token, string(pub)) } func SignCert(token, pubKey string) (string, error) { if token == "" { return "", errors.New("invalid token") } jwt, err := jose.ParseJWT(token) if err != nil { return "", errors.Wrap(err, "failed to parse JWT") } claims, err := jwt.Claims() if err != nil { return "", errors.Wrap(err, "failed to retrieve JWT claims") } issuer, _, err := claims.StringClaim("iss") if err != nil { return "", errors.Wrap(err, "failed to retrieve JWT iss") } buf, err := json.Marshal(&signPayload{ PublicKey: pubKey, JWT: token, Issuer: issuer, }) if err != nil { return "", errors.Wrap(err, "failed to marshal signPayload") } var res *http.Response if mockRequest != nil { res, err = mockRequest(issuer+signEndpoint, "application/json", bytes.NewBuffer(buf)) } else { client := http.Client{ Timeout: 10 * time.Second, } res, err = client.Post(issuer+signEndpoint, "application/json", bytes.NewBuffer(buf)) } if err != nil { return "", errors.Wrap(err, "failed to send request") } defer res.Body.Close() decoder := json.NewDecoder(res.Body) if res.StatusCode != 200 { var errResponse errorResponse if err := decoder.Decode(&errResponse); err != nil { return "", err } return "", fmt.Errorf("%d: %s", errResponse.Status, errResponse.Message) } var signRes signResponse if err := decoder.Decode(&signRes); err != nil { return "", errors.Wrap(err, "failed to decode HTTP response") } return signRes.Certificate, nil } // generateKeyPair creates a EC keypair (P256) and stores them in the homedir. // returns the generated public key from the successful keypair generation func generateKeyPair(fullName string) ([]byte, error) { pubKeyName := fullName + ".pub" exist, err := config.FileExists(pubKeyName) if err != nil { return nil, err } if exist { return ioutil.ReadFile(pubKeyName) } key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, err } parsed, err := x509.MarshalECPrivateKey(key) if err != nil { return nil, err } if err := writeKey(fullName, pem.EncodeToMemory(&pem.Block{ Type: "EC PRIVATE KEY", Bytes: parsed, })); err != nil { return nil, err } pub, err := gossh.NewPublicKey(&key.PublicKey) if err != nil { return nil, err } data := gossh.MarshalAuthorizedKey(pub) if err := writeKey(pubKeyName, data); err != nil { return nil, err } return data, nil } // writeKey will write a key to disk in DER format (it's a standard pem key) func writeKey(filename string, data []byte) error { filepath, err := homedir.Expand(filename) if err != nil { return err } return ioutil.WriteFile(filepath, data, 0600) }