From 96673b16f8bfbb93d2152e7d9087668e79c1d3be Mon Sep 17 00:00:00 2001 From: Gregor Reitzenstein Date: Tue, 7 Sep 2021 22:50:51 +0200 Subject: [PATCH] Adds GSSAPI Bind support to go-ldap This adds a new Mechanism for SASL Binds using GSSAPI. It does *not* implement security layers. It does *not* implement any of the newer GS2 mechanism. It does *not* implement the KERBEROSV5 mechanism. It also due to implementing GSSAPI specifically, not allow for channel bindings. Use this with caution. Closes #115. --- bind.go | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 +- gssapi.go | 130 +++++++++++++++++++++++++++++++++++++++++++++ v3/bind.go | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++ v3/gssapi.go | 130 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 552 insertions(+), 1 deletion(-) create mode 100644 gssapi.go create mode 100644 v3/gssapi.go diff --git a/bind.go b/bind.go index 9bc57482..3ac1cd5a 100644 --- a/bind.go +++ b/bind.go @@ -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 @@ -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/" + 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")) + 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 { + 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) + 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 +} diff --git a/go.mod b/go.mod index 40576051..8a0fa002 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/gssapi.go b/gssapi.go new file mode 100644 index 00000000..bfcfc3f4 --- /dev/null +++ b/gssapi.go @@ -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{}) + 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) + 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, + Payload: payload, + } + + if err := token.SetCheckSum(key, keyusage.GSSAPI_INITIATOR_SEAL); err != nil { + return nil, err + } + + return token.Marshal() +} diff --git a/v3/bind.go b/v3/bind.go index 9bc57482..3ac1cd5a 100644 --- a/v3/bind.go +++ b/v3/bind.go @@ -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 @@ -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/" + 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")) + 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 { + 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) + 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 +} diff --git a/v3/gssapi.go b/v3/gssapi.go new file mode 100644 index 00000000..bfcfc3f4 --- /dev/null +++ b/v3/gssapi.go @@ -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{}) + 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) + 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, + Payload: payload, + } + + if err := token.SetCheckSum(key, keyusage.GSSAPI_INITIATOR_SEAL); err != nil { + return nil, err + } + + return token.Marshal() +}