1package ldap 2 3import ( 4 "bytes" 5 "crypto/md5" 6 enchex "encoding/hex" 7 "errors" 8 "fmt" 9 "io/ioutil" 10 "math/rand" 11 "strings" 12 13 "github.com/Azure/go-ntlmssp" 14 ber "github.com/go-asn1-ber/asn1-ber" 15) 16 17// SimpleBindRequest represents a username/password bind operation 18type SimpleBindRequest struct { 19 // Username is the name of the Directory object that the client wishes to bind as 20 Username string 21 // Password is the credentials to bind with 22 Password string 23 // Controls are optional controls to send with the bind request 24 Controls []Control 25 // AllowEmptyPassword sets whether the client allows binding with an empty password 26 // (normally used for unauthenticated bind). 27 AllowEmptyPassword bool 28} 29 30// SimpleBindResult contains the response from the server 31type SimpleBindResult struct { 32 Controls []Control 33} 34 35// NewSimpleBindRequest returns a bind request 36func NewSimpleBindRequest(username string, password string, controls []Control) *SimpleBindRequest { 37 return &SimpleBindRequest{ 38 Username: username, 39 Password: password, 40 Controls: controls, 41 AllowEmptyPassword: false, 42 } 43} 44 45func (req *SimpleBindRequest) appendTo(envelope *ber.Packet) error { 46 pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") 47 pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) 48 pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.Username, "User Name")) 49 pkt.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, req.Password, "Password")) 50 51 envelope.AppendChild(pkt) 52 if len(req.Controls) > 0 { 53 envelope.AppendChild(encodeControls(req.Controls)) 54 } 55 56 return nil 57} 58 59// SimpleBind performs the simple bind operation defined in the given request 60func (l *Conn) SimpleBind(simpleBindRequest *SimpleBindRequest) (*SimpleBindResult, error) { 61 if simpleBindRequest.Password == "" && !simpleBindRequest.AllowEmptyPassword { 62 return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client")) 63 } 64 65 msgCtx, err := l.doRequest(simpleBindRequest) 66 if err != nil { 67 return nil, err 68 } 69 defer l.finishMessage(msgCtx) 70 71 packet, err := l.readPacket(msgCtx) 72 if err != nil { 73 return nil, err 74 } 75 76 result := &SimpleBindResult{ 77 Controls: make([]Control, 0), 78 } 79 80 if len(packet.Children) == 3 { 81 for _, child := range packet.Children[2].Children { 82 decodedChild, decodeErr := DecodeControl(child) 83 if decodeErr != nil { 84 return nil, fmt.Errorf("failed to decode child control: %s", decodeErr) 85 } 86 result.Controls = append(result.Controls, decodedChild) 87 } 88 } 89 90 err = GetLDAPError(packet) 91 return result, err 92} 93 94// Bind performs a bind with the given username and password. 95// 96// It does not allow unauthenticated bind (i.e. empty password). Use the UnauthenticatedBind method 97// for that. 98func (l *Conn) Bind(username, password string) error { 99 req := &SimpleBindRequest{ 100 Username: username, 101 Password: password, 102 AllowEmptyPassword: false, 103 } 104 _, err := l.SimpleBind(req) 105 return err 106} 107 108// UnauthenticatedBind performs an unauthenticated bind. 109// 110// A username may be provided for trace (e.g. logging) purpose only, but it is normally not 111// authenticated or otherwise validated by the LDAP server. 112// 113// See https://tools.ietf.org/html/rfc4513#section-5.1.2 . 114// See https://tools.ietf.org/html/rfc4513#section-6.3.1 . 115func (l *Conn) UnauthenticatedBind(username string) error { 116 req := &SimpleBindRequest{ 117 Username: username, 118 Password: "", 119 AllowEmptyPassword: true, 120 } 121 _, err := l.SimpleBind(req) 122 return err 123} 124 125// DigestMD5BindRequest represents a digest-md5 bind operation 126type DigestMD5BindRequest struct { 127 Host string 128 // Username is the name of the Directory object that the client wishes to bind as 129 Username string 130 // Password is the credentials to bind with 131 Password string 132 // Controls are optional controls to send with the bind request 133 Controls []Control 134} 135 136func (req *DigestMD5BindRequest) appendTo(envelope *ber.Packet) error { 137 request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") 138 request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) 139 request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) 140 141 auth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication") 142 auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "DIGEST-MD5", "SASL Mech")) 143 request.AppendChild(auth) 144 envelope.AppendChild(request) 145 if len(req.Controls) > 0 { 146 envelope.AppendChild(encodeControls(req.Controls)) 147 } 148 return nil 149} 150 151// DigestMD5BindResult contains the response from the server 152type DigestMD5BindResult struct { 153 Controls []Control 154} 155 156// MD5Bind performs a digest-md5 bind with the given host, username and password. 157func (l *Conn) MD5Bind(host, username, password string) error { 158 req := &DigestMD5BindRequest{ 159 Host: host, 160 Username: username, 161 Password: password, 162 } 163 _, err := l.DigestMD5Bind(req) 164 return err 165} 166 167// DigestMD5Bind performs the digest-md5 bind operation defined in the given request 168func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*DigestMD5BindResult, error) { 169 if digestMD5BindRequest.Password == "" { 170 return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client")) 171 } 172 173 msgCtx, err := l.doRequest(digestMD5BindRequest) 174 if err != nil { 175 return nil, err 176 } 177 defer l.finishMessage(msgCtx) 178 179 packet, err := l.readPacket(msgCtx) 180 if err != nil { 181 return nil, err 182 } 183 l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 184 if l.Debug { 185 if err = addLDAPDescriptions(packet); err != nil { 186 return nil, err 187 } 188 ber.PrintPacket(packet) 189 } 190 191 result := &DigestMD5BindResult{ 192 Controls: make([]Control, 0), 193 } 194 var params map[string]string 195 if len(packet.Children) == 2 { 196 if len(packet.Children[1].Children) == 4 { 197 child := packet.Children[1].Children[0] 198 if child.Tag != ber.TagEnumerated { 199 return result, GetLDAPError(packet) 200 } 201 if child.Value.(int64) != 14 { 202 return result, GetLDAPError(packet) 203 } 204 child = packet.Children[1].Children[3] 205 if child.Tag != ber.TagObjectDescriptor { 206 return result, GetLDAPError(packet) 207 } 208 if child.Data == nil { 209 return result, GetLDAPError(packet) 210 } 211 data, _ := ioutil.ReadAll(child.Data) 212 params, err = parseParams(string(data)) 213 if err != nil { 214 return result, fmt.Errorf("parsing digest-challenge: %s", err) 215 } 216 } 217 } 218 219 if params != nil { 220 resp := computeResponse( 221 params, 222 "ldap/"+strings.ToLower(digestMD5BindRequest.Host), 223 digestMD5BindRequest.Username, 224 digestMD5BindRequest.Password, 225 ) 226 packet = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 227 packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) 228 229 request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") 230 request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) 231 request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) 232 233 auth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication") 234 auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "DIGEST-MD5", "SASL Mech")) 235 auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, resp, "Credentials")) 236 request.AppendChild(auth) 237 packet.AppendChild(request) 238 msgCtx, err = l.sendMessage(packet) 239 if err != nil { 240 return nil, fmt.Errorf("send message: %s", err) 241 } 242 defer l.finishMessage(msgCtx) 243 packetResponse, ok := <-msgCtx.responses 244 if !ok { 245 return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) 246 } 247 packet, err = packetResponse.ReadPacket() 248 l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 249 if err != nil { 250 return nil, fmt.Errorf("read packet: %s", err) 251 } 252 } 253 254 err = GetLDAPError(packet) 255 return result, err 256} 257 258func parseParams(str string) (map[string]string, error) { 259 m := make(map[string]string) 260 var key, value string 261 var state int 262 for i := 0; i <= len(str); i++ { 263 switch state { 264 case 0: //reading key 265 if i == len(str) { 266 return nil, fmt.Errorf("syntax error on %d", i) 267 } 268 if str[i] != '=' { 269 key += string(str[i]) 270 continue 271 } 272 state = 1 273 case 1: //reading value 274 if i == len(str) { 275 m[key] = value 276 break 277 } 278 switch str[i] { 279 case ',': 280 m[key] = value 281 state = 0 282 key = "" 283 value = "" 284 case '"': 285 if value != "" { 286 return nil, fmt.Errorf("syntax error on %d", i) 287 } 288 state = 2 289 default: 290 value += string(str[i]) 291 } 292 case 2: //inside quotes 293 if i == len(str) { 294 return nil, fmt.Errorf("syntax error on %d", i) 295 } 296 if str[i] != '"' { 297 value += string(str[i]) 298 } else { 299 state = 1 300 } 301 } 302 } 303 return m, nil 304} 305 306func computeResponse(params map[string]string, uri, username, password string) string { 307 nc := "00000001" 308 qop := "auth" 309 cnonce := enchex.EncodeToString(randomBytes(16)) 310 x := username + ":" + params["realm"] + ":" + password 311 y := md5Hash([]byte(x)) 312 313 a1 := bytes.NewBuffer(y) 314 a1.WriteString(":" + params["nonce"] + ":" + cnonce) 315 if len(params["authzid"]) > 0 { 316 a1.WriteString(":" + params["authzid"]) 317 } 318 a2 := bytes.NewBuffer([]byte("AUTHENTICATE")) 319 a2.WriteString(":" + uri) 320 ha1 := enchex.EncodeToString(md5Hash(a1.Bytes())) 321 ha2 := enchex.EncodeToString(md5Hash(a2.Bytes())) 322 323 kd := ha1 324 kd += ":" + params["nonce"] 325 kd += ":" + nc 326 kd += ":" + cnonce 327 kd += ":" + qop 328 kd += ":" + ha2 329 resp := enchex.EncodeToString(md5Hash([]byte(kd))) 330 return fmt.Sprintf( 331 `username="%s",realm="%s",nonce="%s",cnonce="%s",nc=00000001,qop=%s,digest-uri="%s",response=%s`, 332 username, 333 params["realm"], 334 params["nonce"], 335 cnonce, 336 qop, 337 uri, 338 resp, 339 ) 340} 341 342func md5Hash(b []byte) []byte { 343 hasher := md5.New() 344 hasher.Write(b) 345 return hasher.Sum(nil) 346} 347 348func randomBytes(len int) []byte { 349 b := make([]byte, len) 350 for i := 0; i < len; i++ { 351 b[i] = byte(rand.Intn(256)) 352 } 353 return b 354} 355 356var externalBindRequest = requestFunc(func(envelope *ber.Packet) error { 357 pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") 358 pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) 359 pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) 360 361 saslAuth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication") 362 saslAuth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "EXTERNAL", "SASL Mech")) 363 saslAuth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "SASL Cred")) 364 365 pkt.AppendChild(saslAuth) 366 367 envelope.AppendChild(pkt) 368 369 return nil 370}) 371 372// ExternalBind performs SASL/EXTERNAL authentication. 373// 374// Use ldap.DialURL("ldapi://") to connect to the Unix socket before ExternalBind. 375// 376// See https://tools.ietf.org/html/rfc4422#appendix-A 377func (l *Conn) ExternalBind() error { 378 msgCtx, err := l.doRequest(externalBindRequest) 379 if err != nil { 380 return err 381 } 382 defer l.finishMessage(msgCtx) 383 384 packet, err := l.readPacket(msgCtx) 385 if err != nil { 386 return err 387 } 388 389 return GetLDAPError(packet) 390} 391 392// NTLMBind performs an NTLMSSP bind leveraging https://github.com/Azure/go-ntlmssp 393 394// NTLMBindRequest represents an NTLMSSP bind operation 395type NTLMBindRequest struct { 396 // Domain is the AD Domain to authenticate too. If not specified, it will be grabbed from the NTLMSSP Challenge 397 Domain string 398 // Username is the name of the Directory object that the client wishes to bind as 399 Username string 400 // Password is the credentials to bind with 401 Password string 402 // Hash is the hex NTLM hash to bind with. Password or hash must be provided 403 Hash string 404 // Controls are optional controls to send with the bind request 405 Controls []Control 406} 407 408func (req *NTLMBindRequest) appendTo(envelope *ber.Packet) error { 409 request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") 410 request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) 411 request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) 412 413 // generate an NTLMSSP Negotiation message for the specified domain (it can be blank) 414 negMessage, err := ntlmssp.NewNegotiateMessage(req.Domain, "") 415 if err != nil { 416 return fmt.Errorf("err creating negmessage: %s", err) 417 } 418 419 // append the generated NTLMSSP message as a TagEnumerated BER value 420 auth := ber.Encode(ber.ClassContext, ber.TypePrimitive, ber.TagEnumerated, negMessage, "authentication") 421 request.AppendChild(auth) 422 envelope.AppendChild(request) 423 if len(req.Controls) > 0 { 424 envelope.AppendChild(encodeControls(req.Controls)) 425 } 426 return nil 427} 428 429// NTLMBindResult contains the response from the server 430type NTLMBindResult struct { 431 Controls []Control 432} 433 434// NTLMBind performs an NTLMSSP Bind with the given domain, username and password 435func (l *Conn) NTLMBind(domain, username, password string) error { 436 req := &NTLMBindRequest{ 437 Domain: domain, 438 Username: username, 439 Password: password, 440 } 441 _, err := l.NTLMChallengeBind(req) 442 return err 443} 444 445// NTLMBindWithHash performs an NTLM Bind with an NTLM hash instead of plaintext password (pass-the-hash) 446func (l *Conn) NTLMBindWithHash(domain, username, hash string) error { 447 req := &NTLMBindRequest{ 448 Domain: domain, 449 Username: username, 450 Hash: hash, 451 } 452 _, err := l.NTLMChallengeBind(req) 453 return err 454} 455 456// NTLMChallengeBind performs the NTLMSSP bind operation defined in the given request 457func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindResult, error) { 458 if ntlmBindRequest.Password == "" && ntlmBindRequest.Hash == "" { 459 return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client")) 460 } 461 462 msgCtx, err := l.doRequest(ntlmBindRequest) 463 if err != nil { 464 return nil, err 465 } 466 defer l.finishMessage(msgCtx) 467 packet, err := l.readPacket(msgCtx) 468 if err != nil { 469 return nil, err 470 } 471 l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 472 if l.Debug { 473 if err = addLDAPDescriptions(packet); err != nil { 474 return nil, err 475 } 476 ber.PrintPacket(packet) 477 } 478 result := &NTLMBindResult{ 479 Controls: make([]Control, 0), 480 } 481 var ntlmsspChallenge []byte 482 483 // now find the NTLM Response Message 484 if len(packet.Children) == 2 { 485 if len(packet.Children[1].Children) == 3 { 486 child := packet.Children[1].Children[1] 487 ntlmsspChallenge = child.ByteValue 488 // Check to make sure we got the right message. It will always start with NTLMSSP 489 if len(ntlmsspChallenge) < 7 || !bytes.Equal(ntlmsspChallenge[:7], []byte("NTLMSSP")) { 490 return result, GetLDAPError(packet) 491 } 492 l.Debug.Printf("%d: found ntlmssp challenge", msgCtx.id) 493 } 494 } 495 if ntlmsspChallenge != nil { 496 var err error 497 var responseMessage []byte 498 // generate a response message to the challenge with the given Username/Password if password is provided 499 if ntlmBindRequest.Password != "" { 500 responseMessage, err = ntlmssp.ProcessChallenge(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Password) 501 } else if ntlmBindRequest.Hash != "" { 502 responseMessage, err = ntlmssp.ProcessChallengeWithHash(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Hash) 503 } else { 504 err = fmt.Errorf("need a password or hash to generate reply") 505 } 506 if err != nil { 507 return result, fmt.Errorf("parsing ntlm-challenge: %s", err) 508 } 509 packet = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 510 packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) 511 512 request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") 513 request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) 514 request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) 515 516 // append the challenge response message as a TagEmbeddedPDV BER value 517 auth := ber.Encode(ber.ClassContext, ber.TypePrimitive, ber.TagEmbeddedPDV, responseMessage, "authentication") 518 519 request.AppendChild(auth) 520 packet.AppendChild(request) 521 msgCtx, err = l.sendMessage(packet) 522 if err != nil { 523 return nil, fmt.Errorf("send message: %s", err) 524 } 525 defer l.finishMessage(msgCtx) 526 packetResponse, ok := <-msgCtx.responses 527 if !ok { 528 return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) 529 } 530 packet, err = packetResponse.ReadPacket() 531 l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 532 if err != nil { 533 return nil, fmt.Errorf("read packet: %s", err) 534 } 535 536 } 537 538 err = GetLDAPError(packet) 539 return result, err 540} 541