ssh/gss: support kerberos authentication for ssh server and client

Change-Id: I20e3356476dc50402dd34d2b39ad030c1e63a9ef
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/170919
Run-TryBot: Han-Wen Nienhuys <hanwen@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Han-Wen Nienhuys <hanwen@google.com>
diff --git a/ssh/client_auth.go b/ssh/client_auth.go
index 5f44b77..0590070 100644
--- a/ssh/client_auth.go
+++ b/ssh/client_auth.go
@@ -523,3 +523,117 @@
 func RetryableAuthMethod(auth AuthMethod, maxTries int) AuthMethod {
 	return &retryableAuthMethod{authMethod: auth, maxTries: maxTries}
 }
+
+// GSSAPIWithMICAuthMethod is an AuthMethod with "gssapi-with-mic" authentication.
+// See RFC 4462 section 3
+// gssAPIClient is implementation of the GSSAPIClient interface, see the definition of the interface for details.
+// target is the server host you want to log in to.
+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) (authResult, []string, error) {
+	m := &userAuthRequestMsg{
+		User:    user,
+		Service: serviceSSH,
+		Method:  g.method(),
+	}
+	// The GSS-API authentication method is initiated when the client sends an SSH_MSG_USERAUTH_REQUEST.
+	// See RFC 4462 section 3.2.
+	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
+	}
+	// The server responds to the SSH_MSG_USERAUTH_REQUEST with either an
+	// SSH_MSG_USERAUTH_FAILURE if none of the mechanisms are supported or
+	// with an SSH_MSG_USERAUTH_GSSAPI_RESPONSE.
+	// See RFC 4462 section 3.3.
+	// OpenSSH supports Kerberos V5 mechanism only for GSS-API authentication,so I don't want to check
+	// selected mech if it is valid.
+	packet, err := c.readPacket()
+	if err != nil {
+		return authFailure, nil, err
+	}
+	userAuthGSSAPIResp := &userAuthGSSAPIResponse{}
+	if err := Unmarshal(packet, userAuthGSSAPIResp); err != nil {
+		return authFailure, nil, err
+	}
+	// Start the loop into the exchange token.
+	// See RFC 4462 section 3.4.
+	var token []byte
+	defer g.gssAPIClient.DeleteSecContext()
+	for {
+		// Initiates the establishment of a security context between the application and a remote peer.
+		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
+		}
+	}
+	// Binding Encryption Keys.
+	// See RFC 4462 section 3.5.
+	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"
+}
diff --git a/ssh/client_auth_test.go b/ssh/client_auth_test.go
index ae60b9f..9200cb3 100644
--- a/ssh/client_auth_test.go
+++ b/ssh/client_auth_test.go
@@ -33,12 +33,19 @@
 // tryAuth runs a handshake with a given config against an SSH server
 // with config serverConfig. Returns both client and server side errors.
 func tryAuth(t *testing.T, config *ClientConfig) error {
-	err, _ := tryAuthBothSides(t, config)
+	err, _ := tryAuthBothSides(t, config, nil)
+	return err
+}
+
+// tryAuth runs a handshake with a given config against an SSH server
+// with a given GSSAPIWithMICConfig and config serverConfig. Returns both client and server side errors.
+func tryAuthWithGSSAPIWithMICConfig(t *testing.T, clientConfig *ClientConfig, gssAPIWithMICConfig *GSSAPIWithMICConfig) error {
+	err, _ := tryAuthBothSides(t, clientConfig, gssAPIWithMICConfig)
 	return err
 }
 
 // tryAuthBothSides runs the handshake and returns the resulting errors from both sides of the connection.
-func tryAuthBothSides(t *testing.T, config *ClientConfig) (clientError error, serverAuthErrors []error) {
+func tryAuthBothSides(t *testing.T, config *ClientConfig, gssAPIWithMICConfig *GSSAPIWithMICConfig) (clientError error, serverAuthErrors []error) {
 	c1, c2, err := netPipe()
 	if err != nil {
 		t.Fatalf("netPipe: %v", err)
@@ -61,7 +68,6 @@
 			return c.Serial == 666
 		},
 	}
-
 	serverConfig := &ServerConfig{
 		PasswordCallback: func(conn ConnMetadata, pass []byte) (*Permissions, error) {
 			if conn.User() == "testuser" && string(pass) == clientPassword {
@@ -85,6 +91,7 @@
 			}
 			return nil, errors.New("keyboard-interactive failed")
 		},
+		GSSAPIWithMICConfig: gssAPIWithMICConfig,
 	}
 	serverConfig.AddHostKey(testSigners["rsa"])
 
@@ -247,7 +254,7 @@
 		HostKeyCallback: InsecureIgnoreHostKey(),
 	}
 
-	err, serverErrors := tryAuthBothSides(t, config)
+	err, serverErrors := tryAuthBothSides(t, config, nil)
 	if err == nil {
 		t.Fatalf("login succeeded")
 	}
@@ -686,3 +693,206 @@
 		}
 	}
 }
+
+func TestAuthMethodGSSAPIWithMIC(t *testing.T) {
+	type testcase struct {
+		config        *ClientConfig
+		gssConfig     *GSSAPIWithMICConfig
+		clientWantErr string
+		serverWantErr string
+	}
+	testcases := []*testcase{
+		{
+			config: &ClientConfig{
+				User: "testuser",
+				Auth: []AuthMethod{
+					GSSAPIWithMICAuthMethod(
+						&FakeClient{
+							exchanges: []*exchange{
+								{
+									outToken: "client-valid-token-1",
+								},
+								{
+									expectedToken: "server-valid-token-1",
+								},
+							},
+							mic:      []byte("valid-mic"),
+							maxRound: 2,
+						}, "testtarget",
+					),
+				},
+				HostKeyCallback: InsecureIgnoreHostKey(),
+			},
+			gssConfig: &GSSAPIWithMICConfig{
+				AllowLogin: func(conn ConnMetadata, srcName string) (*Permissions, error) {
+					if srcName != conn.User()+"@DOMAIN" {
+						return nil, fmt.Errorf("srcName is %s, conn user is %s", srcName, conn.User())
+					}
+					return nil, nil
+				},
+				Server: &FakeServer{
+					exchanges: []*exchange{
+						{
+							outToken:      "server-valid-token-1",
+							expectedToken: "client-valid-token-1",
+						},
+					},
+					maxRound:    1,
+					expectedMIC: []byte("valid-mic"),
+					srcName:     "testuser@DOMAIN",
+				},
+			},
+		},
+		{
+			config: &ClientConfig{
+				User: "testuser",
+				Auth: []AuthMethod{
+					GSSAPIWithMICAuthMethod(
+						&FakeClient{
+							exchanges: []*exchange{
+								{
+									outToken: "client-valid-token-1",
+								},
+								{
+									expectedToken: "server-valid-token-1",
+								},
+							},
+							mic:      []byte("valid-mic"),
+							maxRound: 2,
+						}, "testtarget",
+					),
+				},
+				HostKeyCallback: InsecureIgnoreHostKey(),
+			},
+			gssConfig: &GSSAPIWithMICConfig{
+				AllowLogin: func(conn ConnMetadata, srcName string) (*Permissions, error) {
+					return nil, fmt.Errorf("user is not allowed to login")
+				},
+				Server: &FakeServer{
+					exchanges: []*exchange{
+						{
+							outToken:      "server-valid-token-1",
+							expectedToken: "client-valid-token-1",
+						},
+					},
+					maxRound:    1,
+					expectedMIC: []byte("valid-mic"),
+					srcName:     "testuser@DOMAIN",
+				},
+			},
+			serverWantErr: "user is not allowed to login",
+			clientWantErr: "ssh: handshake failed: ssh: unable to authenticate",
+		},
+		{
+			config: &ClientConfig{
+				User: "testuser",
+				Auth: []AuthMethod{
+					GSSAPIWithMICAuthMethod(
+						&FakeClient{
+							exchanges: []*exchange{
+								{
+									outToken: "client-valid-token-1",
+								},
+								{
+									expectedToken: "server-valid-token-1",
+								},
+							},
+							mic:      []byte("valid-mic"),
+							maxRound: 2,
+						}, "testtarget",
+					),
+				},
+				HostKeyCallback: InsecureIgnoreHostKey(),
+			},
+			gssConfig: &GSSAPIWithMICConfig{
+				AllowLogin: func(conn ConnMetadata, srcName string) (*Permissions, error) {
+					if srcName != conn.User() {
+						return nil, fmt.Errorf("srcName is %s, conn user is %s", srcName, conn.User())
+					}
+					return nil, nil
+				},
+				Server: &FakeServer{
+					exchanges: []*exchange{
+						{
+							outToken:      "server-invalid-token-1",
+							expectedToken: "client-valid-token-1",
+						},
+					},
+					maxRound:    1,
+					expectedMIC: []byte("valid-mic"),
+					srcName:     "testuser@DOMAIN",
+				},
+			},
+			clientWantErr: "ssh: handshake failed: got \"server-invalid-token-1\", want token \"server-valid-token-1\"",
+		},
+		{
+			config: &ClientConfig{
+				User: "testuser",
+				Auth: []AuthMethod{
+					GSSAPIWithMICAuthMethod(
+						&FakeClient{
+							exchanges: []*exchange{
+								{
+									outToken: "client-valid-token-1",
+								},
+								{
+									expectedToken: "server-valid-token-1",
+								},
+							},
+							mic:      []byte("invalid-mic"),
+							maxRound: 2,
+						}, "testtarget",
+					),
+				},
+				HostKeyCallback: InsecureIgnoreHostKey(),
+			},
+			gssConfig: &GSSAPIWithMICConfig{
+				AllowLogin: func(conn ConnMetadata, srcName string) (*Permissions, error) {
+					if srcName != conn.User() {
+						return nil, fmt.Errorf("srcName is %s, conn user is %s", srcName, conn.User())
+					}
+					return nil, nil
+				},
+				Server: &FakeServer{
+					exchanges: []*exchange{
+						{
+							outToken:      "server-valid-token-1",
+							expectedToken: "client-valid-token-1",
+						},
+					},
+					maxRound:    1,
+					expectedMIC: []byte("valid-mic"),
+					srcName:     "testuser@DOMAIN",
+				},
+			},
+			serverWantErr: "got MICToken \"invalid-mic\", want \"valid-mic\"",
+			clientWantErr: "ssh: handshake failed: ssh: unable to authenticate",
+		},
+	}
+
+	for i, c := range testcases {
+		clientErr, serverErrs := tryAuthBothSides(t, c.config, c.gssConfig)
+		if (c.clientWantErr == "") != (clientErr == nil) {
+			t.Fatalf("client got %v, want %s, case %d", clientErr, c.clientWantErr, i)
+		}
+		if (c.serverWantErr == "") != (len(serverErrs) == 2 && serverErrs[1] == nil || len(serverErrs) == 1) {
+			t.Fatalf("server got err %v, want %s", serverErrs, c.serverWantErr)
+		}
+		if c.clientWantErr != "" {
+			if clientErr != nil && !strings.Contains(clientErr.Error(), c.clientWantErr) {
+				t.Fatalf("client  got %v, want %s, case %d", clientErr, c.clientWantErr, i)
+			}
+		}
+		found := false
+		var errStrings []string
+		if c.serverWantErr != "" {
+			for _, err := range serverErrs {
+				found = found || (err != nil && strings.Contains(err.Error(), c.serverWantErr))
+				errStrings = append(errStrings, err.Error())
+			}
+			if !found {
+				t.Errorf("server got error %q, want substring %q, case %d", errStrings, c.serverWantErr, i)
+			}
+		}
+	}
+}
diff --git a/ssh/messages.go b/ssh/messages.go
index 5ec42af..db914d8 100644
--- a/ssh/messages.go
+++ b/ssh/messages.go
@@ -275,6 +275,42 @@
 	PubKey []byte
 }
 
+// See RFC 4462, section 3
+const msgUserAuthGSSAPIResponse = 60
+
+type userAuthGSSAPIResponse struct {
+	SupportMech []byte `sshtype:"60"`
+}
+
+const msgUserAuthGSSAPIToken = 61
+
+type userAuthGSSAPIToken struct {
+	Token []byte `sshtype:"61"`
+}
+
+const msgUserAuthGSSAPIMIC = 66
+
+type userAuthGSSAPIMIC struct {
+	MIC []byte `sshtype:"66"`
+}
+
+// See RFC 4462, section 3.9
+const msgUserAuthGSSAPIErrTok = 64
+
+type userAuthGSSAPIErrTok struct {
+	ErrorToken []byte `sshtype:"64"`
+}
+
+// See RFC 4462, section 3.8
+const msgUserAuthGSSAPIError = 65
+
+type userAuthGSSAPIError struct {
+	MajorStatus uint32 `sshtype:"65"`
+	MinorStatus uint32
+	Message     string
+	LanguageTag string
+}
+
 // typeTags returns the possible type bytes for the given reflect.Type, which
 // should be a struct. The possible values are separated by a '|' character.
 func typeTags(structType reflect.Type) (tags []byte) {
@@ -756,6 +792,14 @@
 		msg = new(channelRequestSuccessMsg)
 	case msgChannelFailure:
 		msg = new(channelRequestFailureMsg)
+	case msgUserAuthGSSAPIToken:
+		msg = new(userAuthGSSAPIToken)
+	case msgUserAuthGSSAPIMIC:
+		msg = new(userAuthGSSAPIMIC)
+	case msgUserAuthGSSAPIErrTok:
+		msg = new(userAuthGSSAPIErrTok)
+	case msgUserAuthGSSAPIError:
+		msg = new(userAuthGSSAPIError)
 	default:
 		return nil, unexpectedMessageError(0, packet[0])
 	}
diff --git a/ssh/server.go b/ssh/server.go
index e86e896..ac7f807 100644
--- a/ssh/server.go
+++ b/ssh/server.go
@@ -45,6 +45,20 @@
 	Extensions map[string]string
 }
 
+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
+	// results of the GSS-API authentication. The format is username@DOMAIN.
+	// GSSAPI just guarantees to the server who the user is, but not if they can log in, and with what permissions.
+	// This callback is called after the user identity is established with GSSAPI to decide if the user can login with
+	// which permissions. If the user is allowed to login, it should return a nil error.
+	AllowLogin func(conn ConnMetadata, srcName string) (*Permissions, error)
+
+	// Server must be set. It's the implementation
+	// of the GSSAPIServer interface. See GSSAPIServer interface for details.
+	Server GSSAPIServer
+}
+
 // ServerConfig holds server specific configuration data.
 type ServerConfig struct {
 	// Config contains configuration shared between client and server.
@@ -99,6 +113,10 @@
 	// BannerCallback, if present, is called and the return string is sent to
 	// the client after key exchange completed but before authentication.
 	BannerCallback func(conn ConnMetadata) string
+
+	// GSSAPIWithMICConfig includes gssapi server and callback, which if both non-nil, is used
+	// when gssapi-with-mic authentication is selected (RFC 4462 section 3).
+	GSSAPIWithMICConfig *GSSAPIWithMICConfig
 }
 
 // AddHostKey adds a private key as a host key. If an existing host
@@ -204,7 +222,9 @@
 		return nil, errors.New("ssh: server has no host keys")
 	}
 
-	if !config.NoClientAuth && config.PasswordCallback == nil && config.PublicKeyCallback == nil && config.KeyboardInteractiveCallback == nil {
+	if !config.NoClientAuth && config.PasswordCallback == nil && config.PublicKeyCallback == nil &&
+		config.KeyboardInteractiveCallback == nil && (config.GSSAPIWithMICConfig == nil ||
+		config.GSSAPIWithMICConfig.AllowLogin == nil || config.GSSAPIWithMICConfig.Server == nil) {
 		return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
 	}
 
@@ -295,6 +315,55 @@
 	return fmt.Errorf("ssh: remote address %v is not allowed because of source-address restriction", addr)
 }
 
+func gssExchangeToken(gssapiConfig *GSSAPIWithMICConfig, firstToken []byte, s *connection,
+	sessionID []byte, userAuthReq userAuthRequestMsg) (authErr error, perms *Permissions, err error) {
+	gssAPIServer := gssapiConfig.Server
+	defer gssAPIServer.DeleteSecContext()
+	var srcName string
+	for {
+		var (
+			outToken     []byte
+			needContinue bool
+		)
+		outToken, srcName, needContinue, err = gssAPIServer.AcceptSecContext(firstToken)
+		if err != nil {
+			return err, nil, nil
+		}
+		if len(outToken) != 0 {
+			if err := s.transport.writePacket(Marshal(&userAuthGSSAPIToken{
+				Token: outToken,
+			})); err != nil {
+				return nil, nil, err
+			}
+		}
+		if !needContinue {
+			break
+		}
+		packet, err := s.transport.readPacket()
+		if err != nil {
+			return nil, nil, err
+		}
+		userAuthGSSAPITokenReq := &userAuthGSSAPIToken{}
+		if err := Unmarshal(packet, userAuthGSSAPITokenReq); err != nil {
+			return nil, nil, err
+		}
+	}
+	packet, err := s.transport.readPacket()
+	if err != nil {
+		return nil, nil, err
+	}
+	userAuthGSSAPIMICReq := &userAuthGSSAPIMIC{}
+	if err := Unmarshal(packet, userAuthGSSAPIMICReq); err != nil {
+		return nil, nil, err
+	}
+	mic := buildMIC(string(sessionID), userAuthReq.User, userAuthReq.Service, userAuthReq.Method)
+	if err := gssAPIServer.VerifyMIC(mic, userAuthGSSAPIMICReq.MIC); err != nil {
+		return err, nil, nil
+	}
+	perms, authErr = gssapiConfig.AllowLogin(s, srcName)
+	return authErr, perms, nil
+}
+
 // ServerAuthError represents server authentication errors and is
 // sometimes returned by NewServerConn. It appends any authentication
 // errors that may occur, and is returned if all of the authentication
@@ -496,6 +565,49 @@
 				authErr = candidate.result
 				perms = candidate.perms
 			}
+		case "gssapi-with-mic":
+			gssapiConfig := config.GSSAPIWithMICConfig
+			userAuthRequestGSSAPI, err := parseGSSAPIPayload(userAuthReq.Payload)
+			if err != nil {
+				return nil, parseError(msgUserAuthRequest)
+			}
+			// OpenSSH supports Kerberos V5 mechanism only for GSS-API authentication.
+			if userAuthRequestGSSAPI.N == 0 {
+				authErr = fmt.Errorf("ssh: Mechanism negotiation is not supported")
+				break
+			}
+			var i uint32
+			present := false
+			for i = 0; i < userAuthRequestGSSAPI.N; i++ {
+				if userAuthRequestGSSAPI.OIDS[i].Equal(krb5Mesh) {
+					present = true
+					break
+				}
+			}
+			if !present {
+				authErr = fmt.Errorf("ssh: GSSAPI authentication must use the Kerberos V5 mechanism")
+				break
+			}
+			// Initial server response, see RFC 4462 section 3.3.
+			if err := s.transport.writePacket(Marshal(&userAuthGSSAPIResponse{
+				SupportMech: krb5OID,
+			})); err != nil {
+				return nil, err
+			}
+			// Exchange token, see RFC 4462 section 3.4.
+			packet, err := s.transport.readPacket()
+			if err != nil {
+				return nil, err
+			}
+			userAuthGSSAPITokenReq := &userAuthGSSAPIToken{}
+			if err := Unmarshal(packet, userAuthGSSAPITokenReq); err != nil {
+				return nil, err
+			}
+			authErr, perms, err = gssExchangeToken(gssapiConfig, userAuthGSSAPITokenReq.Token, s, sessionID,
+				userAuthReq)
+			if err != nil {
+				return nil, err
+			}
 		default:
 			authErr = fmt.Errorf("ssh: unknown method %q", userAuthReq.Method)
 		}
@@ -522,6 +634,10 @@
 		if config.KeyboardInteractiveCallback != nil {
 			failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive")
 		}
+		if config.GSSAPIWithMICConfig != nil && config.GSSAPIWithMICConfig.Server != nil &&
+			config.GSSAPIWithMICConfig.AllowLogin != nil {
+			failureMsg.Methods = append(failureMsg.Methods, "gssapi-with-mic")
+		}
 
 		if len(failureMsg.Methods) == 0 {
 			return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
diff --git a/ssh/ssh_gss.go b/ssh/ssh_gss.go
new file mode 100644
index 0000000..24bd7c8
--- /dev/null
+++ b/ssh/ssh_gss.go
@@ -0,0 +1,139 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ssh
+
+import (
+	"encoding/asn1"
+	"errors"
+)
+
+var krb5OID []byte
+
+func init() {
+	krb5OID, _ = asn1.Marshal(krb5Mesh)
+}
+
+// GSSAPIClient provides the API to plug-in GSSAPI authentication for client logins.
+type GSSAPIClient interface {
+	// InitSecContext initiates the establishment of a security context for GSS-API between the
+	// ssh client and ssh server. Initially the token parameter should be specified as nil.
+	// The routine may return a outputToken which should be transferred to
+	// the ssh server, where the ssh server will present it to
+	// AcceptSecContext. If no token need be sent, InitSecContext will indicate this by setting
+	// needContinue to false. To complete the context
+	// establishment, one or more reply tokens may be required from the ssh
+	// server;if so, InitSecContext will return a needContinue which is true.
+	// In this case, InitSecContext should be called again when the
+	// reply token is received from the ssh server, passing the reply
+	// token to InitSecContext via the token parameters.
+	// See RFC 2743 section 2.2.1 and RFC 4462 section 3.4.
+	InitSecContext(target string, token []byte, isGSSDelegCreds bool) (outputToken []byte, needContinue bool, err error)
+	// GetMIC generates a cryptographic MIC for the SSH2 message, and places
+	// the MIC in a token for transfer to the ssh server.
+	// The contents of the MIC field are obtained by calling GSS_GetMIC()
+	// over the following, using the GSS-API context that was just
+	// established:
+	//  string    session identifier
+	//  byte      SSH_MSG_USERAUTH_REQUEST
+	//  string    user name
+	//  string    service
+	//  string    "gssapi-with-mic"
+	// See RFC 2743 section 2.3.1 and RFC 4462 3.5.
+	GetMIC(micFiled []byte) ([]byte, error)
+	// Whenever possible, it should be possible for
+	// DeleteSecContext() calls to be successfully processed even
+	// if other calls cannot succeed, thereby enabling context-related
+	// resources to be released.
+	// In addition to deleting established security contexts,
+	// gss_delete_sec_context must also be able to delete "half-built"
+	// security contexts resulting from an incomplete sequence of
+	// InitSecContext()/AcceptSecContext() calls.
+	// See RFC 2743 section 2.2.3.
+	DeleteSecContext() error
+}
+
+// GSSAPIServer provides the API to plug in GSSAPI authentication for server logins.
+type GSSAPIServer interface {
+	// AcceptSecContext allows a remotely initiated security context between the application
+	// and a remote peer to be established by the ssh client. The routine may return a
+	// outputToken which should be transferred to the ssh client,
+	// where the ssh client will present it to InitSecContext.
+	// If no token need be sent, AcceptSecContext will indicate this
+	// by setting the needContinue to false. To
+	// complete the context establishment, one or more reply tokens may be
+	// required from the ssh client. if so, AcceptSecContext
+	// will return a needContinue which is true, in which case it
+	// should be called again when the reply token is received from the ssh
+	// client, passing the token to AcceptSecContext via the
+	// token parameters.
+	// The srcName return value is the authenticated username.
+	// See RFC 2743 section 2.2.2 and RFC 4462 section 3.4.
+	AcceptSecContext(token []byte) (outputToken []byte, srcName string, needContinue bool, err error)
+	// VerifyMIC verifies that a cryptographic MIC, contained in the token parameter,
+	// fits the supplied message is received from the ssh client.
+	// See RFC 2743 section 2.3.2.
+	VerifyMIC(micField []byte, micToken []byte) error
+	// Whenever possible, it should be possible for
+	// DeleteSecContext() calls to be successfully processed even
+	// if other calls cannot succeed, thereby enabling context-related
+	// resources to be released.
+	// In addition to deleting established security contexts,
+	// gss_delete_sec_context must also be able to delete "half-built"
+	// security contexts resulting from an incomplete sequence of
+	// InitSecContext()/AcceptSecContext() calls.
+	// See RFC 2743 section 2.2.3.
+	DeleteSecContext() error
+}
+
+var (
+	// OpenSSH supports Kerberos V5 mechanism only for GSS-API authentication,
+	// so we also support the krb5 mechanism only.
+	// See RFC 1964 section 1.
+	krb5Mesh = asn1.ObjectIdentifier{1, 2, 840, 113554, 1, 2, 2}
+)
+
+// The GSS-API authentication method is initiated when the client sends an SSH_MSG_USERAUTH_REQUEST
+// See RFC 4462 section 3.2.
+type userAuthRequestGSSAPI struct {
+	N    uint32
+	OIDS []asn1.ObjectIdentifier
+}
+
+func parseGSSAPIPayload(payload []byte) (*userAuthRequestGSSAPI, error) {
+	n, rest, ok := parseUint32(payload)
+	if !ok {
+		return nil, errors.New("parse uint32 failed")
+	}
+	s := &userAuthRequestGSSAPI{
+		N:    n,
+		OIDS: make([]asn1.ObjectIdentifier, n),
+	}
+	for i := 0; i < int(n); i++ {
+		var (
+			desiredMech []byte
+			err         error
+		)
+		desiredMech, rest, ok = parseString(rest)
+		if !ok {
+			return nil, errors.New("parse string failed")
+		}
+		if rest, err = asn1.Unmarshal(desiredMech, &s.OIDS[i]); err != nil {
+			return nil, err
+		}
+
+	}
+	return s, nil
+}
+
+// See RFC 4462 section 3.6.
+func buildMIC(sessionID string, username string, service string, authMethod string) []byte {
+	out := make([]byte, 0, 0)
+	out = appendString(out, sessionID)
+	out = append(out, msgUserAuthRequest)
+	out = appendString(out, username)
+	out = appendString(out, service)
+	out = appendString(out, authMethod)
+	return out
+}
diff --git a/ssh/ssh_gss_test.go b/ssh/ssh_gss_test.go
new file mode 100644
index 0000000..39a1112
--- /dev/null
+++ b/ssh/ssh_gss_test.go
@@ -0,0 +1,109 @@
+package ssh
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestParseGSSAPIPayload(t *testing.T) {
+	payload := []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x06, 0x09,
+		0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x01, 0x02, 0x02}
+	res, err := parseGSSAPIPayload(payload)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if ok := res.OIDS[0].Equal(krb5Mesh); !ok {
+		t.Fatalf("got %v, want %v", res, krb5Mesh)
+	}
+}
+
+func TestBuildMIC(t *testing.T) {
+	sessionID := []byte{134, 180, 134, 194, 62, 145, 171, 82, 119, 149, 254, 196, 125, 173, 177, 145, 187, 85, 53,
+		183, 44, 150, 219, 129, 166, 195, 19, 33, 209, 246, 175, 121}
+	username := "testuser"
+	service := "ssh-connection"
+	authMethod := "gssapi-with-mic"
+	expected := []byte{0, 0, 0, 32, 134, 180, 134, 194, 62, 145, 171, 82, 119, 149, 254, 196, 125, 173, 177, 145, 187, 85, 53, 183, 44, 150, 219, 129, 166, 195, 19, 33, 209, 246, 175, 121, 50, 0, 0, 0, 8, 116, 101, 115, 116, 117, 115, 101, 114, 0, 0, 0, 14, 115, 115, 104, 45, 99, 111, 110, 110, 101, 99, 116, 105, 111, 110, 0, 0, 0, 15, 103, 115, 115, 97, 112, 105, 45, 119, 105, 116, 104, 45, 109, 105, 99}
+	result := buildMIC(string(sessionID), username, service, authMethod)
+	if string(result) != string(expected) {
+		t.Fatalf("buildMic: got %v, want %v", result, expected)
+	}
+}
+
+type exchange struct {
+	outToken      string
+	expectedToken string
+}
+
+type FakeClient struct {
+	exchanges []*exchange
+	round     int
+	mic       []byte
+	maxRound  int
+}
+
+func (f *FakeClient) InitSecContext(target string, token []byte, isGSSDelegCreds bool) (outputToken []byte, needContinue bool, err error) {
+	if token == nil {
+		if f.exchanges[f.round].expectedToken != "" {
+			err = fmt.Errorf("got empty token, want %q", f.exchanges[f.round].expectedToken)
+		} else {
+			outputToken = []byte(f.exchanges[f.round].outToken)
+		}
+	} else {
+		if string(token) != string(f.exchanges[f.round].expectedToken) {
+			err = fmt.Errorf("got %q, want token %q", token, f.exchanges[f.round].expectedToken)
+		} else {
+			outputToken = []byte(f.exchanges[f.round].outToken)
+		}
+	}
+	f.round++
+	needContinue = f.round < f.maxRound
+	return
+}
+
+func (f *FakeClient) GetMIC(micField []byte) ([]byte, error) {
+	return f.mic, nil
+}
+
+func (f *FakeClient) DeleteSecContext() error {
+	return nil
+}
+
+type FakeServer struct {
+	exchanges   []*exchange
+	round       int
+	expectedMIC []byte
+	srcName     string
+	maxRound    int
+}
+
+func (f *FakeServer) AcceptSecContext(token []byte) (outputToken []byte, srcName string, needContinue bool, err error) {
+	if token == nil {
+		if f.exchanges[f.round].expectedToken != "" {
+			err = fmt.Errorf("got empty token, want %q", f.exchanges[f.round].expectedToken)
+		} else {
+			outputToken = []byte(f.exchanges[f.round].outToken)
+		}
+	} else {
+		if string(token) != string(f.exchanges[f.round].expectedToken) {
+			err = fmt.Errorf("got %q, want token %q", token, f.exchanges[f.round].expectedToken)
+		} else {
+			outputToken = []byte(f.exchanges[f.round].outToken)
+		}
+	}
+	f.round++
+	needContinue = f.round < f.maxRound
+	srcName = f.srcName
+	return
+}
+
+func (f *FakeServer) VerifyMIC(micField []byte, micToken []byte) error {
+	if string(micToken) != string(f.expectedMIC) {
+		return fmt.Errorf("got MICToken %q, want %q", micToken, f.expectedMIC)
+	}
+	return nil
+}
+
+func (f *FakeServer) DeleteSecContext() error {
+	return nil
+}