|
@@ -59,6 +59,27 @@ type GSSAPIWithMICConfig struct {
|
|
|
Server GSSAPIServer
|
|
Server GSSAPIServer
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// SendAuthBanner implements [ServerPreAuthConn].
|
|
|
|
|
+func (s *connection) SendAuthBanner(msg string) error {
|
|
|
|
|
+ return s.transport.writePacket(Marshal(&userAuthBannerMsg{
|
|
|
|
|
+ Message: msg,
|
|
|
|
|
+ }))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (*connection) unexportedMethodForFutureProofing() {}
|
|
|
|
|
+
|
|
|
|
|
+// ServerPreAuthConn is the interface available on an incoming server
|
|
|
|
|
+// connection before authentication has completed.
|
|
|
|
|
+type ServerPreAuthConn interface {
|
|
|
|
|
+ unexportedMethodForFutureProofing() // permits growing ServerPreAuthConn safely later, ala testing.TB
|
|
|
|
|
+
|
|
|
|
|
+ ConnMetadata
|
|
|
|
|
+
|
|
|
|
|
+ // SendAuthBanner sends a banner message to the client.
|
|
|
|
|
+ // It returns an error once the authentication phase has ended.
|
|
|
|
|
+ SendAuthBanner(string) error
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ServerConfig holds server specific configuration data.
|
|
// ServerConfig holds server specific configuration data.
|
|
|
type ServerConfig struct {
|
|
type ServerConfig struct {
|
|
|
// Config contains configuration shared between client and server.
|
|
// Config contains configuration shared between client and server.
|
|
@@ -118,6 +139,12 @@ type ServerConfig struct {
|
|
|
// attempts.
|
|
// attempts.
|
|
|
AuthLogCallback func(conn ConnMetadata, method string, err error)
|
|
AuthLogCallback func(conn ConnMetadata, method string, err error)
|
|
|
|
|
|
|
|
|
|
+ // PreAuthConnCallback, if non-nil, is called upon receiving a new connection
|
|
|
|
|
+ // before any authentication has started. The provided ServerPreAuthConn
|
|
|
|
|
+ // can be used at any time before authentication is complete, including
|
|
|
|
|
+ // after this callback has returned.
|
|
|
|
|
+ PreAuthConnCallback func(ServerPreAuthConn)
|
|
|
|
|
+
|
|
|
// ServerVersion is the version identification string to announce in
|
|
// ServerVersion is the version identification string to announce in
|
|
|
// the public handshake.
|
|
// the public handshake.
|
|
|
// If empty, a reasonable default is used.
|
|
// If empty, a reasonable default is used.
|
|
@@ -149,7 +176,7 @@ func (s *ServerConfig) AddHostKey(key Signer) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// cachedPubKey contains the results of querying whether a public key is
|
|
// cachedPubKey contains the results of querying whether a public key is
|
|
|
-// acceptable for a user.
|
|
|
|
|
|
|
+// acceptable for a user. This is a FIFO cache.
|
|
|
type cachedPubKey struct {
|
|
type cachedPubKey struct {
|
|
|
user string
|
|
user string
|
|
|
pubKeyData []byte
|
|
pubKeyData []byte
|
|
@@ -157,7 +184,13 @@ type cachedPubKey struct {
|
|
|
perms *Permissions
|
|
perms *Permissions
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const maxCachedPubKeys = 16
|
|
|
|
|
|
|
+// maxCachedPubKeys is the number of cache entries we store.
|
|
|
|
|
+//
|
|
|
|
|
+// Due to consistent misuse of the PublicKeyCallback API, we have reduced this
|
|
|
|
|
+// to 1, such that the only key in the cache is the most recently seen one. This
|
|
|
|
|
+// forces the behavior that the last call to PublicKeyCallback will always be
|
|
|
|
|
+// with the key that is used for authentication.
|
|
|
|
|
+const maxCachedPubKeys = 1
|
|
|
|
|
|
|
|
// pubKeyCache caches tests for public keys. Since SSH clients
|
|
// pubKeyCache caches tests for public keys. Since SSH clients
|
|
|
// will query whether a public key is acceptable before attempting to
|
|
// will query whether a public key is acceptable before attempting to
|
|
@@ -179,9 +212,10 @@ func (c *pubKeyCache) get(user string, pubKeyData []byte) (cachedPubKey, bool) {
|
|
|
|
|
|
|
|
// add adds the given tuple to the cache.
|
|
// add adds the given tuple to the cache.
|
|
|
func (c *pubKeyCache) add(candidate cachedPubKey) {
|
|
func (c *pubKeyCache) add(candidate cachedPubKey) {
|
|
|
- if len(c.keys) < maxCachedPubKeys {
|
|
|
|
|
- c.keys = append(c.keys, candidate)
|
|
|
|
|
|
|
+ if len(c.keys) >= maxCachedPubKeys {
|
|
|
|
|
+ c.keys = c.keys[1:]
|
|
|
}
|
|
}
|
|
|
|
|
+ c.keys = append(c.keys, candidate)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ServerConn is an authenticated SSH connection, as seen from the
|
|
// ServerConn is an authenticated SSH connection, as seen from the
|
|
@@ -426,6 +460,35 @@ func (l ServerAuthError) Error() string {
|
|
|
return "[" + strings.Join(errs, ", ") + "]"
|
|
return "[" + strings.Join(errs, ", ") + "]"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ServerAuthCallbacks defines server-side authentication callbacks.
|
|
|
|
|
+type ServerAuthCallbacks struct {
|
|
|
|
|
+ // PasswordCallback behaves like [ServerConfig.PasswordCallback].
|
|
|
|
|
+ PasswordCallback func(conn ConnMetadata, password []byte) (*Permissions, error)
|
|
|
|
|
+
|
|
|
|
|
+ // PublicKeyCallback behaves like [ServerConfig.PublicKeyCallback].
|
|
|
|
|
+ PublicKeyCallback func(conn ConnMetadata, key PublicKey) (*Permissions, error)
|
|
|
|
|
+
|
|
|
|
|
+ // KeyboardInteractiveCallback behaves like [ServerConfig.KeyboardInteractiveCallback].
|
|
|
|
|
+ KeyboardInteractiveCallback func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error)
|
|
|
|
|
+
|
|
|
|
|
+ // GSSAPIWithMICConfig behaves like [ServerConfig.GSSAPIWithMICConfig].
|
|
|
|
|
+ GSSAPIWithMICConfig *GSSAPIWithMICConfig
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// PartialSuccessError can be returned by any of the [ServerConfig]
|
|
|
|
|
+// authentication callbacks to indicate to the client that authentication has
|
|
|
|
|
+// partially succeeded, but further steps are required.
|
|
|
|
|
+type PartialSuccessError struct {
|
|
|
|
|
+ // Next defines the authentication callbacks to apply to further steps. The
|
|
|
|
|
+ // available methods communicated to the client are based on the non-nil
|
|
|
|
|
+ // ServerAuthCallbacks fields.
|
|
|
|
|
+ Next ServerAuthCallbacks
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (p *PartialSuccessError) Error() string {
|
|
|
|
|
+ return "ssh: authenticated with partial success"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ErrNoAuth is the error value returned if no
|
|
// ErrNoAuth is the error value returned if no
|
|
|
// authentication method has been passed yet. This happens as a normal
|
|
// authentication method has been passed yet. This happens as a normal
|
|
|
// part of the authentication loop, since the client first tries
|
|
// part of the authentication loop, since the client first tries
|
|
@@ -433,14 +496,46 @@ func (l ServerAuthError) Error() string {
|
|
|
// It is returned in ServerAuthError.Errors from NewServerConn.
|
|
// It is returned in ServerAuthError.Errors from NewServerConn.
|
|
|
var ErrNoAuth = errors.New("ssh: no auth passed yet")
|
|
var ErrNoAuth = errors.New("ssh: no auth passed yet")
|
|
|
|
|
|
|
|
|
|
+// BannerError is an error that can be returned by authentication handlers in
|
|
|
|
|
+// ServerConfig to send a banner message to the client.
|
|
|
|
|
+type BannerError struct {
|
|
|
|
|
+ Err error
|
|
|
|
|
+ Message string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (b *BannerError) Unwrap() error {
|
|
|
|
|
+ return b.Err
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (b *BannerError) Error() string {
|
|
|
|
|
+ if b.Err == nil {
|
|
|
|
|
+ return b.Message
|
|
|
|
|
+ }
|
|
|
|
|
+ return b.Err.Error()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, error) {
|
|
func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, error) {
|
|
|
|
|
+ if config.PreAuthConnCallback != nil {
|
|
|
|
|
+ config.PreAuthConnCallback(s)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
sessionID := s.transport.getSessionID()
|
|
sessionID := s.transport.getSessionID()
|
|
|
var cache pubKeyCache
|
|
var cache pubKeyCache
|
|
|
var perms *Permissions
|
|
var perms *Permissions
|
|
|
|
|
|
|
|
authFailures := 0
|
|
authFailures := 0
|
|
|
|
|
+ noneAuthCount := 0
|
|
|
var authErrs []error
|
|
var authErrs []error
|
|
|
- var displayedBanner bool
|
|
|
|
|
|
|
+ var calledBannerCallback bool
|
|
|
|
|
+ partialSuccessReturned := false
|
|
|
|
|
+ // Set the initial authentication callbacks from the config. They can be
|
|
|
|
|
+ // changed if a PartialSuccessError is returned.
|
|
|
|
|
+ authConfig := ServerAuthCallbacks{
|
|
|
|
|
+ PasswordCallback: config.PasswordCallback,
|
|
|
|
|
+ PublicKeyCallback: config.PublicKeyCallback,
|
|
|
|
|
+ KeyboardInteractiveCallback: config.KeyboardInteractiveCallback,
|
|
|
|
|
+ GSSAPIWithMICConfig: config.GSSAPIWithMICConfig,
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
userAuthLoop:
|
|
userAuthLoop:
|
|
|
for {
|
|
for {
|
|
@@ -453,8 +548,8 @@ userAuthLoop:
|
|
|
if err := s.transport.writePacket(Marshal(discMsg)); err != nil {
|
|
if err := s.transport.writePacket(Marshal(discMsg)); err != nil {
|
|
|
return nil, err
|
|
return nil, err
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return nil, discMsg
|
|
|
|
|
|
|
+ authErrs = append(authErrs, discMsg)
|
|
|
|
|
+ return nil, &ServerAuthError{Errors: authErrs}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var userAuthReq userAuthRequestMsg
|
|
var userAuthReq userAuthRequestMsg
|
|
@@ -471,16 +566,17 @@ userAuthLoop:
|
|
|
return nil, errors.New("ssh: client attempted to negotiate for unknown service: " + userAuthReq.Service)
|
|
return nil, errors.New("ssh: client attempted to negotiate for unknown service: " + userAuthReq.Service)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if s.user != userAuthReq.User && partialSuccessReturned {
|
|
|
|
|
+ return nil, fmt.Errorf("ssh: client changed the user after a partial success authentication, previous user %q, current user %q",
|
|
|
|
|
+ s.user, userAuthReq.User)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
s.user = userAuthReq.User
|
|
s.user = userAuthReq.User
|
|
|
|
|
|
|
|
- if !displayedBanner && config.BannerCallback != nil {
|
|
|
|
|
- displayedBanner = true
|
|
|
|
|
- msg := config.BannerCallback(s)
|
|
|
|
|
- if msg != "" {
|
|
|
|
|
- bannerMsg := &userAuthBannerMsg{
|
|
|
|
|
- Message: msg,
|
|
|
|
|
- }
|
|
|
|
|
- if err := s.transport.writePacket(Marshal(bannerMsg)); err != nil {
|
|
|
|
|
|
|
+ if !calledBannerCallback && config.BannerCallback != nil {
|
|
|
|
|
+ calledBannerCallback = true
|
|
|
|
|
+ if msg := config.BannerCallback(s); msg != "" {
|
|
|
|
|
+ if err := s.SendAuthBanner(msg); err != nil {
|
|
|
return nil, err
|
|
return nil, err
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -491,20 +587,18 @@ userAuthLoop:
|
|
|
|
|
|
|
|
switch userAuthReq.Method {
|
|
switch userAuthReq.Method {
|
|
|
case "none":
|
|
case "none":
|
|
|
- if config.NoClientAuth {
|
|
|
|
|
|
|
+ noneAuthCount++
|
|
|
|
|
+ // We don't allow none authentication after a partial success
|
|
|
|
|
+ // response.
|
|
|
|
|
+ if config.NoClientAuth && !partialSuccessReturned {
|
|
|
if config.NoClientAuthCallback != nil {
|
|
if config.NoClientAuthCallback != nil {
|
|
|
perms, authErr = config.NoClientAuthCallback(s)
|
|
perms, authErr = config.NoClientAuthCallback(s)
|
|
|
} else {
|
|
} else {
|
|
|
authErr = nil
|
|
authErr = nil
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // allow initial attempt of 'none' without penalty
|
|
|
|
|
- if authFailures == 0 {
|
|
|
|
|
- authFailures--
|
|
|
|
|
- }
|
|
|
|
|
case "password":
|
|
case "password":
|
|
|
- if config.PasswordCallback == nil {
|
|
|
|
|
|
|
+ if authConfig.PasswordCallback == nil {
|
|
|
authErr = errors.New("ssh: password auth not configured")
|
|
authErr = errors.New("ssh: password auth not configured")
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
@@ -518,17 +612,17 @@ userAuthLoop:
|
|
|
return nil, parseError(msgUserAuthRequest)
|
|
return nil, parseError(msgUserAuthRequest)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- perms, authErr = config.PasswordCallback(s, password)
|
|
|
|
|
|
|
+ perms, authErr = authConfig.PasswordCallback(s, password)
|
|
|
case "keyboard-interactive":
|
|
case "keyboard-interactive":
|
|
|
- if config.KeyboardInteractiveCallback == nil {
|
|
|
|
|
|
|
+ if authConfig.KeyboardInteractiveCallback == nil {
|
|
|
authErr = errors.New("ssh: keyboard-interactive auth not configured")
|
|
authErr = errors.New("ssh: keyboard-interactive auth not configured")
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
prompter := &sshClientKeyboardInteractive{s}
|
|
prompter := &sshClientKeyboardInteractive{s}
|
|
|
- perms, authErr = config.KeyboardInteractiveCallback(s, prompter.Challenge)
|
|
|
|
|
|
|
+ perms, authErr = authConfig.KeyboardInteractiveCallback(s, prompter.Challenge)
|
|
|
case "publickey":
|
|
case "publickey":
|
|
|
- if config.PublicKeyCallback == nil {
|
|
|
|
|
|
|
+ if authConfig.PublicKeyCallback == nil {
|
|
|
authErr = errors.New("ssh: publickey auth not configured")
|
|
authErr = errors.New("ssh: publickey auth not configured")
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
@@ -562,11 +656,18 @@ userAuthLoop:
|
|
|
if !ok {
|
|
if !ok {
|
|
|
candidate.user = s.user
|
|
candidate.user = s.user
|
|
|
candidate.pubKeyData = pubKeyData
|
|
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(
|
|
|
|
|
|
|
+ candidate.perms, candidate.result = authConfig.PublicKeyCallback(s, pubKey)
|
|
|
|
|
+ _, isPartialSuccessError := candidate.result.(*PartialSuccessError)
|
|
|
|
|
+
|
|
|
|
|
+ if (candidate.result == nil || isPartialSuccessError) &&
|
|
|
|
|
+ candidate.perms != nil &&
|
|
|
|
|
+ candidate.perms.CriticalOptions != nil &&
|
|
|
|
|
+ candidate.perms.CriticalOptions[sourceAddressCriticalOption] != "" {
|
|
|
|
|
+ if err := checkSourceAddress(
|
|
|
s.RemoteAddr(),
|
|
s.RemoteAddr(),
|
|
|
- candidate.perms.CriticalOptions[sourceAddressCriticalOption])
|
|
|
|
|
|
|
+ candidate.perms.CriticalOptions[sourceAddressCriticalOption]); err != nil {
|
|
|
|
|
+ candidate.result = err
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
cache.add(candidate)
|
|
cache.add(candidate)
|
|
|
}
|
|
}
|
|
@@ -578,8 +679,8 @@ userAuthLoop:
|
|
|
if len(payload) > 0 {
|
|
if len(payload) > 0 {
|
|
|
return nil, parseError(msgUserAuthRequest)
|
|
return nil, parseError(msgUserAuthRequest)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- if candidate.result == nil {
|
|
|
|
|
|
|
+ _, isPartialSuccessError := candidate.result.(*PartialSuccessError)
|
|
|
|
|
+ if candidate.result == nil || isPartialSuccessError {
|
|
|
okMsg := userAuthPubKeyOkMsg{
|
|
okMsg := userAuthPubKeyOkMsg{
|
|
|
Algo: algo,
|
|
Algo: algo,
|
|
|
PubKey: pubKeyData,
|
|
PubKey: pubKeyData,
|
|
@@ -629,11 +730,11 @@ userAuthLoop:
|
|
|
perms = candidate.perms
|
|
perms = candidate.perms
|
|
|
}
|
|
}
|
|
|
case "gssapi-with-mic":
|
|
case "gssapi-with-mic":
|
|
|
- if config.GSSAPIWithMICConfig == nil {
|
|
|
|
|
|
|
+ if authConfig.GSSAPIWithMICConfig == nil {
|
|
|
authErr = errors.New("ssh: gssapi-with-mic auth not configured")
|
|
authErr = errors.New("ssh: gssapi-with-mic auth not configured")
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
- gssapiConfig := config.GSSAPIWithMICConfig
|
|
|
|
|
|
|
+ gssapiConfig := authConfig.GSSAPIWithMICConfig
|
|
|
userAuthRequestGSSAPI, err := parseGSSAPIPayload(userAuthReq.Payload)
|
|
userAuthRequestGSSAPI, err := parseGSSAPIPayload(userAuthReq.Payload)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
return nil, parseError(msgUserAuthRequest)
|
|
return nil, parseError(msgUserAuthRequest)
|
|
@@ -685,53 +786,83 @@ userAuthLoop:
|
|
|
config.AuthLogCallback(s, userAuthReq.Method, authErr)
|
|
config.AuthLogCallback(s, userAuthReq.Method, authErr)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ var bannerErr *BannerError
|
|
|
|
|
+ if errors.As(authErr, &bannerErr) {
|
|
|
|
|
+ if bannerErr.Message != "" {
|
|
|
|
|
+ if err := s.SendAuthBanner(bannerErr.Message); err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if authErr == nil {
|
|
if authErr == nil {
|
|
|
break userAuthLoop
|
|
break userAuthLoop
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- authFailures++
|
|
|
|
|
- if config.MaxAuthTries > 0 && authFailures >= config.MaxAuthTries {
|
|
|
|
|
- // If we have hit the max attempts, don't bother sending the
|
|
|
|
|
- // final SSH_MSG_USERAUTH_FAILURE message, since there are
|
|
|
|
|
- // no more authentication methods which can be attempted,
|
|
|
|
|
- // and this message may cause the client to re-attempt
|
|
|
|
|
- // authentication while we send the disconnect message.
|
|
|
|
|
- // Continue, and trigger the disconnect at the start of
|
|
|
|
|
- // the loop.
|
|
|
|
|
- //
|
|
|
|
|
- // The SSH specification is somewhat confusing about this,
|
|
|
|
|
- // RFC 4252 Section 5.1 requires each authentication failure
|
|
|
|
|
- // be responded to with a respective SSH_MSG_USERAUTH_FAILURE
|
|
|
|
|
- // message, but Section 4 says the server should disconnect
|
|
|
|
|
- // after some number of attempts, but it isn't explicit which
|
|
|
|
|
- // message should take precedence (i.e. should there be a failure
|
|
|
|
|
- // message than a disconnect message, or if we are going to
|
|
|
|
|
- // disconnect, should we only send that message.)
|
|
|
|
|
- //
|
|
|
|
|
- // Either way, OpenSSH disconnects immediately after the last
|
|
|
|
|
- // failed authnetication attempt, and given they are typically
|
|
|
|
|
- // considered the golden implementation it seems reasonable
|
|
|
|
|
- // to match that behavior.
|
|
|
|
|
- continue
|
|
|
|
|
|
|
+ var failureMsg userAuthFailureMsg
|
|
|
|
|
+
|
|
|
|
|
+ if partialSuccess, ok := authErr.(*PartialSuccessError); ok {
|
|
|
|
|
+ // After a partial success error we don't allow changing the user
|
|
|
|
|
+ // name and execute the NoClientAuthCallback.
|
|
|
|
|
+ partialSuccessReturned = true
|
|
|
|
|
+
|
|
|
|
|
+ // In case a partial success is returned, the server may send
|
|
|
|
|
+ // a new set of authentication methods.
|
|
|
|
|
+ authConfig = partialSuccess.Next
|
|
|
|
|
+
|
|
|
|
|
+ // Reset pubkey cache, as the new PublicKeyCallback might
|
|
|
|
|
+ // accept a different set of public keys.
|
|
|
|
|
+ cache = pubKeyCache{}
|
|
|
|
|
+
|
|
|
|
|
+ // Send back a partial success message to the user.
|
|
|
|
|
+ failureMsg.PartialSuccess = true
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Allow initial attempt of 'none' without penalty.
|
|
|
|
|
+ if authFailures > 0 || userAuthReq.Method != "none" || noneAuthCount != 1 {
|
|
|
|
|
+ authFailures++
|
|
|
|
|
+ }
|
|
|
|
|
+ if config.MaxAuthTries > 0 && authFailures >= config.MaxAuthTries {
|
|
|
|
|
+ // If we have hit the max attempts, don't bother sending the
|
|
|
|
|
+ // final SSH_MSG_USERAUTH_FAILURE message, since there are
|
|
|
|
|
+ // no more authentication methods which can be attempted,
|
|
|
|
|
+ // and this message may cause the client to re-attempt
|
|
|
|
|
+ // authentication while we send the disconnect message.
|
|
|
|
|
+ // Continue, and trigger the disconnect at the start of
|
|
|
|
|
+ // the loop.
|
|
|
|
|
+ //
|
|
|
|
|
+ // The SSH specification is somewhat confusing about this,
|
|
|
|
|
+ // RFC 4252 Section 5.1 requires each authentication failure
|
|
|
|
|
+ // be responded to with a respective SSH_MSG_USERAUTH_FAILURE
|
|
|
|
|
+ // message, but Section 4 says the server should disconnect
|
|
|
|
|
+ // after some number of attempts, but it isn't explicit which
|
|
|
|
|
+ // message should take precedence (i.e. should there be a failure
|
|
|
|
|
+ // message than a disconnect message, or if we are going to
|
|
|
|
|
+ // disconnect, should we only send that message.)
|
|
|
|
|
+ //
|
|
|
|
|
+ // Either way, OpenSSH disconnects immediately after the last
|
|
|
|
|
+ // failed authentication attempt, and given they are typically
|
|
|
|
|
+ // considered the golden implementation it seems reasonable
|
|
|
|
|
+ // to match that behavior.
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- var failureMsg userAuthFailureMsg
|
|
|
|
|
- if config.PasswordCallback != nil {
|
|
|
|
|
|
|
+ if authConfig.PasswordCallback != nil {
|
|
|
failureMsg.Methods = append(failureMsg.Methods, "password")
|
|
failureMsg.Methods = append(failureMsg.Methods, "password")
|
|
|
}
|
|
}
|
|
|
- if config.PublicKeyCallback != nil {
|
|
|
|
|
|
|
+ if authConfig.PublicKeyCallback != nil {
|
|
|
failureMsg.Methods = append(failureMsg.Methods, "publickey")
|
|
failureMsg.Methods = append(failureMsg.Methods, "publickey")
|
|
|
}
|
|
}
|
|
|
- if config.KeyboardInteractiveCallback != nil {
|
|
|
|
|
|
|
+ if authConfig.KeyboardInteractiveCallback != nil {
|
|
|
failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive")
|
|
failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive")
|
|
|
}
|
|
}
|
|
|
- if config.GSSAPIWithMICConfig != nil && config.GSSAPIWithMICConfig.Server != nil &&
|
|
|
|
|
- config.GSSAPIWithMICConfig.AllowLogin != nil {
|
|
|
|
|
|
|
+ if authConfig.GSSAPIWithMICConfig != nil && authConfig.GSSAPIWithMICConfig.Server != nil &&
|
|
|
|
|
+ authConfig.GSSAPIWithMICConfig.AllowLogin != nil {
|
|
|
failureMsg.Methods = append(failureMsg.Methods, "gssapi-with-mic")
|
|
failureMsg.Methods = append(failureMsg.Methods, "gssapi-with-mic")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if len(failureMsg.Methods) == 0 {
|
|
if len(failureMsg.Methods) == 0 {
|
|
|
- return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
|
|
|
|
|
|
|
+ return nil, errors.New("ssh: no authentication methods available")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if err := s.transport.writePacket(Marshal(&failureMsg)); err != nil {
|
|
if err := s.transport.writePacket(Marshal(&failureMsg)); err != nil {
|