Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[bugfix] Added APOptions []int in client.GSSAPIBindRequest(...) and client.InitSecContext(...), fixes #536 #537

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

p0dalirius
Copy link

@p0dalirius p0dalirius commented Nov 14, 2024

Fixing Kerberos authentication due to missing APOption "MutualRequired"

Overview of the fix

In client.InitSecContext(...)

I have changed the client.InitSecContext(...) prototype from:

func (client *Client) InitSecContext(target string, input []byte) ([]byte, bool, error)

to:

func (client *Client) InitSecContext(target string, input []byte, APOptions []int) ([]byte, bool, error)

to be able to pass specific flags through the APOptions []int to the call to spnego.NewKRB5TokenAPREQ(client.Client, tkt, ekey, gssapiFlags, APOptions)

In client.GSSAPIBind(...)

I have left this function as it was, so it can be used in the generic case of GSSAPI authentication without the need to pass specific AP Options flags in parameters.

In client.GSSAPIBindRequest(...)

I have changed the client.GSSAPIBindRequest(...) prototype from:

func (l *Conn) GSSAPIBindRequest(client GSSAPIClient, req *GSSAPIBindRequest) error

to

func (l *Conn) GSSAPIBindRequest(client GSSAPIClient, req *GSSAPIBindRequest, APOptions []int) error

Example of a working code for Kerberos authentication

package main

import (
	"encoding/hex"
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/go-ldap/ldap/v3"
	"github.com/jcmturner/gokrb5/v8/iana/flags"
	"github.com/go-ldap/ldap/v3/gssapi"
	"github.com/jcmturner/gokrb5/v8/client"
	"github.com/jcmturner/gokrb5/v8/config"
)

func main() {
	fqdnLDAPHost := "SRV-DC01.lab.local"
	baseDN := "DC=LAB,DC=local"

	realm := "lab.local"
	realm = strings.ToUpper(realm)
	// This is always in uppercase, if not we get the error:
	// error performing GSSAPI bind: [Root cause: KRBMessage_Handling_Error]
	// | KRBMessage_Handling_Error: AS Exchange Error: AS_REP is not valid or client password/keytab incorrect
	// |  | KRBMessage_Handling_Error: CRealm in response does not match what was requested.
	// |  |  | Requested: lab.local;
	// |  |  | Reply: lab.local
	// | 2024/10/08 15:36:16 error querying AD: LDAP Result Code 1 "Operations Error": 000004DC: LdapErr: DSID-0C090A5C,
	// | comment: In order to perform this operation a successful bind must be completed on the connection., data 0, v4563

	username := "Administrator"
	// error performing GSSAPI bind: [Root cause: KDC_Error] KDC_Error: AS Exchange Error: kerberos error response from KDC:
	// KRB Error: (6) KDC_ERR_C_PRINCIPAL_UNKNOWN Client not found in Kerberos database
	// KDC_ERR_C_PRINCIPAL_UNKNOWN (error code 6) for these means that the domain controller to which the request
	// was made does not host the account and the client should choose a different domain controller.
	// src: https://learn.microsoft.com/en-us/troubleshoot/windows-server/certificates-and-public-key-infrastructure-pki/kdc-err-c-principal-unknown-s4u2self-request
	// ==> This means this username does not exist

	password := "Admin123!"

	servicePrincipalName := fmt.Sprintf("ldap/%s", fqdnLDAPHost)

	krb5Conf := config.New()
	// LibDefaults
	krb5Conf.LibDefaults.AllowWeakCrypto = true
	krb5Conf.LibDefaults.DefaultRealm = realm
	krb5Conf.LibDefaults.DNSLookupRealm = false
	krb5Conf.LibDefaults.DNSLookupKDC = false
	krb5Conf.LibDefaults.TicketLifetime = time.Duration(24) * time.Hour
	krb5Conf.LibDefaults.RenewLifetime = time.Duration(24*7) * time.Hour
	krb5Conf.LibDefaults.Forwardable = true
	krb5Conf.LibDefaults.Proxiable = true
	krb5Conf.LibDefaults.RDNS = false
	krb5Conf.LibDefaults.UDPPreferenceLimit = 1 // Force use of tcp
	krb5Conf.LibDefaults.DefaultTGSEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "arcfour-hmac-md5"}
	krb5Conf.LibDefaults.DefaultTktEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "arcfour-hmac-md5"}
	krb5Conf.LibDefaults.PermittedEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "arcfour-hmac-md5"}
	krb5Conf.LibDefaults.PermittedEnctypeIDs = []int32{18, 17, 23}
	krb5Conf.LibDefaults.DefaultTGSEnctypeIDs = []int32{18, 17, 23}
	krb5Conf.LibDefaults.DefaultTktEnctypeIDs = []int32{18, 17, 23}
	krb5Conf.LibDefaults.PreferredPreauthTypes = []int{18, 17, 23}

	// Realms
	krb5Conf.Realms = append(krb5Conf.Realms, config.Realm{
		Realm:         realm,
		AdminServer:   []string{fqdnLDAPHost},
		DefaultDomain: realm,
		KDC:           []string{fmt.Sprintf("%s:88", fqdnLDAPHost)},
		KPasswdServer: []string{fmt.Sprintf("%s:464", fqdnLDAPHost)},
		MasterKDC:     []string{fqdnLDAPHost},
	})

	// Domain Realm
	krb5Conf.DomainRealm[strings.ToLower(realm)] = realm
	krb5Conf.DomainRealm[fmt.Sprintf(".%s", strings.ToLower(realm))] = realm

	printKrb5Conf(krb5Conf)

	// Connect to LDAP server
	bindString := fmt.Sprintf("ldaps://%s:636", fqdnLDAPHost)
	ldapConnection, err := ldap.DialURL(
		bindString,
		ldap.DialWithTLSConfig(
			&tls.Config{
				InsecureSkipVerify: true,
			},
		),
	)
	if err != nil {
		log.Printf("[error] ldap.DialURL(\"%s\"): %s\n", bindString, err)
		return
	} else {
		log.Printf("[debug] ldap.DialURL(\"%s\"): success\n", bindString)
	}
	ldapConnection.Debug = true

	// Initialize kerberos client
	// Inspired from: https://github.com/go-ldap/ldap/blob/06d50d1ad03bcd323e48f2fe174d95ceb31b8b90/v3/gssapi/client.go#L51
	kerberosClient := gssapi.Client{
		Client: client.NewWithPassword(
			username,
			realm,
			password,
			krb5Conf,
			// Active Directory does not commonly support FAST negotiationso you will need to disable this on the client.
			// If this is the case you will see this error: KDC did not respond appropriately to FAST negotiation
			// https://github.com/jcmturner/gokrb5/blob/master/USAGE.md#active-directory-kdc-and-fast-negotiation
			client.DisablePAFXFAST(true),
		),
	}
	defer kerberosClient.Close()

	// Initiating ldap GSSAPIBind
	err = ldapConnection.GSSAPIBindRequest(
		&kerberosClient,
		&ldap.GSSAPIBindRequest{
			ServicePrincipalName: servicePrincipalName,
			AuthZID:              "",
		},
		[]int{flags.APOptionMutualRequired},
        )
	if err != nil {
		log.Printf("[error] ldapConnection.GSSAPIBind(): %s\n", err)
		return
	} else {
		log.Printf("[debug] ldapConnection.GSSAPIBind(): success\n")
	}

	// Successfully bound
	searchRequest := ldap.NewSearchRequest(
		baseDN,
		ldap.ScopeWholeSubtree,
		ldap.NeverDerefAliases,
		0,
		0,
		false,
		"(objectClass=user)",
		[]string{"distinguishedName"},
		nil,
	)
	ldapResults, err := ldapConnection.SearchWithPaging(searchRequest, 1000)
	if err != nil {
		log.Fatalf("[error] ldapConnection.Search(): %v\n", err)
		return
	} else {
		log.Printf("[debug] ldapConnection.Search(): success\n")
	}

	for _, entry := range ldapResults.Entries {
		fmt.Printf(" - %s", entry.DN)
	}

	log.Printf("[debug] All done!\n")
}
}

Summary

These APOptions []int can now be set when calling client.GSSAPIBindRequest(...), which will then pass it to the underlying client.InitSecContext(...), which will then be processed in the call to spnego.NewKRB5TokenAPREQ(client.Client, tkt, ekey, gssapiFlags, APOptions)

Best Regards,

@p0dalirius p0dalirius force-pushed the fix-ldap-gssapi-kerberos-auth-on-windows-active-directory branch from c76a77b to 3a20a5c Compare November 14, 2024 15:14
@cpuschma
Copy link
Member

Hi @p0dalirius,

thank you very much for your PR and the work you put into debugging this issue (#536)! My strengths are not at all in Kerberos and my knowledge of the protocol is practically zero, so thank you again!

But: Modifying the signature of an existing function will definitely interfere with existing code. Even the addition of changes that are only noticed by linters has caused trouble in the past (#463).

If there is really no other way to implement Kerberos authentication, I would be willing to do so and include it in the next release, but only if no one objects. @go-ldap/committers thoughts?

@p0dalirius
Copy link
Author

p0dalirius commented Nov 18, 2024

@cpuschma, before merging, It would be nice if someone can try to connect to a UNIX ldap server with GSSAPI to confirm that it still works with this fix

I will try to setup one and test it

@stefanmcshane
Copy link
Contributor

Hi @p0dalirius,

thank you very much for your PR and the work you put into debugging this issue (#536)! My strengths are not at all in Kerberos and my knowledge of the protocol is practically zero, so thank you again!

But: Modifying the signature of an existing function will definitely interfere with existing code. Even the addition of changes that are only noticed by linters has caused trouble in the past (#463).

If there is really no other way to implement Kerberos authentication, I would be willing to do so and include it in the next release, but only if no one objects. @go-ldap/committers thoughts?

Using functional options instead of a slice of ints should not break existing implementations

@stevensbkang
Copy link

Could someone please advise if this will be merged soon? :)

Copy link
Member

@johnweldon johnweldon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the difficulties with this PR is the lack of GSSAPI knowledge by many of the maintainers - if someone with GSSAPI experience could weigh in that would be helpful.

That being said there are some things that should be done differently, and I've tried to point out some of them in this review.

I like the idea of using functional options to avoid future breaking changes, but I'm not sure there's really a need for it in this particular case.

v3/gssapi/client.go Outdated Show resolved Hide resolved
@@ -99,7 +103,7 @@ func (client *Client) DeleteSecContext() error {
// InitSecContext initiates the establishment of a security context for
// GSS-API between the client and server.
// See RFC 4752 section 3.1.
func (client *Client) InitSecContext(target string, input []byte) ([]byte, bool, error) {
func (client *Client) InitSecContext(target string, input []byte, APOptions []int) ([]byte, bool, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not break the API.

I suggest adding a new func (*Client) InitSecContextWithOptions(string, []byte, []int) ([]byte, bool, error) and then modify the internals of the original InitSecContext to simply call the new InitSecContextWithOptions, passing in suitable defaults.

@@ -212,3 +216,48 @@ func (client *Client) NegotiateSaslAuth(input []byte, authzid string) ([]byte, e

return output, nil
}

func UnmarshalWrapToken(wt *gssapi.WrapToken, data []byte, expectFromAcceptor bool) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate this new function that appears to do more robust checking while unmarshalling the input, however:

  • We should not export the function (and thereby add to our API), unless absolutely necessary.
  • I would be much more comfortable if there were some unit tests asserting that this function actually works as intended.

func UnmarshalWrapToken(wt *gssapi.WrapToken, data []byte, expectFromAcceptor bool) error {
// Check if we can read a whole header
if len(data) < 16 {
return errors.New("bytes shorter than header length")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we anticipate any kind of code handling of these errors it may be kinder to actually create named errors (var ErrInvalidGSSAPIFooBarHeaderLength = "bytes shorter ..."), that can be used with the errors.Is and related helper functions.

// Is the Token ID correct?
expectedWrapTokenId := [2]byte{0x05, 0x04}
if !bytes.Equal(expectedWrapTokenId[:], data[0:2]) {
return fmt.Errorf("wrong Token ID. Expected %s, was %s", hex.EncodeToString(expectedWrapTokenId[:]), hex.EncodeToString(data[0:2]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In cases where returning additional context with the error it may be nice for it to wrap a simpler 'known' error as the related comment says.

eg return fmt.Errorf("wrong blah blah. expected %s, was %s: %w", x, y, ErrUnmarshalFault) or something like that

v3/bind.go Outdated
@@ -576,7 +576,7 @@ type GSSAPIClient interface {
// reply token is received from the server, passing the reply token
// to InitSecContext via the token parameters.
// See RFC 4752 section 3.1.
InitSecContext(target string, token []byte) (outputToken []byte, needContinue bool, err error)
InitSecContext(target string, token []byte, APOptions []int) (outputToken []byte, needContinue bool, err error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the comment on the implementation; let's not modify (break) an existing API - we should add a new method if we really need to.

@FlipB
Copy link
Contributor

FlipB commented Dec 20, 2024

The earlier commits would have broken the client in gssapi/sspi.go.
These last changes keep the krb5 specific details in the krb5 client, so that's good.

(I think it makes sense to add asserts that both clients implement the interface to make breaking changes less likely, although you would still have to build for Windows (looks like CI only builds for linux) to be sure.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
6 participants