From 48b6f3563ecc7fe5d271aeaf1eef1bed3a7c9938 Mon Sep 17 00:00:00 2001 From: "Mgr. Vladimir Magyar" Date: Mon, 9 Sep 2019 11:04:44 +0200 Subject: [PATCH 1/6] Add DIGEST-MD5 bind support --- bind.go | 249 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 248 insertions(+), 1 deletion(-) diff --git a/bind.go b/bind.go index 59c3f5ef..cfc422e7 100644 --- a/bind.go +++ b/bind.go @@ -1,10 +1,16 @@ package ldap import ( + "bytes" + "crypto/md5" + enchex "encoding/hex" "errors" "fmt" + "io/ioutil" + "math/rand" + "strings" - "gopkg.in/asn1-ber.v1" + ber "gopkg.in/asn1-ber.v1" ) // SimpleBindRequest represents a username/password bind operation @@ -133,3 +139,244 @@ func (l *Conn) UnauthenticatedBind(username string) error { _, err := l.SimpleBind(req) return err } + +type DigestMD5BindRequest struct { + Host string + // Username is the name of the Directory object that the client wishes to bind as + Username string + // Password is the credentials to bind with + Password string + // Controls are optional controls to send with the bind request + Controls []Control +} + +func (bindRequest *DigestMD5BindRequest) encode() *ber.Packet { + 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, "DIGEST-MD5", "SASL Mech")) + request.AppendChild(auth) + return request +} + +type DigestMD5BindResult struct { + Controls []Control +} + +func (l *Conn) MD5Bind(host, username, password string) error { + req := &DigestMD5BindRequest{ + Host: host, + Username: username, + Password: password, + } + _, err := l.DigestMD5Bind(req) + return err +} + +func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*DigestMD5BindResult, error) { + if digestMD5BindRequest.Password == "" { + return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client")) + } + + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) + encodedBindRequest := digestMD5BindRequest.encode() + packet.AppendChild(encodedBindRequest) + if len(digestMD5BindRequest.Controls) > 0 { + packet.AppendChild(encodeControls(digestMD5BindRequest.Controls)) + } + + if l.Debug { + ber.PrintPacket(packet) + } + + msgCtx, err := l.sendMessage(packet) + if err != nil { + return nil, fmt.Errorf("send message: %w", err) + } + defer l.finishMessage(msgCtx) + + packetResponse, ok := <-msgCtx.responses + if !ok { + return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + } + packet, err = packetResponse.ReadPacket() + l.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + return nil, fmt.Errorf("read packet: %w", err) + } + + if l.Debug { + if err = addLDAPDescriptions(packet); err != nil { + return nil, err + } + ber.PrintPacket(packet) + } + + result := &DigestMD5BindResult{ + Controls: make([]Control, 0), + } + var params map[string]string + + if len(packet.Children) == 2 { + if len(packet.Children[1].Children) == 4 { + child := packet.Children[1].Children[0] + if child.Tag != ber.TagEnumerated { + return result, GetLDAPError(packet) + } + if child.Value.(int64) != 14 { + return result, GetLDAPError(packet) + } + child = packet.Children[1].Children[3] + if child.Tag != ber.TagObjectDescriptor { + return result, GetLDAPError(packet) + } + if child.Data == nil { + return result, GetLDAPError(packet) + } + data, _ := ioutil.ReadAll(child.Data) + params, err = parseParams(string(data)) + if err != nil { + return result, fmt.Errorf("parsing digest-challenge: %w", err) + } + } + } + + if params != nil { + resp := computeResponse( + params, + "ldap/"+strings.ToLower(digestMD5BindRequest.Host), + digestMD5BindRequest.Username, + digestMD5BindRequest.Password, + ) + packet = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) + + 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, "DIGEST-MD5", "SASL Mech")) + auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, resp, "Credentials")) + request.AppendChild(auth) + packet.AppendChild(request) + msgCtx, err := l.sendMessage(packet) + if err != nil { + return nil, fmt.Errorf("send message: %w", err) + } + defer l.finishMessage(msgCtx) + packetResponse, ok := <-msgCtx.responses + if !ok { + return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + } + packet, err = packetResponse.ReadPacket() + l.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + return nil, fmt.Errorf("read packet: %w", err) + } + } + + err = GetLDAPError(packet) + return result, err +} + +func parseParams(str string) (map[string]string, error) { + m := make(map[string]string) + var key, value string + var state int + for i := 0; i <= len(str); i++ { + switch state { + case 0: //reading key + if i == len(str) { + return nil, fmt.Errorf("syntax error on %d", i) + } + if str[i] != '=' { + key += string(str[i]) + continue + } + state = 1 + case 1: //reading value + if i == len(str) { + m[key] = value + break + } + switch str[i] { + case ',': + m[key] = value + state = 0 + key = "" + value = "" + case '"': + if value != "" { + return nil, fmt.Errorf("syntax error on %d", i) + } + state = 2 + default: + value += string(str[i]) + } + case 2: //inside quotes + if i == len(str) { + return nil, fmt.Errorf("syntax error on %d", i) + } + if str[i] != '"' { + value += string(str[i]) + } else { + state = 1 + } + } + } + return m, nil +} + +func computeResponse(params map[string]string, uri, username, password string) string { + nc := "00000001" + qop := "auth" + cnonce := enchex.EncodeToString(randomBytes(16)) + x := username + ":" + params["realm"] + ":" + password + y := md5Hash([]byte(x)) + + a1 := bytes.NewBuffer(y) + a1.WriteString(":" + params["nonce"] + ":" + cnonce) + if len(params["authzid"]) > 0 { + a1.WriteString(":" + params["authzid"]) + } + a2 := bytes.NewBuffer([]byte("AUTHENTICATE")) + a2.WriteString(":" + uri) + ha1 := enchex.EncodeToString(md5Hash(a1.Bytes())) + ha2 := enchex.EncodeToString(md5Hash(a2.Bytes())) + + kd := ha1 + kd += ":" + params["nonce"] + kd += ":" + nc + kd += ":" + cnonce + kd += ":" + qop + kd += ":" + ha2 + resp := enchex.EncodeToString(md5Hash([]byte(kd))) + return fmt.Sprintf( + `username="%s",realm="%s",nonce="%s",cnonce="%s",nc=00000001,qop=%s,digest-uri="%s",response=%s`, + username, + params["realm"], + params["nonce"], + cnonce, + qop, + uri, + resp, + ) +} + +func md5Hash(b []byte) []byte { + hasher := md5.New() + hasher.Write(b) + return hasher.Sum(nil) +} + +func randomBytes(len int) []byte { + b := make([]byte, len) + for i := 0; i < len; i++ { + b[i] = byte(rand.Intn(256)) + } + return b +} From 6525ac17c8de11727def07cebc1778dddb44b212 Mon Sep 17 00:00:00 2001 From: "Mgr. Vladimir Magyar" Date: Mon, 9 Sep 2019 11:13:41 +0200 Subject: [PATCH 2/6] Remove new 1.13 %w verb --- bind.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bind.go b/bind.go index cfc422e7..d6cda477 100644 --- a/bind.go +++ b/bind.go @@ -194,7 +194,7 @@ func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*Diges msgCtx, err := l.sendMessage(packet) if err != nil { - return nil, fmt.Errorf("send message: %w", err) + return nil, fmt.Errorf("send message: %s", err) } defer l.finishMessage(msgCtx) @@ -205,7 +205,7 @@ func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*Diges packet, err = packetResponse.ReadPacket() l.Debug.Printf("%d: got response %p", msgCtx.id, packet) if err != nil { - return nil, fmt.Errorf("read packet: %w", err) + return nil, fmt.Errorf("read packet: %s", err) } if l.Debug { @@ -239,7 +239,7 @@ func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*Diges data, _ := ioutil.ReadAll(child.Data) params, err = parseParams(string(data)) if err != nil { - return result, fmt.Errorf("parsing digest-challenge: %w", err) + return result, fmt.Errorf("parsing digest-challenge: %s", err) } } } @@ -265,7 +265,7 @@ func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*Diges packet.AppendChild(request) msgCtx, err := l.sendMessage(packet) if err != nil { - return nil, fmt.Errorf("send message: %w", err) + return nil, fmt.Errorf("send message: %s", err) } defer l.finishMessage(msgCtx) packetResponse, ok := <-msgCtx.responses @@ -275,7 +275,7 @@ func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*Diges packet, err = packetResponse.ReadPacket() l.Debug.Printf("%d: got response %p", msgCtx.id, packet) if err != nil { - return nil, fmt.Errorf("read packet: %w", err) + return nil, fmt.Errorf("read packet: %s", err) } } From 770217fe350feca3c29d61c6f983eb506352a2c2 Mon Sep 17 00:00:00 2001 From: "Mgr. Vladimir Magyar" Date: Mon, 3 Feb 2020 08:41:52 +0100 Subject: [PATCH 3/6] Fix vet and add docs --- bind.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bind.go b/bind.go index d6cda477..e578e49e 100644 --- a/bind.go +++ b/bind.go @@ -140,6 +140,7 @@ func (l *Conn) UnauthenticatedBind(username string) error { return err } +// DigestMD5BindRequest represents a digest-md5 bind operation type DigestMD5BindRequest struct { Host string // Username is the name of the Directory object that the client wishes to bind as @@ -161,10 +162,12 @@ func (bindRequest *DigestMD5BindRequest) encode() *ber.Packet { return request } +// DigestMD5BindResult contains the response from the server type DigestMD5BindResult struct { Controls []Control } +// MD5Bind performs a digest-md5 bind with the given host, username and password. func (l *Conn) MD5Bind(host, username, password string) error { req := &DigestMD5BindRequest{ Host: host, @@ -175,6 +178,7 @@ func (l *Conn) MD5Bind(host, username, password string) error { return err } +// DigestMD5Bind performs the digest-md5 bind operation defined in the given request func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*DigestMD5BindResult, error) { if digestMD5BindRequest.Password == "" { return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client")) @@ -263,7 +267,7 @@ func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*Diges auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, resp, "Credentials")) request.AppendChild(auth) packet.AppendChild(request) - msgCtx, err := l.sendMessage(packet) + msgCtx, err = l.sendMessage(packet) if err != nil { return nil, fmt.Errorf("send message: %s", err) } From a707f429daffbf78224dee71447d1f64c3464bcf Mon Sep 17 00:00:00 2001 From: "Mgr. Vladimir Magyar" Date: Mon, 3 Feb 2020 09:40:44 +0100 Subject: [PATCH 4/6] Fix go mod --- go.mod | 5 +---- go.sum | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 531c2ba8..1d0aa72e 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,4 @@ module github.com/go-ldap/ldap go 1.13 -require ( - github.com/go-asn1-ber/asn1-ber v1.3.1 - github.com/microo8/ldap v3.0.4+incompatible // indirect -) +require github.com/go-asn1-ber/asn1-ber v1.3.1 diff --git a/go.sum b/go.sum index 1bbcb265..c8b9085b 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,2 @@ github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/microo8/ldap v3.0.4+incompatible h1:oF739bJfEO0Zq0mDiGHLRHD5P4cZ6pfyNaQDiLVgBqQ= -github.com/microo8/ldap v3.0.4+incompatible/go.mod h1:3JLmXuKHy6SfeHKBH614137akF4TnAWS9SFEU93d988= From ef41a0d7f607451a3d25895eebedd0cf87b3fe5c Mon Sep 17 00:00:00 2001 From: "Mgr. Vladimir Magyar" Date: Mon, 3 Feb 2020 09:50:57 +0100 Subject: [PATCH 5/6] Fix go mod --- go.mod | 7 +++++-- go.sum | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 1d0aa72e..e6b07d97 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,8 @@ -module github.com/go-ldap/ldap +module github.com/microo8/ldap go 1.13 -require github.com/go-asn1-ber/asn1-ber v1.3.1 +require ( + github.com/go-asn1-ber/asn1-ber v1.3.1 + github.com/go-ldap/ldap v3.0.3+incompatible // indirect +) diff --git a/go.sum b/go.sum index c8b9085b..943a4af2 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHjsvuZyatzwk= +github.com/go-ldap/ldap v3.0.3+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= From 162300a57330e6f85758062b4b85cb1fae999798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vl=CE=B1do=20Magyar?= Date: Wed, 5 Feb 2020 09:07:47 +0100 Subject: [PATCH 6/6] Update go.mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index e6b07d97..d0dfceb1 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/microo8/ldap +module github.com/go-ldap/ldap go 1.13