From 435038545e9df0767469dab11abf7a7436a53a78 Mon Sep 17 00:00:00 2001 From: Robi Nino Date: Tue, 13 Nov 2018 16:28:09 +0200 Subject: [PATCH] SSH Authentication behaviour update Following Issue 83 on the CLI, the behaviour of ssh authentication was changed to: 1. Try authenticating via ssh-agent. If failed due to any reason (No agent / connection failed / no key in agent): 2. Try authenticating via ssh-key. If the key is encrypted, passphrase has to be passed via the --ssh-passphrase flag. --- artifactory/auth/rtdetails.go | 6 +-- artifactory/auth/sshlogin.go | 83 ++++++++++++++++++++++++++++++----- utils/utils.go | 19 ++++++++ 3 files changed, 95 insertions(+), 13 deletions(-) diff --git a/artifactory/auth/rtdetails.go b/artifactory/auth/rtdetails.go index 9691afcee..b14f0bbf8 100644 --- a/artifactory/auth/rtdetails.go +++ b/artifactory/auth/rtdetails.go @@ -29,7 +29,7 @@ type ArtifactoryDetails interface { SetApiKey(apiKey string) SetSshAuthHeaders(sshAuthHeaders map[string]string) - AuthenticateSsh(sshKey, sshPassphrase []byte) error + AuthenticateSsh(sshKey, sshPassphrase string) error CreateHttpClientDetails() httputils.HttpClientDetails } @@ -83,8 +83,8 @@ func (rt *artifactoryDetails) SetSshAuthHeaders(sshAuthHeaders map[string]string rt.SshAuthHeaders = sshAuthHeaders } -func (rt *artifactoryDetails) AuthenticateSsh(sshKey, sshPassphrase []byte) error { - sshHeaders, baseUrl, err := sshAuthentication(rt.Url, sshKey, sshPassphrase) +func (rt *artifactoryDetails) AuthenticateSsh(sshKeyPath, sshPassphrase string) error { + sshHeaders, baseUrl, err := sshAuthentication(rt.Url, sshKeyPath, sshPassphrase) if err != nil { return err } diff --git a/artifactory/auth/sshlogin.go b/artifactory/auth/sshlogin.go index 35b9c1319..34461abeb 100644 --- a/artifactory/auth/sshlogin.go +++ b/artifactory/auth/sshlogin.go @@ -3,33 +3,71 @@ package auth import ( "bytes" "encoding/json" + "encoding/pem" "errors" + "fmt" "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/xanzy/ssh-agent" "golang.org/x/crypto/ssh" "io" + "io/ioutil" "regexp" "strconv" + "strings" ) -func sshAuthentication(url string, sshKey, sshPassphrase []byte) (map[string]string, string, error) { +func sshAuthentication(url, sshKeyPath, sshPassphrase string) (sshAuthHeaders map[string]string, newUrl string, err error) { _, host, port, err := parseUrl(url) if err != nil { return nil, "", err } - log.Info("Performing SSH authentication...") var sshAuth ssh.AuthMethod - if len(sshKey) == 0 { - sshAuth, err = sshAuthAgent() - } else { - sshAuth, err = sshAuthPublicKey(sshKey, sshPassphrase) + log.Info("Performing SSH authentication...") + log.Info("Trying to authenticate via SSH-Agent...") + + // Try authenticating via agent. If failed, try authenticating via key. + sshAuth, err = sshAuthAgent() + if err == nil { + sshAuthHeaders, newUrl, err = getSshHeaders(sshAuth, host, port) } if err != nil { - return nil, "", err + log.Info("Authentication via SSH-Agent failed. Error:\n", err) + log.Info("Trying to authenticate via SSH Key...") + + // Check if key specified + if len(sshKeyPath) <= 0 { + log.Info("Authentication via SSH key failed.") + return nil, "", fmt.Errorf("SSH key not specified.") + } + + // Read key and passphrase + var sshKey, sshPassphraseBytes []byte + sshKey, sshPassphraseBytes, err = readSshKeyAndPassphrase(sshKeyPath, sshPassphrase) + if err != nil { + log.Info("Authentication via SSH key failed.") + return nil, "", err + } + + // Verify key and get ssh headers + sshAuth, err = sshAuthPublicKey(sshKey, sshPassphraseBytes) + if err == nil { + sshAuthHeaders, newUrl, err = getSshHeaders(sshAuth, host, port) + } + if err != nil { + log.Info("Authentication via SSH Key failed.") + return nil, "", err + } } + + // If successful, return headers + log.Info("SSH authentication successful.") + return sshAuthHeaders, newUrl, nil +} + +func getSshHeaders(sshAuth ssh.AuthMethod, host string, port int) (map[string]string, string, error) { sshConfig := &ssh.ClientConfig{ User: "admin", Auth: []ssh.AuthMethod{ @@ -65,12 +103,38 @@ func sshAuthentication(url string, sshKey, sshPassphrase []byte) (map[string]str if err = json.Unmarshal(buf.Bytes(), &result); errorutils.CheckError(err) != nil { return nil, "", err } - url = utils.AddTrailingSlashIfNeeded(result.Href) + url := utils.AddTrailingSlashIfNeeded(result.Href) sshAuthHeaders := result.Headers - log.Info("SSH authentication successful.") return sshAuthHeaders, url, nil } +func readSshKeyAndPassphrase(sshKeyPath, sshPassphrase string) ([]byte, []byte, error) { + sshKey, err := ioutil.ReadFile(utils.ReplaceTildeWithUserHome(sshKeyPath)) + if err != nil { + return nil, nil, errorutils.CheckError(err) + } + if len(sshPassphrase) == 0 { + encryptedKey, err := IsEncrypted(sshKey) + if err != nil { + return nil, nil, errorutils.CheckError(err) + } + // If key is encrypted but no passphrase specified + if encryptedKey { + return nil, nil, errorutils.CheckError(errors.New("SSH Key is encrypted but no passphrase was specified.")) + } + } + + return sshKey, []byte(sshPassphrase), err +} + +func IsEncrypted(buffer []byte) (bool, error) { + block, _ := pem.Decode(buffer) + if block == nil { + return false, errors.New("SSH: no key found") + } + return strings.Contains(block.Headers["Proc-Type"], "ENCRYPTED"), nil +} + func parseUrl(url string) (protocol, host string, port int, err error) { pattern1 := "^(.+)://(.+):([0-9].+)/$" pattern2 := "^(.+)://(.+)$" @@ -114,7 +178,6 @@ func sshAuthPublicKey(sshKey, sshPassphrase []byte) (ssh.AuthMethod, error) { } func sshAuthAgent() (ssh.AuthMethod, error) { - log.Info("Authenticating Using SSH agent") sshAgent, _, err := sshagent.New() if errorutils.CheckError(err) != nil { return nil, err diff --git a/utils/utils.go b/utils/utils.go index a6e8b8a6b..62099837a 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" "os" "regexp" "runtime" @@ -216,6 +217,24 @@ func GetUserHomeDir() string { return os.Getenv("HOME") } +func GetBoolEnvValue(flagName string, defValue bool) (bool, error) { + envVarValue := os.Getenv(flagName) + if envVarValue == "" { + return defValue, nil + } + val, err := strconv.ParseBool(envVarValue) + err = CheckErrorWithMessage(err, "can't parse environment variable "+flagName) + return val, err +} + +func CheckErrorWithMessage(err error, message string) error { + if err != nil { + log.Error(message) + err = errorutils.CheckError(err) + } + return err +} + func GetMapFromStringSlice(slice []string, sep string) map[string]string { mapFromSlice := make(map[string]string) for _, value := range slice {