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