Improve host key scanner, add Ed25519 generator

pull/32/head
Hidde Beydals 5 years ago
parent 3a8151bcc0
commit 6017946144

@ -2,10 +2,12 @@ package main
import ( import (
"context" "context"
"crypto/elliptic"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"os" "os"
"time"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
@ -47,6 +49,14 @@ For private Git repositories, the basic authentication credentials are stored in
--url=ssh://git@github.com/stefanprodan/podinfo \ --url=ssh://git@github.com/stefanprodan/podinfo \
--branch=master --branch=master
# Create a source from a Git repository using SSH authentication and an
# ECDSA P-521 curve public key
create source git podinfo \
--url=ssh://git@github.com/stefanprodan/podinfo \
--branch=master \
--ssh-key-algorithm=ecdsa \
--ssh-ecdsa-curve=p521
# Create a source from a Git repository using basic authentication # Create a source from a Git repository using basic authentication
create source git podinfo \ create source git podinfo \
--url=https://github.com/stefanprodan/podinfo \ --url=https://github.com/stefanprodan/podinfo \
@ -63,9 +73,9 @@ var (
sourceGitSemver string sourceGitSemver string
sourceGitUsername string sourceGitUsername string
sourceGitPassword string sourceGitPassword string
sourceGitKeyAlgorithm PublicKeyAlgorithm sourceGitKeyAlgorithm PublicKeyAlgorithm = "rsa"
sourceGitRSABits RSAKeyBits sourceGitRSABits RSAKeyBits = 2048
sourceGitECDSACurve ECDSACurve sourceGitECDSACurve = ECDSACurve{elliptic.P384()}
) )
func init() { func init() {
@ -75,9 +85,9 @@ func init() {
createSourceGitCmd.Flags().StringVar(&sourceGitSemver, "tag-semver", "", "git tag semver range") createSourceGitCmd.Flags().StringVar(&sourceGitSemver, "tag-semver", "", "git tag semver range")
createSourceGitCmd.Flags().StringVarP(&sourceGitUsername, "username", "u", "", "basic authentication username") createSourceGitCmd.Flags().StringVarP(&sourceGitUsername, "username", "u", "", "basic authentication username")
createSourceGitCmd.Flags().StringVarP(&sourceGitPassword, "password", "p", "", "basic authentication password") createSourceGitCmd.Flags().StringVarP(&sourceGitPassword, "password", "p", "", "basic authentication password")
createSourceGitCmd.Flags().Var(&sourceGitKeyAlgorithm, "ssh-algorithm", "SSH public key algorithm") createSourceGitCmd.Flags().Var(&sourceGitKeyAlgorithm, "ssh-key-algorithm", sourceGitKeyAlgorithm.Description())
createSourceGitCmd.Flags().Var(&sourceGitRSABits, "ssh-rsa-bits", "SSH RSA public key bit size") createSourceGitCmd.Flags().Var(&sourceGitRSABits, "ssh-rsa-bits", sourceGitRSABits.Description())
createSourceGitCmd.Flags().Var(&sourceGitECDSACurve, "ssh-ecdsa-curve", "SSH ECDSA public key curve") createSourceGitCmd.Flags().Var(&sourceGitECDSACurve, "ssh-ecdsa-curve", sourceGitECDSACurve.Description())
createSourceCmd.AddCommand(createSourceGitCmd) createSourceCmd.AddCommand(createSourceGitCmd)
} }
@ -108,18 +118,11 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
withAuth := false withAuth := false
if u.Scheme == "ssh" { if u.Scheme == "ssh" {
var keyGen ssh.KeyPairGenerator
switch sourceGitKeyAlgorithm.String() {
case "rsa":
keyGen = ssh.NewRSAGenerator(int(sourceGitRSABits))
case "ecdsa":
keyGen = ssh.NewECDSAGenerator(sourceGitECDSACurve.Curve)
}
host := u.Host host := u.Host
if u.Port() == "" { if u.Port() == "" {
host = host + ":22" host = host + ":22"
} }
if err := generateSSH(ctx, keyGen, name, host, tmpDir); err != nil { if err := generateSSH(ctx, name, host); err != nil {
return err return err
} }
withAuth = true withAuth = true
@ -212,13 +215,14 @@ func generateBasicAuth(ctx context.Context, name string) error {
return nil return nil
} }
func generateSSH(ctx context.Context, generator ssh.KeyPairGenerator, name, host, user string) error { func generateSSH(ctx context.Context, name, host string) error {
logGenerate("generating deploy key") gen := getKeyPairGenerator()
kp, err := generator.Generate() logGenerate("generating deploy key pair")
pair, err := gen.Generate()
if err != nil { if err != nil {
return fmt.Errorf("SSH key pair generation failed: %w", err) return fmt.Errorf("key pair generation failed: %w", err)
} }
fmt.Printf("%s", kp.PublicKey) fmt.Printf("%s", pair.PublicKey)
prompt := promptui.Prompt{ prompt := promptui.Prompt{
Label: "Have you added the deploy key to your repository", Label: "Have you added the deploy key to your repository",
@ -228,21 +232,21 @@ func generateSSH(ctx context.Context, generator ssh.KeyPairGenerator, name, host
return fmt.Errorf("aborting") return fmt.Errorf("aborting")
} }
logAction("collecting SSH server public key for generated public key algorithm") logAction("collecting preferred public key from SSH server")
hostKey, err := ssh.ScanHostKey(host, user, kp) hostKey, err := ssh.ScanHostKey(host, 30*time.Second)
if err != nil { if err != nil {
return err return err
} }
logSuccess("collected public key from SSH server") logSuccess("collected public key from SSH server:")
fmt.Printf("%s", hostKey) fmt.Printf("%s", hostKey)
logAction("saving keys") logAction("saving keys")
files := fmt.Sprintf("--from-literal=identity=\"%s\" --from-literal=identity.pub=\"%s\" --from-literal=known_hosts=\"%s\"", files := fmt.Sprintf("--from-literal=identity=\"%s\" --from-literal=identity.pub=\"%s\" --from-literal=known_hosts=\"%s\"",
kp.PublicKey, kp.PrivateKey, hostKey) pair.PrivateKey, pair.PublicKey, hostKey)
secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-", secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-",
namespace, name, files) namespace, name, files)
if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil { if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil {
return fmt.Errorf("create secret failed") return fmt.Errorf("failed to create secret")
} }
return nil return nil
} }
@ -301,3 +305,16 @@ func isGitRepositoryReady(ctx context.Context, kubeClient client.Client, name, n
return false, nil return false, nil
} }
} }
func getKeyPairGenerator() ssh.KeyPairGenerator {
var keyGen ssh.KeyPairGenerator
switch sourceGitKeyAlgorithm.String() {
case "rsa":
keyGen = ssh.NewRSAGenerator(int(sourceGitRSABits))
case "ecdsa":
keyGen = ssh.NewECDSAGenerator(sourceGitECDSACurve.Curve)
case "ed25519":
keyGen = ssh.NewEd25519Generator()
}
return keyGen
}

@ -7,7 +7,7 @@ import (
"strings" "strings"
) )
var supportedPublicKeyAlgorithms = []string{"rsa", "ecdsa"} var supportedPublicKeyAlgorithms = []string{"rsa", "ecdsa", "ed25519"}
type PublicKeyAlgorithm string type PublicKeyAlgorithm string
@ -17,8 +17,8 @@ func (a *PublicKeyAlgorithm) String() string {
func (a *PublicKeyAlgorithm) Set(str string) error { func (a *PublicKeyAlgorithm) Set(str string) error {
if strings.TrimSpace(str) == "" { if strings.TrimSpace(str) == "" {
*a = PublicKeyAlgorithm(supportedPublicKeyAlgorithms[0]) return fmt.Errorf("no public key algorithm given, must be one of: %s",
return nil strings.Join(supportedPublicKeyAlgorithms, ", "))
} }
for _, v := range supportedPublicKeyAlgorithms { for _, v := range supportedPublicKeyAlgorithms {
if str == v { if str == v {
@ -26,17 +26,18 @@ func (a *PublicKeyAlgorithm) Set(str string) error {
return nil return nil
} }
} }
return fmt.Errorf( return fmt.Errorf("unsupported public key algorithm '%s', must be one of: %s",
"unsupported public key algorithm '%s', must be one of: %s", str, strings.Join(supportedPublicKeyAlgorithms, ", "))
str,
strings.Join(supportedPublicKeyAlgorithms, ", "),
)
} }
func (a *PublicKeyAlgorithm) Type() string { func (a *PublicKeyAlgorithm) Type() string {
return "publicKeyAlgorithm" return "publicKeyAlgorithm"
} }
func (a *PublicKeyAlgorithm) Description() string {
return fmt.Sprintf("SSH public key algorithm (%s)", strings.Join(supportedPublicKeyAlgorithms, ", "))
}
var defaultRSAKeyBits = 2048 var defaultRSAKeyBits = 2048
type RSAKeyBits int type RSAKeyBits int
@ -65,37 +66,47 @@ func (b *RSAKeyBits) Type() string {
return "rsaKeyBits" return "rsaKeyBits"
} }
func (b *RSAKeyBits) Description() string {
return "SSH RSA public key bit size (multiplies of 8)"
}
type ECDSACurve struct { type ECDSACurve struct {
elliptic.Curve elliptic.Curve
} }
var supportedECDSACurves = map[string]elliptic.Curve{ var supportedECDSACurves = map[string]elliptic.Curve{
"P-256": elliptic.P256(), "p256": elliptic.P256(),
"P-384": elliptic.P384(), "p384": elliptic.P384(),
"P-521": elliptic.P521(), "p521": elliptic.P521(),
} }
func (c *ECDSACurve) String() string { func (c *ECDSACurve) String() string {
if c == nil || c.Curve == nil { if c.Curve == nil {
return "" return ""
} }
return c.Curve.Params().Name return strings.ToLower(strings.Replace(c.Curve.Params().Name, "-", "", 1))
} }
func (c *ECDSACurve) Set(str string) error { func (c *ECDSACurve) Set(str string) error {
if strings.TrimSpace(str) == "" { if v, ok := supportedECDSACurves[str]; ok {
*c = ECDSACurve{supportedECDSACurves["P-384"]}
return nil
}
for k, v := range supportedECDSACurves {
if k == str {
*c = ECDSACurve{v} *c = ECDSACurve{v}
return nil return nil
} }
} return fmt.Errorf("unsupported curve '%s', should be one of: %s", str, strings.Join(ecdsaCurves(), ", "))
return fmt.Errorf("unsupported curve '%s', should be one of: P-256, P-384, P-521", str)
} }
func (c *ECDSACurve) Type() string { func (c *ECDSACurve) Type() string {
return "ecdsaCurve" return "ecdsaCurve"
} }
func (c *ECDSACurve) Description() string {
return fmt.Sprintf("SSH ECDSA public key curve (%s)", strings.Join(ecdsaCurves(), ", "))
}
func ecdsaCurves() []string {
keys := make([]string, 0, len(supportedECDSACurves))
for k := range supportedECDSACurves {
keys = append(keys, k)
}
return keys
}

@ -4,27 +4,20 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"net" "net"
"time"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts" "golang.org/x/crypto/ssh/knownhosts"
) )
// ScanHostKey collects the given host's preferred public key for the // ScanHostKey collects the given host's preferred public key for the
// algorithm of the given key pair. Any errors (e.g. authentication // Any errors (e.g. authentication failures) are ignored, except if
// failures) are ignored, except if no key could be collected from the // no key could be collected from the host.
// host. func ScanHostKey(host string, timeout time.Duration) ([]byte, error) {
func ScanHostKey(host string, user string, pair *KeyPair) ([]byte, error) {
signer, err := ssh.ParsePrivateKey(pair.PrivateKey)
if err != nil {
return nil, err
}
col := &collector{} col := &collector{}
config := &ssh.ClientConfig{ config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: col.StoreKey(), HostKeyCallback: col.StoreKey(),
Timeout: timeout,
} }
client, err := ssh.Dial("tcp", host, config) client, err := ssh.Dial("tcp", host, config)
if err == nil { if err == nil {
@ -40,6 +33,10 @@ type collector struct {
knownKeys []byte knownKeys []byte
} }
// StoreKey stores the public key in bytes as returned by the host.
// To collect multiple public key types from the host, multiple
// SSH dials need with the ClientConfig HostKeyAlgorithms set to
// the algorithm you want to collect.
func (c *collector) StoreKey() ssh.HostKeyCallback { func (c *collector) StoreKey() ssh.HostKeyCallback {
return func(hostname string, remote net.Addr, key ssh.PublicKey) error { return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
c.knownKeys = append( c.knownKeys = append(

@ -2,6 +2,7 @@ package ssh
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
@ -79,6 +80,31 @@ func (g *ECDSAGenerator) Generate() (*KeyPair, error) {
}, nil }, nil
} }
type Ed25519Generator struct{}
func NewEd25519Generator() KeyPairGenerator {
return &Ed25519Generator{}
}
func (g *Ed25519Generator) Generate() (*KeyPair, error) {
pk, pv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
pub, err := generatePublicKey(pk)
if err != nil {
return nil, err
}
priv, err := encodePrivateKeyToPEM(pv)
if err != nil {
return nil, err
}
return &KeyPair{
PublicKey: pub,
PrivateKey: priv,
}, nil
}
func generatePublicKey(pk interface{}) ([]byte, error) { func generatePublicKey(pk interface{}) ([]byte, error) {
b, err := ssh.NewPublicKey(pk) b, err := ssh.NewPublicKey(pk)
if err != nil { if err != nil {

Loading…
Cancel
Save