Skip to content

ssh: add support for server side multi-step authentication #130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions ssh/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ type ConnMetadata interface {

// LocalAddr returns the local address for this connection.
LocalAddr() net.Addr

// PartialSuccessMethods returns the ordered list of
// authentication methods that returned ErrPartialSuccess.
// It can be used inside callbacks to find if a multi-step
// authentication is done using the correct sequence and to
// return the authentication methods that can continue
PartialSuccessMethods() []string
}

// Conn represents an SSH connection for both server and client roles.
Expand Down Expand Up @@ -102,10 +109,11 @@ func (c *connection) Close() error {
type sshConn struct {
conn net.Conn

user string
sessionID []byte
clientVersion []byte
serverVersion []byte
user string
sessionID []byte
clientVersion []byte
serverVersion []byte
partialSuccessMethods []string
}

func dup(src []byte) []byte {
Expand All @@ -114,6 +122,10 @@ func dup(src []byte) []byte {
return dst
}

func (c *sshConn) PartialSuccessMethods() []string {
return c.partialSuccessMethods
}

func (c *sshConn) User() string {
return c.user
}
Expand Down
74 changes: 71 additions & 3 deletions ssh/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ type ServerConfig struct {

// PasswordCallback, if non-nil, is called when a user
// attempts to authenticate using a password.
// If this is a step of a multi-step authentication
// "PartialSuccessMethods()", available in ConnMetadata,
// can be used to find the steps sequence.
PasswordCallback func(conn ConnMetadata, password []byte) (*Permissions, error)

// PublicKeyCallback, if non-nil, is called when a client
Expand All @@ -88,6 +91,9 @@ type ServerConfig struct {
// offered is in fact used to authenticate. To record any data
// depending on the public key, store it inside a
// Permissions.Extensions entry.
// If this is a step of a multi-step authentication
// "PartialSuccessMethods()", available in ConnMetadata,
// can be used to find the steps sequence.
PublicKeyCallback func(conn ConnMetadata, key PublicKey) (*Permissions, error)

// KeyboardInteractiveCallback, if non-nil, is called when
Expand All @@ -97,8 +103,20 @@ type ServerConfig struct {
// Challenge rounds. To avoid information leaks, the client
// should be presented a challenge even if the user is
// unknown.
// If this is a step of a multi-step authentication
// "PartialSuccessMethods()", available in ConnMetadata,
// can be used to find the steps sequence.
KeyboardInteractiveCallback func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error)

// NextAuthMethodsCallback, if not-nil, is called when another
// authentication callback returns ErrPartialSuccess or if, after an
// initial partial success, an authentication step fails.
// This callback must return the list of authentications methods
// that can continue.
// An empty list means no supported methods remain and so the
// multi-step authentication will fail
NextAuthMethodsCallback func(conn ConnMetadata) []string

// AuthLogCallback, if non-nil, is called to log all authentication
// attempts.
AuthLogCallback func(conn ConnMetadata, method string, err error)
Expand Down Expand Up @@ -395,6 +413,11 @@ func (l ServerAuthError) Error() string {
// It is returned in ServerAuthError.Errors from NewServerConn.
var ErrNoAuth = errors.New("ssh: no auth passed yet")

// ErrPartialSuccess defines the error value that authentication
// callbacks must return for multi-step authentication when a
// specific authentication step succeed
var ErrPartialSuccess = errors.New("ssh: authenticated with partial success")

func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, error) {
sessionID := s.transport.getSessionID()
var cache pubKeyCache
Expand Down Expand Up @@ -433,6 +456,20 @@ userAuthLoop:
return nil, errors.New("ssh: client attempted to negotiate for unknown service: " + userAuthReq.Service)
}

// RFC 4252 section 5 says:
//
// """
// The 'user name' and 'service name' are repeated in every new
// authentication attempt, and MAY change. The server implementation
// MUST carefully check them in every message, and MUST flush any
// accumulated authentication states if they change.
// """
//
// So we reset partialSuccessMethods if the user changes
//
if s.user != userAuthReq.User {
s.partialSuccessMethods = nil
}
s.user = userAuthReq.User

if !displayedBanner && config.BannerCallback != nil {
Expand Down Expand Up @@ -521,10 +558,16 @@ userAuthLoop:
candidate.user = s.user
candidate.pubKeyData = pubKeyData
candidate.perms, candidate.result = config.PublicKeyCallback(s, pubKey)
if candidate.result == nil && candidate.perms != nil && candidate.perms.CriticalOptions != nil && candidate.perms.CriticalOptions[sourceAddressCriticalOption] != "" {
candidate.result = checkSourceAddress(
// If PublicKeyCallback returns ErrPartialSuccess we need to check source address
// and update the returned error if this check fails
if (candidate.result == nil || candidate.result == ErrPartialSuccess) && candidate.perms != nil && candidate.perms.CriticalOptions != nil && candidate.perms.CriticalOptions[sourceAddressCriticalOption] != "" {
err = checkSourceAddress(
s.RemoteAddr(),
candidate.perms.CriticalOptions[sourceAddressCriticalOption])
// We need to update candidate.result only if the source address check fails
if err != nil {
candidate.result = err
}
}
cache.add(candidate)
}
Expand All @@ -537,7 +580,8 @@ userAuthLoop:
return nil, parseError(msgUserAuthRequest)
}

if candidate.result == nil {
// a public key is ok if result is nil or partial success
if candidate.result == nil || candidate.result == ErrPartialSuccess {
okMsg := userAuthPubKeyOkMsg{
Algo: algo,
PubKey: pubKeyData,
Expand Down Expand Up @@ -645,6 +689,30 @@ userAuthLoop:
failureMsg.Methods = append(failureMsg.Methods, "gssapi-with-mic")
}

if authErr == ErrPartialSuccess && config.NextAuthMethodsCallback != nil {
// if the current callback returns partial success we update the list of methods
// that returned partial success and we call NextAuthMethodsCallback.
// We set the methods allowed to continue from the NextAuthMethodsCallback
// response, an empty list means no supported methods remain and so the
// multi-step auth fails
s.partialSuccessMethods = append(s.partialSuccessMethods, userAuthReq.Method)
allowedMethods := config.NextAuthMethodsCallback(s)
if len(allowedMethods) > 0 {
failureMsg.PartialSuccess = true
failureMsg.Methods = allowedMethods
// a partial success response isn't an auth failure
authFailures--
} else {
return nil, errors.New("ssh: no authentication methods can continue for Multi-Step Authentication")
}
} else if len(s.partialSuccessMethods) > 0 && config.NextAuthMethodsCallback != nil {
// If we are doing a multi-step auth and a step fails we must only return the
// allowed methods and not the ones already completed as required in RFC 4252.
// The application can use "PartialSuccessMethods()" available in ConnMetadata
// to know which authentication methods were already completed.
failureMsg.Methods = config.NextAuthMethodsCallback(s)
}

if len(failureMsg.Methods) == 0 {
return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
}
Expand Down
Loading