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>
This commit is contained in:
Родитель
e1dfcc5662
Коммит
cbcb750295
|
@ -523,3 +523,117 @@ func (r *retryableAuthMethod) method() string {
|
|||
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"
|
||||
}
|
||||
|
|
|
@ -33,12 +33,19 @@ var clientPassword = "tiger"
|
|||
// 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 @@ func tryAuthBothSides(t *testing.T, config *ClientConfig) (clientError error, se
|
|||
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 @@ func tryAuthBothSides(t *testing.T, config *ClientConfig) (clientError error, se
|
|||
}
|
||||
return nil, errors.New("keyboard-interactive failed")
|
||||
},
|
||||
GSSAPIWithMICConfig: gssAPIWithMICConfig,
|
||||
}
|
||||
serverConfig.AddHostKey(testSigners["rsa"])
|
||||
|
||||
|
@ -247,7 +254,7 @@ func TestMethodInvalidAlgorithm(t *testing.T) {
|
|||
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 TestClientAuthErrorList(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -275,6 +275,42 @@ type userAuthPubKeyOkMsg struct {
|
|||
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 @@ func decode(packet []byte) (interface{}, error) {
|
|||
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])
|
||||
}
|
||||
|
|
118
ssh/server.go
118
ssh/server.go
|
@ -45,6 +45,20 @@ type Permissions struct {
|
|||
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 @@ type ServerConfig struct {
|
|||
// 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 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error)
|
|||
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 @@ func checkSourceAddress(addr net.Addr, sourceAddrs string) error {
|
|||
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 @@ userAuthLoop:
|
|||
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 @@ userAuthLoop:
|
|||
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")
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
Загрузка…
Ссылка в новой задаче