package ssh import ( "errors" "fmt" "net" "os" "os/user" "path/filepath" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" "golang.org/x/crypto/ssh/knownhosts" ) var ErrEmptySSHAgentAddr = errors.New("SSH_AUTH_SOCK env variable is required") // AuthMethod is the interface all auth methods for the ssh client // must implement. The clientConfig method returns the ssh client // configuration needed to establish an ssh connection. type AuthMethod interface { clientConfig() *ssh.ClientConfig hostKeyCallback() (ssh.HostKeyCallback, error) } // The names of the AuthMethod implementations. To be returned by the // Name() method. Most git servers only allow PublicKeysName and // PublicKeysCallbackName. const ( KeyboardInteractiveName = "ssh-keyboard-interactive" PasswordName = "ssh-password" PasswordCallbackName = "ssh-password-callback" PublicKeysName = "ssh-public-keys" PublicKeysCallbackName = "ssh-public-key-callback" ) // KeyboardInteractive implements AuthMethod by using a // prompt/response sequence controlled by the server. type KeyboardInteractive struct { User string Challenge ssh.KeyboardInteractiveChallenge baseAuthMethod } func (a *KeyboardInteractive) Name() string { return KeyboardInteractiveName } func (a *KeyboardInteractive) String() string { return fmt.Sprintf("user: %s, name: %s", a.User, a.Name()) } func (a *KeyboardInteractive) clientConfig() *ssh.ClientConfig { return &ssh.ClientConfig{ User: a.User, Auth: []ssh.AuthMethod{ssh.KeyboardInteractiveChallenge(a.Challenge)}, } } // Password implements AuthMethod by using the given password. type Password struct { User string Pass string baseAuthMethod } func (a *Password) Name() string { return PasswordName } func (a *Password) String() string { return fmt.Sprintf("user: %s, name: %s", a.User, a.Name()) } func (a *Password) clientConfig() *ssh.ClientConfig { return &ssh.ClientConfig{ User: a.User, Auth: []ssh.AuthMethod{ssh.Password(a.Pass)}, } } // PasswordCallback implements AuthMethod by using a callback // to fetch the password. type PasswordCallback struct { User string Callback func() (pass string, err error) baseAuthMethod } func (a *PasswordCallback) Name() string { return PasswordCallbackName } func (a *PasswordCallback) String() string { return fmt.Sprintf("user: %s, name: %s", a.User, a.Name()) } func (a *PasswordCallback) clientConfig() *ssh.ClientConfig { return &ssh.ClientConfig{ User: a.User, Auth: []ssh.AuthMethod{ssh.PasswordCallback(a.Callback)}, } } // PublicKeys implements AuthMethod by using the given // key pairs. type PublicKeys struct { User string Signer ssh.Signer baseAuthMethod } func (a *PublicKeys) Name() string { return PublicKeysName } func (a *PublicKeys) String() string { return fmt.Sprintf("user: %s, name: %s", a.User, a.Name()) } func (a *PublicKeys) clientConfig() *ssh.ClientConfig { return &ssh.ClientConfig{ User: a.User, Auth: []ssh.AuthMethod{ssh.PublicKeys(a.Signer)}, } } // PublicKeysCallback implements AuthMethod by asking a // ssh.agent.Agent to act as a signer. type PublicKeysCallback struct { User string Callback func() (signers []ssh.Signer, err error) baseAuthMethod } func (a *PublicKeysCallback) Name() string { return PublicKeysCallbackName } func (a *PublicKeysCallback) String() string { return fmt.Sprintf("user: %s, name: %s", a.User, a.Name()) } func (a *PublicKeysCallback) clientConfig() *ssh.ClientConfig { return &ssh.ClientConfig{ User: a.User, Auth: []ssh.AuthMethod{ssh.PublicKeysCallback(a.Callback)}, } } const DefaultSSHUsername = "git" // NewSSHAgentAuth opens a pipe with the SSH agent and uses the pipe // as the implementer of the public key callback function. func NewSSHAgentAuth(user string) (*PublicKeysCallback, error) { if user == "" { user = DefaultSSHUsername } sshAgentAddr := os.Getenv("SSH_AUTH_SOCK") if sshAgentAddr == "" { return nil, ErrEmptySSHAgentAddr } pipe, err := net.Dial("unix", sshAgentAddr) if err != nil { return nil, fmt.Errorf("error connecting to SSH agent: %q", err) } return &PublicKeysCallback{ User: user, Callback: agent.NewClient(pipe).Signers, }, nil } // NewKnownHostsCallback returns ssh.HostKeyCallback based on a file based on a // know_hosts file. http://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT // // If files is empty, the list of files will be read from the SSH_KNOWN_HOSTS // environment variable, example: // /home/foo/custom_known_hosts_file:/etc/custom_known/hosts_file // // If SSH_KNOWN_HOSTS is not set the following file locations will be used: // ~/.ssh/known_hosts // /etc/ssh/ssh_known_hosts func NewKnownHostsCallback(files ...string) (ssh.HostKeyCallback, error) { files, err := getDefaultKnownHostsFiles() if err != nil { return nil, err } files, err = filterKnownHostsFiles(files...) if err != nil { return nil, err } return knownhosts.New(files...) } func getDefaultKnownHostsFiles() ([]string, error) { files := filepath.SplitList(os.Getenv("SSH_KNOWN_HOSTS")) if len(files) != 0 { return files, nil } user, err := user.Current() if err != nil { return nil, err } return []string{ filepath.Join(user.HomeDir, "/.ssh/known_hosts"), "/etc/ssh/ssh_known_hosts", }, nil } func filterKnownHostsFiles(files ...string) ([]string, error) { var out []string for _, file := range files { _, err := os.Stat(file) if err == nil { out = append(out, file) continue } if !os.IsNotExist(err) { return nil, err } } if len(out) == 0 { return nil, fmt.Errorf("unable to find any valid know_hosts file, set SSH_KNOWN_HOSTS env variable") } return out, nil } type baseAuthMethod struct { // HostKeyCallback is the function type used for verifying server keys. // If nil default callback will be create using NewKnownHostsHostKeyCallback // without argument. HostKeyCallback ssh.HostKeyCallback } func (m *baseAuthMethod) hostKeyCallback() (ssh.HostKeyCallback, error) { if m.HostKeyCallback == nil { return NewKnownHostsCallback() } return m.HostKeyCallback, nil }