195 lines
4.6 KiB
Go
195 lines
4.6 KiB
Go
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)
|
|
}
|