package ssh
import (
"bytes"
"errors"
"fmt"
"io"
"strings"
)
type authResult int
const (
authFailure authResult = iota
authPartialSuccess
authSuccess
)
func (c *connection ) clientAuthenticate (config *ClientConfig ) error {
if err := c .transport .writePacket (Marshal (&serviceRequestMsg {serviceUserAuth })); err != nil {
return err
}
packet , err := c .transport .readPacket ()
if err != nil {
return err
}
extensions := make (map [string ][]byte )
if len (packet ) > 0 && packet [0 ] == msgExtInfo {
var extInfo extInfoMsg
if err := Unmarshal (packet , &extInfo ); err != nil {
return err
}
payload := extInfo .Payload
for i := uint32 (0 ); i < extInfo .NumExtensions ; i ++ {
name , rest , ok := parseString (payload )
if !ok {
return parseError (msgExtInfo )
}
value , rest , ok := parseString (rest )
if !ok {
return parseError (msgExtInfo )
}
extensions [string (name )] = value
payload = rest
}
packet , err = c .transport .readPacket ()
if err != nil {
return err
}
}
var serviceAccept serviceAcceptMsg
if err := Unmarshal (packet , &serviceAccept ); err != nil {
return err
}
var tried []string
var lastMethods []string
sessionID := c .transport .getSessionID ()
for auth := AuthMethod (new (noneAuth )); auth != nil ; {
ok , methods , err := auth .auth (sessionID , config .User , c .transport , config .Rand , extensions )
if err != nil {
ok = authFailure
}
if ok == authSuccess {
return nil
} else if ok == authFailure {
if m := auth .method (); !contains (tried , m ) {
tried = append (tried , m )
}
}
if methods == nil {
methods = lastMethods
}
lastMethods = methods
auth = nil
findNext :
for _ , a := range config .Auth {
candidateMethod := a .method ()
if contains (tried , candidateMethod ) {
continue
}
for _ , meth := range methods {
if meth == candidateMethod {
auth = a
break findNext
}
}
}
if auth == nil && err != nil {
return err
}
}
return fmt .Errorf ("ssh: unable to authenticate, attempted methods %v, no supported methods remain" , tried )
}
func contains(list []string , e string ) bool {
for _ , s := range list {
if s == e {
return true
}
}
return false
}
type AuthMethod interface {
auth(session []byte , user string , p packetConn , rand io .Reader , extensions map [string ][]byte ) (authResult , []string , error )
method() string
}
type noneAuth int
func (n *noneAuth ) auth (session []byte , user string , c packetConn , rand io .Reader , _ map [string ][]byte ) (authResult , []string , error ) {
if err := c .writePacket (Marshal (&userAuthRequestMsg {
User : user ,
Service : serviceSSH ,
Method : "none" ,
})); err != nil {
return authFailure , nil , err
}
return handleAuthResponse (c )
}
func (n *noneAuth ) method () string {
return "none"
}
type passwordCallback func () (password string , err error )
func (cb passwordCallback ) auth (session []byte , user string , c packetConn , rand io .Reader , _ map [string ][]byte ) (authResult , []string , error ) {
type passwordAuthMsg struct {
User string `sshtype:"50"`
Service string
Method string
Reply bool
Password string
}
pw , err := cb ()
if err != nil {
return authFailure , nil , err
}
if err := c .writePacket (Marshal (&passwordAuthMsg {
User : user ,
Service : serviceSSH ,
Method : cb .method (),
Reply : false ,
Password : pw ,
})); err != nil {
return authFailure , nil , err
}
return handleAuthResponse (c )
}
func (cb passwordCallback ) method () string {
return "password"
}
func Password (secret string ) AuthMethod {
return passwordCallback (func () (string , error ) { return secret , nil })
}
func PasswordCallback (prompt func () (secret string , err error )) AuthMethod {
return passwordCallback (prompt )
}
type publickeyAuthMsg struct {
User string `sshtype:"50"`
Service string
Method string
HasSig bool
Algoname string
PubKey []byte
Sig []byte `ssh:"rest"`
}
type publicKeyCallback func () ([]Signer , error )
func (cb publicKeyCallback ) method () string {
return "publickey"
}
func pickSignatureAlgorithm(signer Signer , extensions map [string ][]byte ) (MultiAlgorithmSigner , string , error ) {
var as MultiAlgorithmSigner
keyFormat := signer .PublicKey ().Type ()
switch s := signer .(type ) {
case MultiAlgorithmSigner :
as = s
case AlgorithmSigner :
as = &multiAlgorithmSigner {
AlgorithmSigner : s ,
supportedAlgorithms : algorithmsForKeyFormat (underlyingAlgo (keyFormat )),
}
default :
as = &multiAlgorithmSigner {
AlgorithmSigner : algorithmSignerWrapper {signer },
supportedAlgorithms : []string {underlyingAlgo (keyFormat )},
}
}
getFallbackAlgo := func () (string , error ) {
if !contains (as .Algorithms (), underlyingAlgo (keyFormat )) {
return "" , fmt .Errorf ("ssh: no common public key signature algorithm, server only supports %q for key type %q, signer only supports %v" ,
underlyingAlgo (keyFormat ), keyFormat , as .Algorithms ())
}
return keyFormat , nil
}
extPayload , ok := extensions ["server-sig-algs" ]
if !ok {
algo , err := getFallbackAlgo ()
return as , algo , err
}
serverAlgos := strings .Split (string (extPayload ), "," )
for _ , algo := range serverAlgos {
if certAlgo , ok := certificateAlgo (algo ); ok {
serverAlgos = append (serverAlgos , certAlgo )
}
}
var keyAlgos []string
for _ , algo := range algorithmsForKeyFormat (keyFormat ) {
if contains (as .Algorithms (), underlyingAlgo (algo )) {
keyAlgos = append (keyAlgos , algo )
}
}
algo , err := findCommon ("public key signature algorithm" , keyAlgos , serverAlgos )
if err != nil {
algo , err := getFallbackAlgo ()
return as , algo , err
}
return as , algo , nil
}
func (cb publicKeyCallback ) auth (session []byte , user string , c packetConn , rand io .Reader , extensions map [string ][]byte ) (authResult , []string , error ) {
signers , err := cb ()
if err != nil {
return authFailure , nil , err
}
var methods []string
var errSigAlgo error
for _ , signer := range signers {
pub := signer .PublicKey ()
as , algo , err := pickSignatureAlgorithm (signer , extensions )
if err != nil && errSigAlgo == nil {
errSigAlgo = err
continue
}
ok , err := validateKey (pub , algo , user , c )
if err != nil {
return authFailure , nil , err
}
if !ok {
continue
}
pubKey := pub .Marshal ()
data := buildDataSignedForAuth (session , userAuthRequestMsg {
User : user ,
Service : serviceSSH ,
Method : cb .method (),
}, algo , pubKey )
sign , err := as .SignWithAlgorithm (rand , data , underlyingAlgo (algo ))
if err != nil {
return authFailure , nil , err
}
s := Marshal (sign )
sig := make ([]byte , stringLength (len (s )))
marshalString (sig , s )
msg := publickeyAuthMsg {
User : user ,
Service : serviceSSH ,
Method : cb .method (),
HasSig : true ,
Algoname : algo ,
PubKey : pubKey ,
Sig : sig ,
}
p := Marshal (&msg )
if err := c .writePacket (p ); err != nil {
return authFailure , nil , err
}
var success authResult
success , methods , err = handleAuthResponse (c )
if err != nil {
return authFailure , nil , err
}
if success == authSuccess || !contains (methods , cb .method ()) {
return success , methods , err
}
}
return authFailure , methods , errSigAlgo
}
func validateKey(key PublicKey , algo string , user string , c packetConn ) (bool , error ) {
pubKey := key .Marshal ()
msg := publickeyAuthMsg {
User : user ,
Service : serviceSSH ,
Method : "publickey" ,
HasSig : false ,
Algoname : algo ,
PubKey : pubKey ,
}
if err := c .writePacket (Marshal (&msg )); err != nil {
return false , err
}
return confirmKeyAck (key , algo , c )
}
func confirmKeyAck(key PublicKey , algo string , c packetConn ) (bool , error ) {
pubKey := key .Marshal ()
for {
packet , err := c .readPacket ()
if err != nil {
return false , err
}
switch packet [0 ] {
case msgUserAuthBanner :
if err := handleBannerResponse (c , packet ); err != nil {
return false , err
}
case msgUserAuthPubKeyOk :
var msg userAuthPubKeyOkMsg
if err := Unmarshal (packet , &msg ); err != nil {
return false , err
}
if msg .Algo != algo || !bytes .Equal (msg .PubKey , pubKey ) {
return false , nil
}
return true , nil
case msgUserAuthFailure :
return false , nil
default :
return false , unexpectedMessageError (msgUserAuthPubKeyOk , packet [0 ])
}
}
}
func PublicKeys (signers ...Signer ) AuthMethod {
return publicKeyCallback (func () ([]Signer , error ) { return signers , nil })
}
func PublicKeysCallback (getSigners func () (signers []Signer , err error )) AuthMethod {
return publicKeyCallback (getSigners )
}
func handleAuthResponse(c packetConn ) (authResult , []string , error ) {
gotMsgExtInfo := false
for {
packet , err := c .readPacket ()
if err != nil {
return authFailure , nil , err
}
switch packet [0 ] {
case msgUserAuthBanner :
if err := handleBannerResponse (c , packet ); err != nil {
return authFailure , nil , err
}
case msgExtInfo :
if gotMsgExtInfo {
return authFailure , nil , unexpectedMessageError (msgUserAuthSuccess , packet [0 ])
}
gotMsgExtInfo = true
case msgUserAuthFailure :
var msg userAuthFailureMsg
if err := Unmarshal (packet , &msg ); err != nil {
return authFailure , nil , err
}
if msg .PartialSuccess {
return authPartialSuccess , msg .Methods , nil
}
return authFailure , msg .Methods , nil
case msgUserAuthSuccess :
return authSuccess , nil , nil
default :
return authFailure , nil , unexpectedMessageError (msgUserAuthSuccess , packet [0 ])
}
}
}
func handleBannerResponse(c packetConn , packet []byte ) error {
var msg userAuthBannerMsg
if err := Unmarshal (packet , &msg ); err != nil {
return err
}
transport , ok := c .(*handshakeTransport )
if !ok {
return nil
}
if transport .bannerCallback != nil {
return transport .bannerCallback (msg .Message )
}
return nil
}
type KeyboardInteractiveChallenge func (name, instruction string , questions []string , echos []bool ) (answers []string , err error )
func KeyboardInteractive (challenge KeyboardInteractiveChallenge ) AuthMethod {
return challenge
}
func (cb KeyboardInteractiveChallenge ) method () string {
return "keyboard-interactive"
}
func (cb KeyboardInteractiveChallenge ) auth (session []byte , user string , c packetConn , rand io .Reader , _ map [string ][]byte ) (authResult , []string , error ) {
type initiateMsg struct {
User string `sshtype:"50"`
Service string
Method string
Language string
Submethods string
}
if err := c .writePacket (Marshal (&initiateMsg {
User : user ,
Service : serviceSSH ,
Method : "keyboard-interactive" ,
})); err != nil {
return authFailure , nil , err
}
gotMsgExtInfo := false
for {
packet , err := c .readPacket ()
if err != nil {
return authFailure , nil , err
}
switch packet [0 ] {
case msgUserAuthBanner :
if err := handleBannerResponse (c , packet ); err != nil {
return authFailure , nil , err
}
continue
case msgExtInfo :
if gotMsgExtInfo {
return authFailure , nil , unexpectedMessageError (msgUserAuthInfoRequest , packet [0 ])
}
gotMsgExtInfo = true
continue
case msgUserAuthInfoRequest :
case msgUserAuthFailure :
var msg userAuthFailureMsg
if err := Unmarshal (packet , &msg ); err != nil {
return authFailure , nil , err
}
if msg .PartialSuccess {
return authPartialSuccess , msg .Methods , nil
}
return authFailure , msg .Methods , nil
case msgUserAuthSuccess :
return authSuccess , nil , nil
default :
return authFailure , nil , unexpectedMessageError (msgUserAuthInfoRequest , packet [0 ])
}
var msg userAuthInfoRequestMsg
if err := Unmarshal (packet , &msg ); err != nil {
return authFailure , nil , err
}
rest := msg .Prompts
var prompts []string
var echos []bool
for i := 0 ; i < int (msg .NumPrompts ); i ++ {
prompt , r , ok := parseString (rest )
if !ok || len (r ) == 0 {
return authFailure , nil , errors .New ("ssh: prompt format error" )
}
prompts = append (prompts , string (prompt ))
echos = append (echos , r [0 ] != 0 )
rest = r [1 :]
}
if len (rest ) != 0 {
return authFailure , nil , errors .New ("ssh: extra data following keyboard-interactive pairs" )
}
answers , err := cb (msg .Name , msg .Instruction , prompts , echos )
if err != nil {
return authFailure , nil , err
}
if len (answers ) != len (prompts ) {
return authFailure , nil , fmt .Errorf ("ssh: incorrect number of answers from keyboard-interactive callback %d (expected %d)" , len (answers ), len (prompts ))
}
responseLength := 1 + 4
for _ , a := range answers {
responseLength += stringLength (len (a ))
}
serialized := make ([]byte , responseLength )
p := serialized
p [0 ] = msgUserAuthInfoResponse
p = p [1 :]
p = marshalUint32 (p , uint32 (len (answers )))
for _ , a := range answers {
p = marshalString (p , []byte (a ))
}
if err := c .writePacket (serialized ); err != nil {
return authFailure , nil , err
}
}
}
type retryableAuthMethod struct {
authMethod AuthMethod
maxTries int
}
func (r *retryableAuthMethod ) auth (session []byte , user string , c packetConn , rand io .Reader , extensions map [string ][]byte ) (ok authResult , methods []string , err error ) {
for i := 0 ; r .maxTries <= 0 || i < r .maxTries ; i ++ {
ok , methods , err = r .authMethod .auth (session , user , c , rand , extensions )
if ok != authFailure || err != nil {
return ok , methods , err
}
}
return ok , methods , err
}
func (r *retryableAuthMethod ) method () string {
return r .authMethod .method ()
}
func RetryableAuthMethod (auth AuthMethod , maxTries int ) AuthMethod {
return &retryableAuthMethod {authMethod : auth , maxTries : maxTries }
}
func GSSAPIWithMICAuthMethod (gssAPIClient GSSAPIClient , target string ) AuthMethod {
if gssAPIClient == nil {
panic ("gss-api client must be not nil with enable gssapi-with-mic" )
}
return &gssAPIWithMICCallback {gssAPIClient : gssAPIClient , target : target }
}
type gssAPIWithMICCallback struct {
gssAPIClient GSSAPIClient
target string
}
func (g *gssAPIWithMICCallback ) auth (session []byte , user string , c packetConn , rand io .Reader , _ map [string ][]byte ) (authResult , []string , error ) {
m := &userAuthRequestMsg {
User : user ,
Service : serviceSSH ,
Method : g .method (),
}
m .Payload = appendU32 (m .Payload , 1 )
m .Payload = appendString (m .Payload , string (krb5OID ))
if err := c .writePacket (Marshal (m )); err != nil {
return authFailure , nil , err
}
packet , err := c .readPacket ()
if err != nil {
return authFailure , nil , err
}
userAuthGSSAPIResp := &userAuthGSSAPIResponse {}
if err := Unmarshal (packet , userAuthGSSAPIResp ); err != nil {
return authFailure , nil , err
}
var token []byte
defer g .gssAPIClient .DeleteSecContext ()
for {
nextToken , needContinue , err := g .gssAPIClient .InitSecContext ("host@" +g .target , token , false )
if err != nil {
return authFailure , nil , err
}
if len (nextToken ) > 0 {
if err := c .writePacket (Marshal (&userAuthGSSAPIToken {
Token : nextToken ,
})); err != nil {
return authFailure , nil , err
}
}
if !needContinue {
break
}
packet , err = c .readPacket ()
if err != nil {
return authFailure , nil , err
}
switch packet [0 ] {
case msgUserAuthFailure :
var msg userAuthFailureMsg
if err := Unmarshal (packet , &msg ); err != nil {
return authFailure , nil , err
}
if msg .PartialSuccess {
return authPartialSuccess , msg .Methods , nil
}
return authFailure , msg .Methods , nil
case msgUserAuthGSSAPIError :
userAuthGSSAPIErrorResp := &userAuthGSSAPIError {}
if err := Unmarshal (packet , userAuthGSSAPIErrorResp ); err != nil {
return authFailure , nil , err
}
return authFailure , nil , fmt .Errorf ("GSS-API Error:\n" +
"Major Status: %d\n" +
"Minor Status: %d\n" +
"Error Message: %s\n" , userAuthGSSAPIErrorResp .MajorStatus , userAuthGSSAPIErrorResp .MinorStatus ,
userAuthGSSAPIErrorResp .Message )
case msgUserAuthGSSAPIToken :
userAuthGSSAPITokenReq := &userAuthGSSAPIToken {}
if err := Unmarshal (packet , userAuthGSSAPITokenReq ); err != nil {
return authFailure , nil , err
}
token = userAuthGSSAPITokenReq .Token
}
}
micField := buildMIC (string (session ), user , "ssh-connection" , "gssapi-with-mic" )
micToken , err := g .gssAPIClient .GetMIC (micField )
if err != nil {
return authFailure , nil , err
}
if err := c .writePacket (Marshal (&userAuthGSSAPIMIC {
MIC : micToken ,
})); err != nil {
return authFailure , nil , err
}
return handleAuthResponse (c )
}
func (g *gssAPIWithMICCallback ) method () string {
return "gssapi-with-mic"
}
The pages are generated with Golds v0.6.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds .