123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- 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/cloudflare/cloudflared/cmd/cloudflared/config"
- cfpath "github.com/cloudflare/cloudflared/cmd/cloudflared/path"
- "github.com/coreos/go-oidc/jose"
- homedir "github.com/mitchellh/go-homedir"
- "github.com/pkg/errors"
- gossh "golang.org/x/crypto/ssh"
- )
- 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.GenerateFilePathFromURL(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)
- }
|