Skip to content

Commit cbcb750

Browse files
nhywiezahanwen
authored andcommitted
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 <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Han-Wen Nienhuys <[email protected]>
1 parent e1dfcc5 commit cbcb750

File tree

6 files changed

+737
-5
lines changed

6 files changed

+737
-5
lines changed

Diff for: ssh/client_auth.go

+114
Original file line numberDiff line numberDiff line change
@@ -523,3 +523,117 @@ func (r *retryableAuthMethod) method() string {
523523
func RetryableAuthMethod(auth AuthMethod, maxTries int) AuthMethod {
524524
return &retryableAuthMethod{authMethod: auth, maxTries: maxTries}
525525
}
526+
527+
// GSSAPIWithMICAuthMethod is an AuthMethod with "gssapi-with-mic" authentication.
528+
// See RFC 4462 section 3
529+
// gssAPIClient is implementation of the GSSAPIClient interface, see the definition of the interface for details.
530+
// target is the server host you want to log in to.
531+
func GSSAPIWithMICAuthMethod(gssAPIClient GSSAPIClient, target string) AuthMethod {
532+
if gssAPIClient == nil {
533+
panic("gss-api client must be not nil with enable gssapi-with-mic")
534+
}
535+
return &gssAPIWithMICCallback{gssAPIClient: gssAPIClient, target: target}
536+
}
537+
538+
type gssAPIWithMICCallback struct {
539+
gssAPIClient GSSAPIClient
540+
target string
541+
}
542+
543+
func (g *gssAPIWithMICCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) {
544+
m := &userAuthRequestMsg{
545+
User: user,
546+
Service: serviceSSH,
547+
Method: g.method(),
548+
}
549+
// The GSS-API authentication method is initiated when the client sends an SSH_MSG_USERAUTH_REQUEST.
550+
// See RFC 4462 section 3.2.
551+
m.Payload = appendU32(m.Payload, 1)
552+
m.Payload = appendString(m.Payload, string(krb5OID))
553+
if err := c.writePacket(Marshal(m)); err != nil {
554+
return authFailure, nil, err
555+
}
556+
// The server responds to the SSH_MSG_USERAUTH_REQUEST with either an
557+
// SSH_MSG_USERAUTH_FAILURE if none of the mechanisms are supported or
558+
// with an SSH_MSG_USERAUTH_GSSAPI_RESPONSE.
559+
// See RFC 4462 section 3.3.
560+
// OpenSSH supports Kerberos V5 mechanism only for GSS-API authentication,so I don't want to check
561+
// selected mech if it is valid.
562+
packet, err := c.readPacket()
563+
if err != nil {
564+
return authFailure, nil, err
565+
}
566+
userAuthGSSAPIResp := &userAuthGSSAPIResponse{}
567+
if err := Unmarshal(packet, userAuthGSSAPIResp); err != nil {
568+
return authFailure, nil, err
569+
}
570+
// Start the loop into the exchange token.
571+
// See RFC 4462 section 3.4.
572+
var token []byte
573+
defer g.gssAPIClient.DeleteSecContext()
574+
for {
575+
// Initiates the establishment of a security context between the application and a remote peer.
576+
nextToken, needContinue, err := g.gssAPIClient.InitSecContext("host@"+g.target, token, false)
577+
if err != nil {
578+
return authFailure, nil, err
579+
}
580+
if len(nextToken) > 0 {
581+
if err := c.writePacket(Marshal(&userAuthGSSAPIToken{
582+
Token: nextToken,
583+
})); err != nil {
584+
return authFailure, nil, err
585+
}
586+
}
587+
if !needContinue {
588+
break
589+
}
590+
packet, err = c.readPacket()
591+
if err != nil {
592+
return authFailure, nil, err
593+
}
594+
switch packet[0] {
595+
case msgUserAuthFailure:
596+
var msg userAuthFailureMsg
597+
if err := Unmarshal(packet, &msg); err != nil {
598+
return authFailure, nil, err
599+
}
600+
if msg.PartialSuccess {
601+
return authPartialSuccess, msg.Methods, nil
602+
}
603+
return authFailure, msg.Methods, nil
604+
case msgUserAuthGSSAPIError:
605+
userAuthGSSAPIErrorResp := &userAuthGSSAPIError{}
606+
if err := Unmarshal(packet, userAuthGSSAPIErrorResp); err != nil {
607+
return authFailure, nil, err
608+
}
609+
return authFailure, nil, fmt.Errorf("GSS-API Error:\n"+
610+
"Major Status: %d\n"+
611+
"Minor Status: %d\n"+
612+
"Error Message: %s\n", userAuthGSSAPIErrorResp.MajorStatus, userAuthGSSAPIErrorResp.MinorStatus,
613+
userAuthGSSAPIErrorResp.Message)
614+
case msgUserAuthGSSAPIToken:
615+
userAuthGSSAPITokenReq := &userAuthGSSAPIToken{}
616+
if err := Unmarshal(packet, userAuthGSSAPITokenReq); err != nil {
617+
return authFailure, nil, err
618+
}
619+
token = userAuthGSSAPITokenReq.Token
620+
}
621+
}
622+
// Binding Encryption Keys.
623+
// See RFC 4462 section 3.5.
624+
micField := buildMIC(string(session), user, "ssh-connection", "gssapi-with-mic")
625+
micToken, err := g.gssAPIClient.GetMIC(micField)
626+
if err != nil {
627+
return authFailure, nil, err
628+
}
629+
if err := c.writePacket(Marshal(&userAuthGSSAPIMIC{
630+
MIC: micToken,
631+
})); err != nil {
632+
return authFailure, nil, err
633+
}
634+
return handleAuthResponse(c)
635+
}
636+
637+
func (g *gssAPIWithMICCallback) method() string {
638+
return "gssapi-with-mic"
639+
}

Diff for: ssh/client_auth_test.go

+214-4
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,19 @@ var clientPassword = "tiger"
3333
// tryAuth runs a handshake with a given config against an SSH server
3434
// with config serverConfig. Returns both client and server side errors.
3535
func tryAuth(t *testing.T, config *ClientConfig) error {
36-
err, _ := tryAuthBothSides(t, config)
36+
err, _ := tryAuthBothSides(t, config, nil)
37+
return err
38+
}
39+
40+
// tryAuth runs a handshake with a given config against an SSH server
41+
// with a given GSSAPIWithMICConfig and config serverConfig. Returns both client and server side errors.
42+
func tryAuthWithGSSAPIWithMICConfig(t *testing.T, clientConfig *ClientConfig, gssAPIWithMICConfig *GSSAPIWithMICConfig) error {
43+
err, _ := tryAuthBothSides(t, clientConfig, gssAPIWithMICConfig)
3744
return err
3845
}
3946

4047
// tryAuthBothSides runs the handshake and returns the resulting errors from both sides of the connection.
41-
func tryAuthBothSides(t *testing.T, config *ClientConfig) (clientError error, serverAuthErrors []error) {
48+
func tryAuthBothSides(t *testing.T, config *ClientConfig, gssAPIWithMICConfig *GSSAPIWithMICConfig) (clientError error, serverAuthErrors []error) {
4249
c1, c2, err := netPipe()
4350
if err != nil {
4451
t.Fatalf("netPipe: %v", err)
@@ -61,7 +68,6 @@ func tryAuthBothSides(t *testing.T, config *ClientConfig) (clientError error, se
6168
return c.Serial == 666
6269
},
6370
}
64-
6571
serverConfig := &ServerConfig{
6672
PasswordCallback: func(conn ConnMetadata, pass []byte) (*Permissions, error) {
6773
if conn.User() == "testuser" && string(pass) == clientPassword {
@@ -85,6 +91,7 @@ func tryAuthBothSides(t *testing.T, config *ClientConfig) (clientError error, se
8591
}
8692
return nil, errors.New("keyboard-interactive failed")
8793
},
94+
GSSAPIWithMICConfig: gssAPIWithMICConfig,
8895
}
8996
serverConfig.AddHostKey(testSigners["rsa"])
9097

@@ -247,7 +254,7 @@ func TestMethodInvalidAlgorithm(t *testing.T) {
247254
HostKeyCallback: InsecureIgnoreHostKey(),
248255
}
249256

250-
err, serverErrors := tryAuthBothSides(t, config)
257+
err, serverErrors := tryAuthBothSides(t, config, nil)
251258
if err == nil {
252259
t.Fatalf("login succeeded")
253260
}
@@ -686,3 +693,206 @@ func TestClientAuthErrorList(t *testing.T) {
686693
}
687694
}
688695
}
696+
697+
func TestAuthMethodGSSAPIWithMIC(t *testing.T) {
698+
type testcase struct {
699+
config *ClientConfig
700+
gssConfig *GSSAPIWithMICConfig
701+
clientWantErr string
702+
serverWantErr string
703+
}
704+
testcases := []*testcase{
705+
{
706+
config: &ClientConfig{
707+
User: "testuser",
708+
Auth: []AuthMethod{
709+
GSSAPIWithMICAuthMethod(
710+
&FakeClient{
711+
exchanges: []*exchange{
712+
{
713+
outToken: "client-valid-token-1",
714+
},
715+
{
716+
expectedToken: "server-valid-token-1",
717+
},
718+
},
719+
mic: []byte("valid-mic"),
720+
maxRound: 2,
721+
}, "testtarget",
722+
),
723+
},
724+
HostKeyCallback: InsecureIgnoreHostKey(),
725+
},
726+
gssConfig: &GSSAPIWithMICConfig{
727+
AllowLogin: func(conn ConnMetadata, srcName string) (*Permissions, error) {
728+
if srcName != conn.User()+"@DOMAIN" {
729+
return nil, fmt.Errorf("srcName is %s, conn user is %s", srcName, conn.User())
730+
}
731+
return nil, nil
732+
},
733+
Server: &FakeServer{
734+
exchanges: []*exchange{
735+
{
736+
outToken: "server-valid-token-1",
737+
expectedToken: "client-valid-token-1",
738+
},
739+
},
740+
maxRound: 1,
741+
expectedMIC: []byte("valid-mic"),
742+
srcName: "testuser@DOMAIN",
743+
},
744+
},
745+
},
746+
{
747+
config: &ClientConfig{
748+
User: "testuser",
749+
Auth: []AuthMethod{
750+
GSSAPIWithMICAuthMethod(
751+
&FakeClient{
752+
exchanges: []*exchange{
753+
{
754+
outToken: "client-valid-token-1",
755+
},
756+
{
757+
expectedToken: "server-valid-token-1",
758+
},
759+
},
760+
mic: []byte("valid-mic"),
761+
maxRound: 2,
762+
}, "testtarget",
763+
),
764+
},
765+
HostKeyCallback: InsecureIgnoreHostKey(),
766+
},
767+
gssConfig: &GSSAPIWithMICConfig{
768+
AllowLogin: func(conn ConnMetadata, srcName string) (*Permissions, error) {
769+
return nil, fmt.Errorf("user is not allowed to login")
770+
},
771+
Server: &FakeServer{
772+
exchanges: []*exchange{
773+
{
774+
outToken: "server-valid-token-1",
775+
expectedToken: "client-valid-token-1",
776+
},
777+
},
778+
maxRound: 1,
779+
expectedMIC: []byte("valid-mic"),
780+
srcName: "testuser@DOMAIN",
781+
},
782+
},
783+
serverWantErr: "user is not allowed to login",
784+
clientWantErr: "ssh: handshake failed: ssh: unable to authenticate",
785+
},
786+
{
787+
config: &ClientConfig{
788+
User: "testuser",
789+
Auth: []AuthMethod{
790+
GSSAPIWithMICAuthMethod(
791+
&FakeClient{
792+
exchanges: []*exchange{
793+
{
794+
outToken: "client-valid-token-1",
795+
},
796+
{
797+
expectedToken: "server-valid-token-1",
798+
},
799+
},
800+
mic: []byte("valid-mic"),
801+
maxRound: 2,
802+
}, "testtarget",
803+
),
804+
},
805+
HostKeyCallback: InsecureIgnoreHostKey(),
806+
},
807+
gssConfig: &GSSAPIWithMICConfig{
808+
AllowLogin: func(conn ConnMetadata, srcName string) (*Permissions, error) {
809+
if srcName != conn.User() {
810+
return nil, fmt.Errorf("srcName is %s, conn user is %s", srcName, conn.User())
811+
}
812+
return nil, nil
813+
},
814+
Server: &FakeServer{
815+
exchanges: []*exchange{
816+
{
817+
outToken: "server-invalid-token-1",
818+
expectedToken: "client-valid-token-1",
819+
},
820+
},
821+
maxRound: 1,
822+
expectedMIC: []byte("valid-mic"),
823+
srcName: "testuser@DOMAIN",
824+
},
825+
},
826+
clientWantErr: "ssh: handshake failed: got \"server-invalid-token-1\", want token \"server-valid-token-1\"",
827+
},
828+
{
829+
config: &ClientConfig{
830+
User: "testuser",
831+
Auth: []AuthMethod{
832+
GSSAPIWithMICAuthMethod(
833+
&FakeClient{
834+
exchanges: []*exchange{
835+
{
836+
outToken: "client-valid-token-1",
837+
},
838+
{
839+
expectedToken: "server-valid-token-1",
840+
},
841+
},
842+
mic: []byte("invalid-mic"),
843+
maxRound: 2,
844+
}, "testtarget",
845+
),
846+
},
847+
HostKeyCallback: InsecureIgnoreHostKey(),
848+
},
849+
gssConfig: &GSSAPIWithMICConfig{
850+
AllowLogin: func(conn ConnMetadata, srcName string) (*Permissions, error) {
851+
if srcName != conn.User() {
852+
return nil, fmt.Errorf("srcName is %s, conn user is %s", srcName, conn.User())
853+
}
854+
return nil, nil
855+
},
856+
Server: &FakeServer{
857+
exchanges: []*exchange{
858+
{
859+
outToken: "server-valid-token-1",
860+
expectedToken: "client-valid-token-1",
861+
},
862+
},
863+
maxRound: 1,
864+
expectedMIC: []byte("valid-mic"),
865+
srcName: "testuser@DOMAIN",
866+
},
867+
},
868+
serverWantErr: "got MICToken \"invalid-mic\", want \"valid-mic\"",
869+
clientWantErr: "ssh: handshake failed: ssh: unable to authenticate",
870+
},
871+
}
872+
873+
for i, c := range testcases {
874+
clientErr, serverErrs := tryAuthBothSides(t, c.config, c.gssConfig)
875+
if (c.clientWantErr == "") != (clientErr == nil) {
876+
t.Fatalf("client got %v, want %s, case %d", clientErr, c.clientWantErr, i)
877+
}
878+
if (c.serverWantErr == "") != (len(serverErrs) == 2 && serverErrs[1] == nil || len(serverErrs) == 1) {
879+
t.Fatalf("server got err %v, want %s", serverErrs, c.serverWantErr)
880+
}
881+
if c.clientWantErr != "" {
882+
if clientErr != nil && !strings.Contains(clientErr.Error(), c.clientWantErr) {
883+
t.Fatalf("client got %v, want %s, case %d", clientErr, c.clientWantErr, i)
884+
}
885+
}
886+
found := false
887+
var errStrings []string
888+
if c.serverWantErr != "" {
889+
for _, err := range serverErrs {
890+
found = found || (err != nil && strings.Contains(err.Error(), c.serverWantErr))
891+
errStrings = append(errStrings, err.Error())
892+
}
893+
if !found {
894+
t.Errorf("server got error %q, want substring %q, case %d", errStrings, c.serverWantErr, i)
895+
}
896+
}
897+
}
898+
}

0 commit comments

Comments
 (0)