sshgen.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. package sshgen
  2. import (
  3. "bytes"
  4. "crypto/ecdsa"
  5. "crypto/elliptic"
  6. "crypto/rand"
  7. "crypto/x509"
  8. "encoding/json"
  9. "encoding/pem"
  10. "fmt"
  11. "io"
  12. "io/ioutil"
  13. "net/http"
  14. "net/url"
  15. "time"
  16. "github.com/cloudflare/cloudflared/cmd/cloudflared/config"
  17. cfpath "github.com/cloudflare/cloudflared/cmd/cloudflared/path"
  18. "github.com/coreos/go-oidc/jose"
  19. homedir "github.com/mitchellh/go-homedir"
  20. "github.com/pkg/errors"
  21. gossh "golang.org/x/crypto/ssh"
  22. )
  23. const (
  24. signEndpoint = "/cdn-cgi/access/cert_sign"
  25. keyName = "cf_key"
  26. )
  27. // signPayload represents the request body sent to the sign handler API
  28. type signPayload struct {
  29. PublicKey string `json:"public_key"`
  30. JWT string `json:"jwt"`
  31. Issuer string `json:"issuer"`
  32. }
  33. // signResponse represents the response body from the sign handler API
  34. type signResponse struct {
  35. KeyID string `json:"id"`
  36. Certificate string `json:"certificate"`
  37. ExpiresAt time.Time `json:"expires_at"`
  38. }
  39. // ErrorResponse struct stores error information after any error-prone function
  40. type errorResponse struct {
  41. Status int `json:"status"`
  42. Message string `json:"message"`
  43. }
  44. var mockRequest func(url, contentType string, body io.Reader) (*http.Response, error) = nil
  45. // GenerateShortLivedCertificate generates and stores a keypair for short lived certs
  46. func GenerateShortLivedCertificate(appURL *url.URL, token string) error {
  47. fullName, err := cfpath.GenerateFilePathFromURL(appURL, keyName)
  48. if err != nil {
  49. return err
  50. }
  51. cert, err := handleCertificateGeneration(token, fullName)
  52. if err != nil {
  53. return err
  54. }
  55. name := fullName + "-cert.pub"
  56. if err := writeKey(name, []byte(cert)); err != nil {
  57. return err
  58. }
  59. return nil
  60. }
  61. // handleCertificateGeneration takes a JWT and uses it build a signPayload
  62. // to send to the Sign endpoint with the public key from the keypair it generated
  63. func handleCertificateGeneration(token, fullName string) (string, error) {
  64. pub, err := generateKeyPair(fullName)
  65. if err != nil {
  66. return "", err
  67. }
  68. return SignCert(token, string(pub))
  69. }
  70. func SignCert(token, pubKey string) (string, error) {
  71. if token == "" {
  72. return "", errors.New("invalid token")
  73. }
  74. jwt, err := jose.ParseJWT(token)
  75. if err != nil {
  76. return "", errors.Wrap(err, "failed to parse JWT")
  77. }
  78. claims, err := jwt.Claims()
  79. if err != nil {
  80. return "", errors.Wrap(err, "failed to retrieve JWT claims")
  81. }
  82. issuer, _, err := claims.StringClaim("iss")
  83. if err != nil {
  84. return "", errors.Wrap(err, "failed to retrieve JWT iss")
  85. }
  86. buf, err := json.Marshal(&signPayload{
  87. PublicKey: pubKey,
  88. JWT: token,
  89. Issuer: issuer,
  90. })
  91. if err != nil {
  92. return "", errors.Wrap(err, "failed to marshal signPayload")
  93. }
  94. var res *http.Response
  95. if mockRequest != nil {
  96. res, err = mockRequest(issuer+signEndpoint, "application/json", bytes.NewBuffer(buf))
  97. } else {
  98. client := http.Client{
  99. Timeout: 10 * time.Second,
  100. }
  101. res, err = client.Post(issuer+signEndpoint, "application/json", bytes.NewBuffer(buf))
  102. }
  103. if err != nil {
  104. return "", errors.Wrap(err, "failed to send request")
  105. }
  106. defer res.Body.Close()
  107. decoder := json.NewDecoder(res.Body)
  108. if res.StatusCode != 200 {
  109. var errResponse errorResponse
  110. if err := decoder.Decode(&errResponse); err != nil {
  111. return "", err
  112. }
  113. return "", fmt.Errorf("%d: %s", errResponse.Status, errResponse.Message)
  114. }
  115. var signRes signResponse
  116. if err := decoder.Decode(&signRes); err != nil {
  117. return "", errors.Wrap(err, "failed to decode HTTP response")
  118. }
  119. return signRes.Certificate, nil
  120. }
  121. // generateKeyPair creates a EC keypair (P256) and stores them in the homedir.
  122. // returns the generated public key from the successful keypair generation
  123. func generateKeyPair(fullName string) ([]byte, error) {
  124. pubKeyName := fullName + ".pub"
  125. exist, err := config.FileExists(pubKeyName)
  126. if err != nil {
  127. return nil, err
  128. }
  129. if exist {
  130. return ioutil.ReadFile(pubKeyName)
  131. }
  132. key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  133. if err != nil {
  134. return nil, err
  135. }
  136. parsed, err := x509.MarshalECPrivateKey(key)
  137. if err != nil {
  138. return nil, err
  139. }
  140. if err := writeKey(fullName, pem.EncodeToMemory(&pem.Block{
  141. Type: "EC PRIVATE KEY",
  142. Bytes: parsed,
  143. })); err != nil {
  144. return nil, err
  145. }
  146. pub, err := gossh.NewPublicKey(&key.PublicKey)
  147. if err != nil {
  148. return nil, err
  149. }
  150. data := gossh.MarshalAuthorizedKey(pub)
  151. if err := writeKey(pubKeyName, data); err != nil {
  152. return nil, err
  153. }
  154. return data, nil
  155. }
  156. // writeKey will write a key to disk in DER format (it's a standard pem key)
  157. func writeKey(filename string, data []byte) error {
  158. filepath, err := homedir.Expand(filename)
  159. if err != nil {
  160. return err
  161. }
  162. return ioutil.WriteFile(filepath, data, 0600)
  163. }