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

Adds GSSAPI Bind support #340

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import (

"github.com/Azure/go-ntlmssp"
ber "github.com/go-asn1-ber/asn1-ber"
gssapi "github.com/jcmturner/gokrb5/v8/client"
k5conf "github.com/jcmturner/gokrb5/v8/config"
k5creds "github.com/jcmturner/gokrb5/v8/credentials"
k5types "github.com/jcmturner/gokrb5/v8/types"
)

// SimpleBindRequest represents a username/password bind operation
Expand Down Expand Up @@ -538,3 +542,144 @@ func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindRes
err = GetLDAPError(packet)
return result, err
}

// GSSAPI Bind using gokrb5
type GSSAPIBindRequest struct {
// Service Principal Name to try to get a service ticket for. With LDAP in
// most cases this will be "ldap/<hostname>"
SPN string
// Authorization entity to authenticate as
AuthZID string
// KRB5 client as an abstraction over Credentials coming from a keytab,
// ccache or freshly acquired from the KDC
client *gssapi.Client
// Token
token []byte
// Are we on the last step
done bool
// Controls are optional controls to send with the bind request
Controls []Control
}

// GSSAPI Bind using your CCache with an empty AuthZID
func (l *Conn) GSSAPICCBind(confpath, cpath, spn string) error {
return l.GSSAPICCBindZ(confpath, cpath, "", spn)
}

// GSSAPI Bind using your CCache with a set AuthZID
func (l *Conn) GSSAPICCBindZ(confpath, cpath, authzid, spn string) error {
config, err := k5conf.Load(confpath)
if err != nil {
return err
}

ccache, err := k5creds.LoadCCache(cpath)
if err != nil {
return err
}

client, err := gssapi.NewFromCCache(ccache, config)
if err != nil {
return err
}

req := &GSSAPIBindRequest{
SPN: spn,
AuthZID: authzid,
client: client,
}
_, err = l.GSSAPIBind(req)
return err
}

type GSSAPIBindResult struct {
Subkey k5types.EncryptionKey
Controls []Control
}

func (req *GSSAPIBindRequest) appendTo(envelope *ber.Packet) error {
request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request")
request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version"))
request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name"))

auth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication")
auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "GSSAPI", "SASL Mech"))
auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, string(req.token[:]), "Credentials"))
Copy link

Choose a reason for hiding this comment

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

only append credentials child to auth if len(token) > 0

request.AppendChild(auth)
envelope.AppendChild(request)
if len(req.Controls) > 0 {
envelope.AppendChild(encodeControls(req.Controls))
}
return nil
}

// GSSAPIBind performs the GSSAPI bind operation with the credentials in the given Request
func (l *Conn) GSSAPIBind(req *GSSAPIBindRequest) (*GSSAPIBindResult, error) {
result := &GSSAPIBindResult{
Controls: make([]Control, 0),
}

state, err := InitContext(req.client, req.SPN, req.AuthZID)
if err != nil {
return nil, err
}

req.token, err = state.GSSAPIStep(make([]byte, 0))
if err != nil {
return nil, err
}

// Loop until we are done
done := false
OUTER:
for !done {
Copy link

Choose a reason for hiding this comment

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

for !done && err == nil {
  done, err = func() (bool, error) {
    // do the real work here, so that defer is executed properly
  }()
}
return result, err

var data []byte

msgCtx, err := l.doRequest(req)
if err != nil {
return nil, err
}
defer l.finishMessage(msgCtx)

packet, err := l.readPacket(msgCtx)
if err != nil {
return nil, err
}
l.Debug.Printf("%d: got response %p", msgCtx.id, packet)
if l.Debug {
if err = addLDAPDescriptions(packet); err != nil {
return nil, err
}
ber.PrintPacket(packet)
}

if len(packet.Children) == 2 {
child := packet.Children[1].Children[0]
if child.Tag != ber.TagEnumerated {
return result, GetLDAPError(packet)
}
switch child.Value.(int64) {
case 0:
return result, nil
case 14:
break
default:
return nil, GetLDAPError(packet)
}

for _, child := range packet.Children[1].Children {
if child.ClassType == ber.ClassContext && child.Tag == 7 {
data, err = ioutil.ReadAll(child.Data)
Copy link

Choose a reason for hiding this comment

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

check for err of ReadAll step?

req.token, err = state.GSSAPIStep(data)
if err != nil {
return nil, err
}
continue OUTER
}
}
}
return nil, fmt.Errorf("Server sent us a bad tag during bind")
}

return result, nil
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ go 1.13
require (
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c
github.com/go-asn1-ber/asn1-ber v1.5.1
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
github.com/jcmturner/gofork v1.0.0
github.com/jcmturner/gokrb5/v8 v8.4.2
)
130 changes: 130 additions & 0 deletions gssapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package ldap

import (
"fmt"

"github.com/jcmturner/gokrb5/v8/client"
"github.com/jcmturner/gokrb5/v8/crypto"
"github.com/jcmturner/gokrb5/v8/iana/keyusage"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"

"github.com/jcmturner/gokrb5/v8/gssapi"
"github.com/jcmturner/gokrb5/v8/spnego"
)

type GSSAPIState struct {
AuthZID string
token spnego.KRB5Token
ekey types.EncryptionKey
Subkey types.EncryptionKey
init bool
asrep bool
}

func InitContext(client *client.Client, principal, AuthZID string) (*GSSAPIState, error) {
tkt, ekey, err := client.GetServiceTicket(principal)
if err != nil {
return nil, err
}

token, err := spnego.NewKRB5TokenAPREQ(client, tkt, ekey, []int{gssapi.ContextFlagInteg, gssapi.ContextFlagConf, gssapi.ContextFlagMutual}, []int{})
Copy link

Choose a reason for hiding this comment

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

I needed to include flags.APOptionMutualRequired (in the empty int list) for AD support

if err != nil {
return nil, err
}

state := &GSSAPIState{
AuthZID: AuthZID,
ekey: ekey,
token: token,
init: false,
asrep: false,
}

return state, nil
}

func (state *GSSAPIState) GSSAPIStep(input []byte) ([]byte, error) {
if !state.init {
state.init = true
return state.token.Marshal()
}

if !state.asrep {
err := state.token.Unmarshal(input)
if err != nil {
return nil, err
}

if state.token.IsAPRep() {
state.asrep = true

encpart, err := crypto.DecryptEncPart(state.token.APRep.EncPart, state.ekey, keyusage.AP_REP_ENCPART)
if err != nil {
return nil, err
}

part := &messages.EncAPRepPart{}
err = part.Unmarshal(encpart)
if err != nil {
return nil, err
}

state.Subkey = part.Subkey
}

if state.token.IsKRBError() {
return nil, state.token.KRBError
}

return make([]byte, 0), nil
}

token := &gssapi.WrapToken{}
err := token.Unmarshal(input, true)
Copy link

Choose a reason for hiding this comment

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

testing against AD, this only works if I hacked the Unmarshal() call, which could be done here, after the fact, instead of within Unmarshal() itself, if needed. otherwise the call to token.Verify() fails w/ a checksum mismatch.

the hack: Unmarshal() parses header, payload, checksum in that order. to get this working with AD, I need to change the parse order: header, checksum, payload.

if err != nil {
return nil, err
}

if (token.Flags & 0b1) == 0 {
return nil, fmt.Errorf("Got a Wrapped token that's not from the server")
}

key := state.ekey
if (token.Flags & 0b100) != 0 {
key = state.Subkey
}

_, err = token.Verify(key, keyusage.GSSAPI_ACCEPTOR_SEAL)
if err != nil {
return nil, err
}

pl := token.Payload
if len(pl) != 4 {
return nil, fmt.Errorf("Server send bad final token for SASL GSSAPI Handshake")
}

// We never want a security layer
b := [4]byte{0, 0, 0, 0}
payload := append(b[:], []byte(state.AuthZID)...)

encType, err := crypto.GetEtype(key.KeyType)
if err != nil {
return nil, err
}

token = &gssapi.WrapToken{
Flags: 0b100,
EC: uint16(encType.GetHMACBitLength() / 8),
RRC: 0,
SndSeqNum: 1,
Copy link

Choose a reason for hiding this comment

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

maybe properly track this in "state" instead of hardcoding the seq here

Payload: payload,
}

if err := token.SetCheckSum(key, keyusage.GSSAPI_INITIATOR_SEAL); err != nil {
return nil, err
}

return token.Marshal()
}
Loading