From e292c368d592e43c001873a7f67f340807824860 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Thu, 14 Jul 2016 18:50:33 +0600 Subject: [PATCH] add key with passphrase support (fix #2) --- main.go | 92 ++++++++++++++++++- runner_factory.go | 2 +- tests/orgalorg.sh | 24 +++++ tests/setup.sh | 5 +- ...thenticate-via-key-with-passphrase.test.sh | 38 ++++++++ 5 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 tests/testcases/auth/can-authenticate-via-key-with-passphrase.test.sh diff --git a/main.go b/main.go index bf5a33d..44be2ff 100644 --- a/main.go +++ b/main.go @@ -2,8 +2,13 @@ package main import ( "bufio" + "bytes" + "crypto/x509" + "encoding/pem" + "errors" "fmt" "io" + "io/ioutil" "os" "os/user" "path/filepath" @@ -220,7 +225,8 @@ const ( ) var ( - sshPasswordPrompt = "Password: " + sshPasswordPrompt = "Password: " + sshPassphrasePrompt = "Key passphrase: " ) var ( @@ -281,7 +287,9 @@ func main() { bar = barely.NewStatusBar(barStyle.Template) } else { bar = nil + sshPasswordPrompt = "" + sshPassphrasePrompt = "" } switch { @@ -684,6 +692,73 @@ func connectAndLock( return cluster, nil } +func readSSHKey(path string) ([]byte, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, hierr.Errorf( + err, + `can't read SSH key from file`, + ) + } + + decoded, extra := pem.Decode(data) + + if len(extra) != 0 { + return nil, hierr.Errorf( + errors.New(string(extra)), + `extra data found in the SSH key`, + ) + } + + if procType, ok := decoded.Headers[`Proc-Type`]; ok { + // according to pem_decrypt.go in stdlib + if procType == `4,ENCRYPTED` { + passphrase, err := readPassword(sshPassphrasePrompt) + if err != nil { + return nil, hierr.Errorf( + err, + `can't read key passphrase`, + ) + } + + data, err = x509.DecryptPEMBlock(decoded, []byte(passphrase)) + if err != nil { + return nil, hierr.Errorf( + err, + `can't decrypt (using passphrase) SSH key`, + ) + } + + rsa, err := x509.ParsePKCS1PrivateKey(data) + if err != nil { + return nil, hierr.Errorf( + err, + `can't parse decrypted key as RSA key`, + ) + } + + pemBytes := bytes.Buffer{} + err = pem.Encode( + &pemBytes, + &pem.Block{ + Type: `RSA PRIVATE KEY`, + Bytes: x509.MarshalPKCS1PrivateKey(rsa), + }, + ) + if err != nil { + return nil, hierr.Errorf( + err, + `can't convert decrypted RSA key into PEM format`, + ) + } + + data = pemBytes.Bytes() + } + } + + return data, nil +} + func createRunnerFactory( timeouts *runcmd.Timeouts, sshKeyPath string, @@ -707,8 +782,17 @@ func createRunnerFactory( ), nil case sshKeyPath != "": + key, err := readSSHKey(sshKeyPath) + if err != nil { + return nil, hierr.Errorf( + err, + `can't read SSH key: '%s'`, + sshKeyPath, + ) + } + return createRemoteRunnerFactoryWithKey( - sshKeyPath, + string(key), timeouts, ), nil @@ -780,7 +864,7 @@ func generateRunID() string { } func readPassword(prompt string) (string, error) { - fmt.Fprintf(os.Stderr, sshPasswordPrompt) + fmt.Fprintf(os.Stderr, prompt) tty, err := os.Open("/dev/tty") if err != nil { @@ -799,7 +883,7 @@ func readPassword(prompt string) (string, error) { ) } - if sshPasswordPrompt != "" { + if prompt != "" { fmt.Fprintln(os.Stderr) } diff --git a/runner_factory.go b/runner_factory.go index 5934e63..beb0d27 100644 --- a/runner_factory.go +++ b/runner_factory.go @@ -16,7 +16,7 @@ func createRemoteRunnerFactoryWithKey( ) runnerFactory { return func(address address) (runcmd.Runner, error) { return createRunner( - runcmd.NewRemoteKeyAuthRunnerWithTimeouts, + runcmd.NewRemoteRawKeyAuthRunnerWithTimeouts, key, address, *timeouts, diff --git a/tests/orgalorg.sh b/tests/orgalorg.sh index f5afbb5..e0510a4 100644 --- a/tests/orgalorg.sh +++ b/tests/orgalorg.sh @@ -30,6 +30,30 @@ orgalorg_user="orgalorg" EXPECT } +:orgalorg:with-key-passphrase() { + local passphrase="$1" + shift + + :expect() { + expect -f <(cat) -- "${@}" /home/$(ssh-test:print-username)/.ssh/authorized_keys + chown -R \\\\ $(ssh-test:print-username): /home/$(ssh-test:print-username)" - - tests:ensure ssh-test:remote:copy-id < "$(ssh-test:print-key-path).pub" } :start-ssh-daemon() { diff --git a/tests/testcases/auth/can-authenticate-via-key-with-passphrase.test.sh b/tests/testcases/auth/can-authenticate-via-key-with-passphrase.test.sh new file mode 100644 index 0000000..3098dee --- /dev/null +++ b/tests/testcases/auth/can-authenticate-via-key-with-passphrase.test.sh @@ -0,0 +1,38 @@ +passphrase="theone" + +tests:ensure ssh-test:local:keygen -f "$(ssh-test:print-key-path)-encrypted" \ + -b 4096 -P "$passphrase" + +tests:ensure cat $(ssh-test:print-key-path) + +:copy-key() { + local container_name="$1" + local container_ip="$2" + + tests:ensure ssh-test:connect:by-key "$container_ip" \ + 'cat > ~/.ssh/authorized_keys' \ + < "$(ssh-test:print-key-path)-encrypted.pub" +} + +containers:do :copy-key + +tests:ensure \ + mv "$(ssh-test:print-key-path)-encrypted" \ + "$(ssh-test:print-key-path)" + +tests:eval :orgalorg:with-key-passphrase "bla-$passphrase" -C -- \ + whoami + +tests:assert-stdout "decryption password incorrect" + +tests:ensure :orgalorg:with-key-passphrase "$passphrase" -C -- \ + whoami + +:check-output() { + local container_name="$1" + local container_ip="$2" + + tests:assert-stdout "$container_ip $orgalorg_user" +} + +containers:do :check-output