-
Notifications
You must be signed in to change notification settings - Fork 365
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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/<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")) | ||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check for err of |
||
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 | ||
} |
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{}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
} |
There was a problem hiding this comment.
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
iflen(token) > 0