Skip to content

Commit

Permalink
adapt to some changes in issueCertificate mutation
Browse files Browse the repository at this point in the history
- SSH keys are now generated client side
- The `[email]` positional argument to `ssh issue` is deprecated (in favor of `-username`)
  • Loading branch information
btoews committed Jan 13, 2023
1 parent 352bd3e commit 7283483
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 91 deletions.
34 changes: 27 additions & 7 deletions api/resource_ssh.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package api

import "context"
import (
"context"
"crypto/ed25519"
"strings"

"golang.org/x/crypto/ssh"
)

func (c *Client) GetLoggedCertificates(ctx context.Context, slug string) ([]LoggedCertificate, error) {
req := c.NewRequest(`
Expand Down Expand Up @@ -46,21 +52,35 @@ mutation($input: EstablishSSHKeyInput!) {
return &data.EstablishSSHKey, nil
}

func (c *Client) IssueSSHCertificate(ctx context.Context, org OrganizationImpl, email string, username *string, valid_hours *int) (*IssuedCertificate, error) {
func (c *Client) IssueSSHCertificate(ctx context.Context, org OrganizationImpl, principals []string, apps []App, valid_hours *int, publicKey ed25519.PublicKey) (*IssuedCertificate, error) {
req := c.NewRequest(`
mutation($input: IssueCertificateInput!) {
issueCertificate(input: $input) {
certificate, key
}
}
`)
inputs := map[string]interface{}{
"organizationId": org.GetID(),
"email": email,

appNames := make([]string, 0, len(apps))
for _, app := range apps {
appNames = append(appNames, app.Name)
}

if username != nil {
inputs["username"] = *username
var pubStr string
if len(publicKey) > 0 {
sshPub, err := ssh.NewPublicKey(publicKey)
if err != nil {
return nil, err
}

pubStr = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPub)))
}

inputs := map[string]interface{}{
"organizationId": org.GetID(),
"principals": principals,
"appNames": appNames,
"publicKey": pubStr,
}

if valid_hours != nil {
Expand Down
7 changes: 1 addition & 6 deletions internal/command/ssh/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,16 +214,11 @@ func runConsole(ctx context.Context) error {
func sshConnect(p *SSHParams, addr string) (*ssh.Client, error) {
terminal.Debugf("Fetching certificate for %s\n", addr)

cert, err := singleUseSSHCertificate(p.Ctx, p.Org)
cert, pk, err := singleUseSSHCertificate(p.Ctx, p.Org)
if err != nil {
return nil, fmt.Errorf("create ssh certificate: %w (if you haven't created a key for your org yet, try `flyctl ssh establish`)", err)
}

pk, err := parsePrivateKey(cert.Key)
if err != nil {
return nil, errors.Wrap(err, "parse ssh certificate")
}

pemkey := marshalED25519PrivateKey(pk, "single-use certificate")

terminal.Debugf("Keys for %s configured; connecting...\n", addr)
Expand Down
120 changes: 60 additions & 60 deletions internal/command/ssh/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"context"
"crypto/ed25519"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net"
"net/mail"
Expand All @@ -33,7 +33,7 @@ func newIssue() *cobra.Command {
into SSH agent. With -hour, set the number of hours (1-72) for credential
validity.`
short = `Issue a new SSH credential`
usage = "issue [org] [email] [path]"
usage = "issue [org] [path]"
)

cmd := command.New(usage, short, long, runSSHIssue, command.RequireSession)
Expand All @@ -42,10 +42,11 @@ validity.`

flag.Add(cmd,
flag.Org(),
flag.String{
flag.StringSlice{
Name: "username",
Shorthand: "u",
Description: "Unix username for SSH cert",
Description: "Unix usernames the SSH cert can authenticate as",
Default: []string{"root", "fly"},
},
flag.Int{
Name: "hours",
Expand Down Expand Up @@ -84,56 +85,77 @@ func runSSHIssue(ctx context.Context) (err error) {
return err
}

// The API used to take an optional `principals` argument, then fall back
// to `username`, then fall back to the name section of `email`. The
// `username` and `email` arguments are now deprecated in favor of
// `principals`. We add the fallback logic here for when the API arguments
// are removed. For a more consistent ux, we call `principals` `usernames`
// here.
principals := flag.GetStringSlice(ctx, "username")

var (
emails string
email *mail.Address
emails string
rootname string
)

for email == nil {
prompt := "Email address for user to issue cert: "
emails, err = argOrPromptLoop(ctx, 1, prompt, emails)
if err != nil {
return err
switch args := flag.Args(ctx); len(args) {
case 0:
// neither
case 1:
// org only
case 2:
// org+email or org+path
if _, err = mail.ParseAddress(args[1]); err == nil {
emails = args[1]
} else {
rootname = args[1]
}
case 3:
// org+email+path
emails = args[1]
rootname = args[2]
default:
return errors.New("Too many positional arguments\n")
}

email, err = mail.ParseAddress(emails)
if len(emails) > 0 {
email, err := mail.ParseAddress(emails)
if err != nil {
fmt.Fprintf(out, "Invalid email address: %s (keep it simple!)\n", err)
email = nil
return fmt.Errorf("Invalid email address: %s\n", err)
}
}

var username *string
name, _, hasAt := strings.Cut(email.Address, "@")
if !hasAt {
return fmt.Errorf("Invalid email address: %s\n", emails)
}

if vals := flag.GetString(ctx, "username"); vals != "" {
username = &vals
principals = append(principals, name)
}

hours := flag.GetInt(ctx, "hours")
if hours < 1 || hours > 72 {
return fmt.Errorf("Invalid expiration time (1-72 hours)\n")
}

icert, err := client.IssueSSHCertificate(ctx, org, email.Address, username, &hours)
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
return err
}

icert, err := client.IssueSSHCertificate(ctx, org, principals, nil, &hours, pub)
if err != nil {
return err
}

doAgent := flag.GetBool(ctx, "agent")
if doAgent {
if err = populateAgent(icert); err != nil {
if err = populateAgent(icert, priv); err != nil {
return err
}

fmt.Printf("Populated agent with cert:\n%s\n", icert.Certificate)
return nil
}

pk, err := parsePrivateKey(icert.Key)
if err != nil {
return err
}

fmt.Printf(`
!!!! WARNING: We're now prompting you to save an SSH private key and certificate !!!!
!!!! (the private key in "id_whatever" and the certificate in "id_whatever-cert.pub"). !!!!
Expand All @@ -143,16 +165,16 @@ func runSSHIssue(ctx context.Context) (err error) {
`)

var (
rootname string
pf *os.File
cf *os.File
pf *os.File
cf *os.File
)

for pf == nil && cf == nil {
prompt := "Path to store private key: "
rootname, err = argOrPromptLoop(ctx, 2, prompt, rootname)
if err != nil {
return err
if rootname == "" {
prompt := "Path to store private key: "
if err := survey.AskOne(&survey.Input{Message: prompt}, &rootname); err != nil {
return err
}
}

if flag.GetBool(ctx, "dotssh") {
Expand All @@ -163,7 +185,7 @@ func runSSHIssue(ctx context.Context) (err error) {
if !flag.GetBool(ctx, "overwrite") {
mode |= os.O_EXCL
} else if _, err = os.Stat(rootname); err == nil {
if buf, err := ioutil.ReadFile(rootname); err != nil {
if buf, err := os.ReadFile(rootname); err != nil {
fmt.Fprintf(out, "File exists, but we can't read it to make sure it's safe to overwrite: %s\n", err)
continue
} else if !strings.Contains(string(buf), "fly.io" /* BUG(tqbf): do better */) {
Expand All @@ -188,7 +210,7 @@ func runSSHIssue(ctx context.Context) (err error) {
io.WriteString(cf, icert.Certificate)
cf.Close()

buf := MarshalED25519PrivateKey(pk, "fly.io")
buf := MarshalED25519PrivateKey(priv, "fly.io")
pf.Write(buf)
pf.Close()

Expand All @@ -197,23 +219,6 @@ func runSSHIssue(ctx context.Context) (err error) {
return nil
}

func argOrPromptImpl(ctx context.Context, nth int, prompt string, first bool) (string, error) {
if len(flag.Args(ctx)) >= (nth + 1) {
return flag.Args(ctx)[nth], nil
}

val := ""
err := survey.AskOne(&survey.Input{
Message: prompt,
}, &val)

return val, err
}

func argOrPromptLoop(ctx context.Context, nth int, prompt, last string) (string, error) {
return argOrPromptImpl(ctx, nth, prompt, last == "")
}

// stolen from `mikesmitty`, thanks, you are a mikesmitty and a scholar
func MarshalED25519PrivateKey(key ed25519.PrivateKey, comment string) []byte {
magic := append([]byte("openssh-key-v1"), 0)
Expand All @@ -237,7 +242,7 @@ func MarshalED25519PrivateKey(key ed25519.PrivateKey, comment string) []byte {
Pad []byte `ssh:"rest"`
}{}

ci := rand.Uint32()
ci := rand.Uint32() // skipcq: GSC-G404
pk1.Check1 = ci
pk1.Check2 = ci

Expand Down Expand Up @@ -281,7 +286,7 @@ func MarshalED25519PrivateKey(key ed25519.PrivateKey, comment string) []byte {
})
}

func populateAgent(icert *api.IssuedCertificate) error {
func populateAgent(icert *api.IssuedCertificate, priv ed25519.PrivateKey) error {
acon, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
if err != nil {
return fmt.Errorf("can't connect to SSH agent: %w", err)
Expand All @@ -294,13 +299,8 @@ func populateAgent(icert *api.IssuedCertificate) error {
return fmt.Errorf("API error: can't parse API-provided SSH certificate: %w", err)
}

pkey, err := parsePrivateKey(icert.Key)
if err != nil {
return err
}

if err = ssha.Add(agent.AddedKey{
PrivateKey: pkey,
PrivateKey: priv,
Certificate: cert.(*ssh.Certificate),
}); err != nil {
return fmt.Errorf("ssh-agent failure: %w", err)
Expand Down
27 changes: 9 additions & 18 deletions internal/command/ssh/ssh_terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"bytes"
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
Expand Down Expand Up @@ -96,16 +95,11 @@ func RunSSHCommand(ctx context.Context, app *api.AppCompact, dialer agent.Dialer
func SSHConnect(p *SSHParams, addr string) error {
terminal.Debugf("Fetching certificate for %s\n", addr)

cert, err := singleUseSSHCertificate(p.Ctx, p.Org)
cert, pk, err := singleUseSSHCertificate(p.Ctx, p.Org)
if err != nil {
return fmt.Errorf("create ssh certificate: %w (if you haven't created a key for your org yet, try `flyctl ssh establish`)", err)
}

pk, err := parsePrivateKey(cert.Key)
if err != nil {
return errors.Wrap(err, "parse ssh certificate")
}

pemkey := marshalED25519PrivateKey(pk, "single-use certificate")

terminal.Debugf("Keys for %s configured; connecting...\n", addr)
Expand Down Expand Up @@ -219,22 +213,19 @@ func marshalED25519PrivateKey(key ed25519.PrivateKey, comment string) []byte {
})
}

func singleUseSSHCertificate(ctx context.Context, org api.OrganizationImpl) (*api.IssuedCertificate, error) {
func singleUseSSHCertificate(ctx context.Context, org api.OrganizationImpl) (*api.IssuedCertificate, ed25519.PrivateKey, error) {
client := client.FromContext(ctx).API()
hours := 1

user, err := client.GetCurrentUser(ctx)
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
return nil, err
return nil, nil, err
}

hours := 1
return client.IssueSSHCertificate(ctx, org, user.Email, nil, &hours)
}

func parsePrivateKey(key64 string) (ed25519.PrivateKey, error) {
pkeys, err := base64.StdEncoding.DecodeString(key64)
icert, err := client.IssueSSHCertificate(ctx, org, []string{"root", "fly"}, nil, &hours, pub)
if err != nil {
return nil, fmt.Errorf("API error: can't parse API-provided private key: %w", err)
return nil, nil, err
}
return ed25519.NewKeyFromSeed(pkeys), nil

return icert, priv, nil
}

0 comments on commit 7283483

Please sign in to comment.