github.com/jfrog/jfrog-client-go@v1.40.2/auth/sshlogin.go (about)

     1  package auth
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"io"
     7  	"os"
     8  	"regexp"
     9  	"strconv"
    10  
    11  	"github.com/jfrog/jfrog-client-go/utils"
    12  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    13  	"github.com/jfrog/jfrog-client-go/utils/log"
    14  	sshagent "github.com/xanzy/ssh-agent"
    15  	"golang.org/x/crypto/ssh"
    16  )
    17  
    18  func SshAuthentication(url, sshKeyPath, sshPassphrase string) (sshAuthHeaders map[string]string, newUrl string, err error) {
    19  	_, host, port, err := parseUrl(url)
    20  	if err != nil {
    21  		return nil, "", err
    22  	}
    23  
    24  	var sshAuth ssh.AuthMethod
    25  	log.Debug("Performing SSH authentication...")
    26  	log.Debug("Trying to authenticate via SSH-Agent...")
    27  
    28  	// Try authenticating via agent. If failed, try authenticating via key.
    29  	sshAuth, err = sshAuthAgent()
    30  	if err == nil {
    31  		sshAuthHeaders, newUrl, err = getSshHeaders(sshAuth, host, port)
    32  	}
    33  	if err != nil {
    34  		log.Debug("Authentication via SSH-Agent failed. Error:\n", err)
    35  		log.Debug("Trying to authenticate via SSH Key...")
    36  
    37  		// Check if key specified
    38  		if len(sshKeyPath) == 0 {
    39  			log.Error("Authentication via SSH key failed.")
    40  			return nil, "", errorutils.CheckErrorf("SSH key not specified.")
    41  		}
    42  
    43  		// Read key and passphrase
    44  		var sshKey, sshPassphraseBytes []byte
    45  		sshKey, sshPassphraseBytes, err = readSshKeyAndPassphrase(sshKeyPath, sshPassphrase)
    46  		if err != nil {
    47  			log.Error("Authentication via SSH key failed.")
    48  			return nil, "", err
    49  		}
    50  
    51  		// Verify key and get ssh headers
    52  		sshAuth, err = sshAuthPublicKey(sshKey, sshPassphraseBytes)
    53  		if err == nil {
    54  			sshAuthHeaders, newUrl, err = getSshHeaders(sshAuth, host, port)
    55  		}
    56  		if err != nil {
    57  			log.Error("Authentication via SSH Key failed.")
    58  			return nil, "", err
    59  		}
    60  	}
    61  
    62  	// If successful, return headers
    63  	log.Debug("SSH authentication successful.")
    64  	return sshAuthHeaders, newUrl, nil
    65  }
    66  
    67  func getSshHeaders(sshAuth ssh.AuthMethod, host string, port int) (map[string]string, string, error) {
    68  	sshConfig := &ssh.ClientConfig{
    69  		User: "admin",
    70  		Auth: []ssh.AuthMethod{
    71  			sshAuth,
    72  		},
    73  		//#nosec G106 -- Used to get ssh headers only.
    74  		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    75  	}
    76  
    77  	hostAndPort := host + ":" + strconv.Itoa(port)
    78  	connection, err := ssh.Dial("tcp", hostAndPort, sshConfig)
    79  	if errorutils.CheckError(err) != nil {
    80  		return nil, "", err
    81  	}
    82  	defer connection.Close()
    83  	session, err := connection.NewSession()
    84  	if errorutils.CheckError(err) != nil {
    85  		return nil, "", err
    86  	}
    87  	defer session.Close()
    88  
    89  	stdout, err := session.StdoutPipe()
    90  	if errorutils.CheckError(err) != nil {
    91  		return nil, "", err
    92  	}
    93  
    94  	if err = session.Run("jfrog-authenticate"); err != nil && err != io.EOF {
    95  		return nil, "", errorutils.CheckError(err)
    96  	}
    97  	var buf bytes.Buffer
    98  	_, err = io.Copy(&buf, stdout)
    99  	if errorutils.CheckError(err) != nil {
   100  		return nil, "", err
   101  	}
   102  	var result SshAuthResult
   103  	if err = json.Unmarshal(buf.Bytes(), &result); errorutils.CheckError(err) != nil {
   104  		return nil, "", err
   105  	}
   106  	url := utils.AddTrailingSlashIfNeeded(result.Href)
   107  	sshAuthHeaders := result.Headers
   108  	return sshAuthHeaders, url, nil
   109  }
   110  
   111  func readSshKeyAndPassphrase(sshKeyPath, sshPassphrase string) ([]byte, []byte, error) {
   112  	sshKey, err := os.ReadFile(utils.ReplaceTildeWithUserHome(sshKeyPath))
   113  	if err != nil {
   114  		return nil, nil, errorutils.CheckError(err)
   115  	}
   116  	if len(sshPassphrase) == 0 {
   117  		encryptedKey, err := IsEncrypted(sshKey)
   118  		if err != nil {
   119  			return nil, nil, errorutils.CheckError(err)
   120  		}
   121  		// If key is encrypted but no passphrase specified
   122  		if encryptedKey {
   123  			return nil, nil, errorutils.CheckErrorf("SSH Key is encrypted but no passphrase was specified.")
   124  		}
   125  	}
   126  
   127  	return sshKey, []byte(sshPassphrase), err
   128  }
   129  
   130  func IsEncrypted(buffer []byte) (bool, error) {
   131  	_, err := ssh.ParsePrivateKey(buffer)
   132  	if _, ok := err.(*ssh.PassphraseMissingError); ok {
   133  		// Key is encrypted
   134  		return true, nil
   135  	}
   136  	// Key is not encrypted or an error occurred
   137  	return false, err
   138  }
   139  
   140  func parseUrl(url string) (protocol, host string, port int, err error) {
   141  	pattern1 := "^(.+)://(.+):([0-9].+)/$"
   142  	pattern2 := "^(.+)://(.+)$"
   143  
   144  	var r *regexp.Regexp
   145  	r, err = regexp.Compile(pattern1)
   146  	if errorutils.CheckError(err) != nil {
   147  		return
   148  	}
   149  	groups := r.FindStringSubmatch(url)
   150  	if len(groups) == 4 {
   151  		protocol = groups[1]
   152  		host = groups[2]
   153  		port, err = strconv.Atoi(groups[3])
   154  		if err != nil {
   155  			err = errorutils.CheckErrorf("URL: " + url + " is invalid. Expecting ssh://<host>:<port> or http(s)://...")
   156  		}
   157  		return
   158  	}
   159  
   160  	r, err = regexp.Compile(pattern2)
   161  	err = errorutils.CheckError(err)
   162  	if err != nil {
   163  		return
   164  	}
   165  	groups = r.FindStringSubmatch(url)
   166  	if len(groups) == 3 {
   167  		protocol = groups[1]
   168  		host = groups[2]
   169  		port = 80
   170  	}
   171  	return
   172  }
   173  
   174  func sshAuthPublicKey(sshKey, sshPassphrase []byte) (ssh.AuthMethod, error) {
   175  	var key ssh.Signer
   176  	var err error
   177  	if len(sshPassphrase) == 0 {
   178  		key, err = ssh.ParsePrivateKey(sshKey)
   179  	} else {
   180  		key, err = ssh.ParsePrivateKeyWithPassphrase(sshKey, sshPassphrase)
   181  	}
   182  	if errorutils.CheckError(err) != nil {
   183  		return nil, err
   184  	}
   185  	return ssh.PublicKeys(key), nil
   186  }
   187  
   188  func sshAuthAgent() (ssh.AuthMethod, error) {
   189  	sshAgent, _, err := sshagent.New()
   190  	if errorutils.CheckError(err) != nil {
   191  		return nil, err
   192  	}
   193  	cbk := sshAgent.Signers
   194  	authMethod := ssh.PublicKeysCallback(cbk)
   195  	return authMethod, nil
   196  }
   197  
   198  type SshAuthResult struct {
   199  	Href    string
   200  	Headers map[string]string
   201  }