diff --git a/ssh/client_auth_test.go b/ssh/client_auth_test.go index b994b78..2e48342 100644 --- a/ssh/client_auth_test.go +++ b/ssh/client_auth_test.go @@ -1654,6 +1654,74 @@ func TestAuthMethodGSSAPIWithMIC(t *testing.T) { } } +type maliciousGSSAPIWithMICAuthMethod struct{} + +func (maliciousGSSAPIWithMICAuthMethod) method() string { + // Reuse an advertised auth method name so the client attempts this + // AuthMethod even when gssapi-with-mic is not advertised. + return "password" +} + +func (maliciousGSSAPIWithMICAuthMethod) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) { + req := &userAuthRequestMsg{ + User: user, + Service: serviceSSH, + Method: "gssapi-with-mic", + } + req.Payload = appendU32(req.Payload, 1) + req.Payload = appendString(req.Payload, string(krb5OID)) + if err := c.writePacket(Marshal(req)); err != nil { + return authFailure, nil, err + } + + 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 + default: + return authFailure, nil, unexpectedMessageError(msgUserAuthFailure, packet[0]) + } +} + +func TestGSSAPIWithMICPartialConfigRejectedAtRuntime(t *testing.T) { + config := &ClientConfig{ + User: "testuser", + Auth: []AuthMethod{ + maliciousGSSAPIWithMICAuthMethod{}, + }, + HostKeyCallback: InsecureIgnoreHostKey(), + } + + clientErr, serverErrs := tryAuthBothSides(t, config, &GSSAPIWithMICConfig{ + Server: &FakeServer{}, + }) + + if clientErr == nil || !strings.Contains(clientErr.Error(), "ssh: unable to authenticate") { + t.Fatalf("client got %v, want authentication failure", clientErr) + } + + found := false + for _, err := range serverErrs { + if err != nil && strings.Contains(err.Error(), "ssh: gssapi-with-mic auth not configured") { + found = true + break + } + } + if !found { + t.Fatalf("server auth errors = %v, want gssapi-with-mic auth not configured", serverErrs) + } +} + func TestCompatibleAlgoAndSignatures(t *testing.T) { type testcase struct { algo string diff --git a/ssh/server.go b/ssh/server.go index 9292f0b..3c0fcc9 100644 --- a/ssh/server.go +++ b/ssh/server.go @@ -54,6 +54,9 @@ type Permissions struct { ExtraData map[any]any } +// GSSAPIWithMICConfig includes the server callbacks for gssapi-with-mic +// authentication. If either field is nil, gssapi-with-mic is considered not +// configured. type GSSAPIWithMICConfig struct { // AllowLogin, must be set, is called when gssapi-with-mic // authentication is selected (RFC 4462 section 3). The srcName is from the @@ -68,6 +71,10 @@ type GSSAPIWithMICConfig struct { Server GSSAPIServer } +func gssapiWithMICConfigured(config *GSSAPIWithMICConfig) bool { + return config != nil && config.AllowLogin != nil && config.Server != nil +} + // SendAuthBanner implements [ServerPreAuthConn]. func (s *connection) SendAuthBanner(msg string) error { return s.transport.writePacket(Marshal(&userAuthBannerMsg{ @@ -382,8 +389,7 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error) } if !config.NoClientAuth && config.PasswordCallback == nil && config.PublicKeyCallback == nil && - config.KeyboardInteractiveCallback == nil && (config.GSSAPIWithMICConfig == nil || - config.GSSAPIWithMICConfig.AllowLogin == nil || config.GSSAPIWithMICConfig.Server == nil) { + config.KeyboardInteractiveCallback == nil && !gssapiWithMICConfigured(config.GSSAPIWithMICConfig) { return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false") } @@ -869,7 +875,7 @@ userAuthLoop: } } case "gssapi-with-mic": - if authConfig.GSSAPIWithMICConfig == nil { + if !gssapiWithMICConfigured(authConfig.GSSAPIWithMICConfig) { authErr = errors.New("ssh: gssapi-with-mic auth not configured") break } @@ -1002,8 +1008,7 @@ userAuthLoop: if authConfig.KeyboardInteractiveCallback != nil { failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive") } - if authConfig.GSSAPIWithMICConfig != nil && authConfig.GSSAPIWithMICConfig.Server != nil && - authConfig.GSSAPIWithMICConfig.AllowLogin != nil { + if gssapiWithMICConfigured(authConfig.GSSAPIWithMICConfig) { failureMsg.Methods = append(failureMsg.Methods, "gssapi-with-mic") }