From cf2967adfbfb8443ad7eaf77d0c1107d390cf322 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:46:27 +0000 Subject: [PATCH] build(deps): bump github.com/go-ldap/ldap/v3 from 3.4.12 to 3.4.13 Bumps [github.com/go-ldap/ldap/v3](https://github.com/go-ldap/ldap) from 3.4.12 to 3.4.13. - [Release notes](https://github.com/go-ldap/ldap/releases) - [Commits](https://github.com/go-ldap/ldap/compare/v3.4.12...v3.4.13) --- updated-dependencies: - dependency-name: github.com/go-ldap/ldap/v3 dependency-version: 3.4.13 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 4 +- go.sum | 8 +- vendor/github.com/Azure/go-ntlmssp/.gitignore | 2 + .../github.com/Azure/go-ntlmssp/.golangci.yml | 39 ++ .../github.com/Azure/go-ntlmssp/.travis.yml | 17 - .../github.com/Azure/go-ntlmssp/E2E_README.md | 107 +++++ vendor/github.com/Azure/go-ntlmssp/README.md | 22 +- .../Azure/go-ntlmssp/authenticate_message.go | 155 ++++---- .../github.com/Azure/go-ntlmssp/authheader.go | 89 ++--- vendor/github.com/Azure/go-ntlmssp/avids.go | 3 + .../Azure/go-ntlmssp/challenge_message.go | 9 +- .../Azure/go-ntlmssp/internal/md4/README.md | 21 + .../Azure/go-ntlmssp/internal/md4/md4.go | 113 ++++++ .../Azure/go-ntlmssp/internal/md4/md4block.go | 91 +++++ .../Azure/go-ntlmssp/messageheader.go | 3 + .../Azure/go-ntlmssp/negotiate_flags.go | 5 +- .../Azure/go-ntlmssp/negotiate_message.go | 27 +- .../github.com/Azure/go-ntlmssp/negotiator.go | 376 +++++++++++++----- vendor/github.com/Azure/go-ntlmssp/nlmp.go | 20 +- vendor/github.com/Azure/go-ntlmssp/unicode.go | 7 +- .../github.com/Azure/go-ntlmssp/varfield.go | 5 +- vendor/github.com/Azure/go-ntlmssp/version.go | 3 + vendor/github.com/go-ldap/ldap/v3/control.go | 12 +- vendor/github.com/go-ldap/ldap/v3/error.go | 7 + vendor/github.com/go-ldap/ldap/v3/extended.go | 19 +- .../go-ldap/ldap/v3/postaladdress.go | 130 ++++++ vendor/github.com/go-ldap/ldap/v3/search.go | 2 +- vendor/github.com/go-ldap/ldap/v3/whoami.go | 76 +--- vendor/modules.txt | 9 +- 29 files changed, 1027 insertions(+), 354 deletions(-) create mode 100644 vendor/github.com/Azure/go-ntlmssp/.gitignore create mode 100644 vendor/github.com/Azure/go-ntlmssp/.golangci.yml delete mode 100644 vendor/github.com/Azure/go-ntlmssp/.travis.yml create mode 100644 vendor/github.com/Azure/go-ntlmssp/E2E_README.md create mode 100644 vendor/github.com/Azure/go-ntlmssp/internal/md4/README.md create mode 100644 vendor/github.com/Azure/go-ntlmssp/internal/md4/md4.go create mode 100644 vendor/github.com/Azure/go-ntlmssp/internal/md4/md4block.go create mode 100644 vendor/github.com/go-ldap/ldap/v3/postaladdress.go diff --git a/go.mod b/go.mod index 5fa6ecac13..a023dd006b 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/render v1.0.3 github.com/go-jose/go-jose/v3 v3.0.4 - github.com/go-ldap/ldap/v3 v3.4.12 + github.com/go-ldap/ldap/v3 v3.4.13 github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3 github.com/go-micro/plugins/v4/client/grpc v1.2.1 github.com/go-micro/plugins/v4/logger/zerolog v1.2.0 @@ -124,7 +124,7 @@ require ( contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect filippo.io/edwards25519 v1.1.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/Azure/go-ntlmssp v0.1.0 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect diff --git a/go.sum b/go.sum index 8896b10b66..0a7789eb7a 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQ github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= -github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= -github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A= +github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -408,8 +408,8 @@ github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBj github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-ldap/ldap/v3 v3.1.7/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= -github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= -github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= +github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= +github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3 h1:sfz1YppV05y4sYaW7kXZtrocU/+vimnIWt4cxAYh7+o= github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3/go.mod h1:ZXFhGda43Z2TVbfGZefXyMJzsDHhCh0go3bZUcwTx7o= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= diff --git a/vendor/github.com/Azure/go-ntlmssp/.gitignore b/vendor/github.com/Azure/go-ntlmssp/.gitignore new file mode 100644 index 0000000000..b8a713d11a --- /dev/null +++ b/vendor/github.com/Azure/go-ntlmssp/.gitignore @@ -0,0 +1,2 @@ +.vscode +*.exe \ No newline at end of file diff --git a/vendor/github.com/Azure/go-ntlmssp/.golangci.yml b/vendor/github.com/Azure/go-ntlmssp/.golangci.yml new file mode 100644 index 0000000000..d01771965c --- /dev/null +++ b/vendor/github.com/Azure/go-ntlmssp/.golangci.yml @@ -0,0 +1,39 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +version: "2" +linters: + enable: + - bodyclose + - godox + - nakedret + - predeclared + - unconvert + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ + - internal/md4 + rules: + - path: negotiate_flags.go + linters: + - unused + - path: negotiator.go + text: "QF1001:" +formatters: + enable: + - gofumpt + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ + - internal/md4 diff --git a/vendor/github.com/Azure/go-ntlmssp/.travis.yml b/vendor/github.com/Azure/go-ntlmssp/.travis.yml deleted file mode 100644 index 23c95fe951..0000000000 --- a/vendor/github.com/Azure/go-ntlmssp/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -sudo: false - -language: go - -before_script: - - go get -u golang.org/x/lint/golint - -go: - - 1.10.x - - master - -script: - - test -z "$(gofmt -s -l . | tee /dev/stderr)" - - test -z "$(golint ./... | tee /dev/stderr)" - - go vet ./... - - go build -v ./... - - go test -v ./... diff --git a/vendor/github.com/Azure/go-ntlmssp/E2E_README.md b/vendor/github.com/Azure/go-ntlmssp/E2E_README.md new file mode 100644 index 0000000000..a77a6c8d04 --- /dev/null +++ b/vendor/github.com/Azure/go-ntlmssp/E2E_README.md @@ -0,0 +1,107 @@ +# E2E NTLM Tests + +This directory contains end-to-end tests for the go-ntlmssp library that test against real NTLM servers. + +## Running E2E Tests Locally + +### Prerequisites + +- Windows machine with IIS capabilities +- Go 1.20 or later +- Administrator privileges (for IIS setup) + +### Setup + +1. **Enable IIS with Windows Authentication:** + ```powershell + # Run as Administrator + Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServerRole -All + Enable-WindowsOptionalFeature -Online -FeatureName IIS-WindowsAuthentication -All + ``` + +2. **Create test site:** + ```powershell + Import-Module WebAdministration + New-Website -Name "ntlmtest" -Port 8080 -PhysicalPath "C:\inetpub\wwwroot" + Set-WebConfigurationProperty -Filter "/system.webServer/security/authentication/anonymousAuthentication" -Name enabled -Value false -PSPath "IIS:\Sites\ntlmtest" + Set-WebConfigurationProperty -Filter "/system.webServer/security/authentication/windowsAuthentication" -Name enabled -Value true -PSPath "IIS:\Sites\ntlmtest" + ``` + +3. **Set environment variables:** + ```powershell + $env:NTLM_TEST_URL = "http://localhost:8080/" + $env:NTLM_TEST_USER = "your_username" + $env:NTLM_TEST_PASSWORD = "your_password" + $env:NTLM_TEST_DOMAIN = "your_domain" # Optional + ``` + + > **Note**: The setup script automatically generates a random secure password if none is provided. For security, avoid hardcoded passwords in scripts or CI environments. + +4. **Run tests:** + ```bash + go test -v -tags=e2e ./e2e -run TestNTLM_E2E + ``` + +## GitHub Actions + +The E2E tests run automatically in GitHub Actions on Windows runners. The workflow: + +1. Sets up a clean Windows Server environment +2. Generates a random secure password for the test user +3. Creates a test user account with the random password +4. Configures IIS with Windows Authentication +5. Runs the E2E tests against the real NTLM server +5. Cleans up resources + +## Test Coverage + +The E2E tests cover: + +- ✅ Basic NTLM authentication flow +- ✅ UPN format usernames (`user@domain.com`) +- ✅ SAM format usernames (`DOMAIN\user`) +- ✅ Authentication failure scenarios +- ✅ Server accessibility checks +- ✅ Context cancellation handling +- ✅ Direct ProcessChallenge function testing + +## Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `NTLM_TEST_URL` | URL of NTLM-enabled server | `http://localhost:8080/` | +| `NTLM_TEST_USER` | Username for authentication | `$USERNAME` (Windows) | +| `NTLM_TEST_PASSWORD` | Password for authentication | Required | +| `NTLM_TEST_DOMAIN` | Domain for authentication | `$USERDOMAIN` (Windows) | + +## Troubleshooting + +### Common Issues + +1. **"No username available"** - Set `NTLM_TEST_USER` environment variable +2. **"No password available"** - Set `NTLM_TEST_PASSWORD` environment variable +3. **Connection refused** - Ensure IIS is running and accessible on the specified port +4. **401 Unauthorized** - Check that Windows Authentication is enabled and working + +### IIS Debugging + +Check IIS status: +```powershell +Get-Website +Get-WebApplication +Get-WebConfigurationProperty -Filter "/system.webServer/security/authentication/windowsAuthentication" -Name enabled -PSPath "IIS:\Sites\Default Web Site" +``` + +View IIS logs: +```powershell +Get-Content "C:\inetpub\logs\LogFiles\W3SVC1\*.log" | Select-Object -Last 50 +``` + +## Security Note + +These tests use real authentication credentials. In CI/CD: +- Test credentials are generated dynamically per job +- Credentials are cleaned up after each test run +- No persistent credentials are stored + +For local development, use test accounts or ensure credentials are not committed to version control. \ No newline at end of file diff --git a/vendor/github.com/Azure/go-ntlmssp/README.md b/vendor/github.com/Azure/go-ntlmssp/README.md index 55cdcefab7..879818390f 100644 --- a/vendor/github.com/Azure/go-ntlmssp/README.md +++ b/vendor/github.com/Azure/go-ntlmssp/README.md @@ -1,22 +1,32 @@ # go-ntlmssp -Golang package that provides NTLM/Negotiate authentication over HTTP -[![GoDoc](https://godoc.org/github.com/Azure/go-ntlmssp?status.svg)](https://godoc.org/github.com/Azure/go-ntlmssp) [![Build Status](https://travis-ci.org/Azure/go-ntlmssp.svg?branch=dev)](https://travis-ci.org/Azure/go-ntlmssp) +[![Go Reference](https://pkg.go.dev/badge/github.com/Azure/go-ntlmssp.svg)](https://pkg.go.dev/github.com/Azure/go-ntlmssp) [![Test](https://github.com/Azure/go-ntlmssp/actions/workflows/test.yml/badge.svg)](https://github.com/Azure/go-ntlmssp/actions/workflows/test.yml) -Protocol details from https://msdn.microsoft.com/en-us/library/cc236621.aspx -Implementation hints from http://davenport.sourceforge.net/ntlm.html +Go package that provides NTLM/Negotiate authentication over HTTP + +* NTLM protocol details from https://msdn.microsoft.com/en-us/library/cc236621.aspx +* NTLM over HTTP details from https://datatracker.ietf.org/doc/html/rfc4559 +* Implementation hints from http://davenport.sourceforge.net/ntlm.html This package only implements authentication, no key exchange or encryption. It only supports Unicode (UTF16LE) encoding of protocol strings, no OEM encoding. This package implements NTLMv2. -# Usage +# Installation + +To install the package, use `go get`: +```bash +go get github.com/Azure/go-ntlmssp ``` + +# Usage + +```go url, user, password := "http://www.example.com/secrets", "robpike", "pw123" client := &http.Client{ Transport: ntlmssp.Negotiator{ - RoundTripper:&http.Transport{}, + RoundTripper: &http.Transport{}, }, } diff --git a/vendor/github.com/Azure/go-ntlmssp/authenticate_message.go b/vendor/github.com/Azure/go-ntlmssp/authenticate_message.go index ab183db6ad..291696fe83 100644 --- a/vendor/github.com/Azure/go-ntlmssp/authenticate_message.go +++ b/vendor/github.com/Azure/go-ntlmssp/authenticate_message.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + package ntlmssp import ( @@ -14,8 +17,9 @@ type authenicateMessage struct { LmChallengeResponse []byte NtChallengeResponse []byte - TargetName string - UserName string + DomainName string + UserName string + Workstation string // only set if negotiateFlag_NTLMSSP_NEGOTIATE_KEY_EXCH EncryptedRandomSessionKey []byte @@ -29,20 +33,20 @@ type authenticateMessageFields struct { messageHeader LmChallengeResponse varField NtChallengeResponse varField - TargetName varField + DomainName varField UserName varField Workstation varField _ [8]byte NegotiateFlags negotiateFlags } -func (m authenicateMessage) MarshalBinary() ([]byte, error) { +func (m *authenicateMessage) MarshalBinary() ([]byte, error) { if !m.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEUNICODE) { - return nil, errors.New("Only unicode is supported") + return nil, errors.New("only unicode is supported") } - target, user := toUnicode(m.TargetName), toUnicode(m.UserName) - workstation := toUnicode("") + domain, user := toUnicode(m.DomainName), toUnicode(m.UserName) + workstation := toUnicode(m.Workstation) ptr := binary.Size(&authenticateMessageFields{}) f := authenticateMessageFields{ @@ -50,7 +54,7 @@ func (m authenicateMessage) MarshalBinary() ([]byte, error) { NegotiateFlags: m.NegotiateFlags, LmChallengeResponse: newVarField(&ptr, len(m.LmChallengeResponse)), NtChallengeResponse: newVarField(&ptr, len(m.NtChallengeResponse)), - TargetName: newVarField(&ptr, len(target)), + DomainName: newVarField(&ptr, len(domain)), UserName: newVarField(&ptr, len(user)), Workstation: newVarField(&ptr, len(workstation)), } @@ -67,7 +71,7 @@ func (m authenicateMessage) MarshalBinary() ([]byte, error) { if err := binary.Write(&b, binary.LittleEndian, &m.NtChallengeResponse); err != nil { return nil, err } - if err := binary.Write(&b, binary.LittleEndian, &target); err != nil { + if err := binary.Write(&b, binary.LittleEndian, &domain); err != nil { return nil, err } if err := binary.Write(&b, binary.LittleEndian, &user); err != nil { @@ -80,80 +84,54 @@ func (m authenicateMessage) MarshalBinary() ([]byte, error) { return b.Bytes(), nil } -//ProcessChallenge crafts an AUTHENTICATE message in response to the CHALLENGE message -//that was received from the server -func ProcessChallenge(challengeMessageData []byte, user, password string, domainNeeded bool) ([]byte, error) { - if user == "" && password == "" { - return nil, errors.New("Anonymous authentication not supported") - } - - var cm challengeMessage - if err := cm.UnmarshalBinary(challengeMessageData); err != nil { - return nil, err - } - - if cm.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATELMKEY) { - return nil, errors.New("Only NTLM v2 is supported, but server requested v1 (NTLMSSP_NEGOTIATE_LM_KEY)") - } - if cm.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEKEYEXCH) { - return nil, errors.New("Key exchange requested but not supported (NTLMSSP_NEGOTIATE_KEY_EXCH)") - } - - if !domainNeeded { - cm.TargetName = "" - } - - am := authenicateMessage{ - UserName: user, - TargetName: cm.TargetName, - NegotiateFlags: cm.NegotiateFlags, - } - - timestamp := cm.TargetInfo[avIDMsvAvTimestamp] - if timestamp == nil { // no time sent, take current time - ft := uint64(time.Now().UnixNano()) / 100 - ft += 116444736000000000 // add time between unix & windows offset - timestamp = make([]byte, 8) - binary.LittleEndian.PutUint64(timestamp, ft) - } - - clientChallenge := make([]byte, 8) - rand.Reader.Read(clientChallenge) +func splitNameForAuth(username string) (user, domain string) { + if strings.Contains(username, "\\") { + ucomponents := strings.SplitN(username, "\\", 2) + domain = ucomponents[0] + user = ucomponents[1] + } else if strings.Contains(username, "@") { + user = username + } else { + user = username + } + return user, domain +} - ntlmV2Hash := getNtlmV2Hash(password, user, cm.TargetName) +// AuthenticateMessageOptions contains optional parameters for the Authenticate message. +type AuthenticateMessageOptions struct { + WorkstationName string - am.NtChallengeResponse = computeNtlmV2Response(ntlmV2Hash, - cm.ServerChallenge[:], clientChallenge, timestamp, cm.TargetInfoRaw) - - if cm.TargetInfoRaw == nil { - am.LmChallengeResponse = computeLmV2Response(ntlmV2Hash, - cm.ServerChallenge[:], clientChallenge) - } - return am.MarshalBinary() + // PasswordHashed indicates whether the provided password is already hashed. + // If true, the password is expected to be in hexadecimal format. + PasswordHashed bool } -func ProcessChallengeWithHash(challengeMessageData []byte, user, hash string) ([]byte, error) { - if user == "" && hash == "" { - return nil, errors.New("Anonymous authentication not supported") +// NewAuthenticateMessage creates a new AUTHENTICATE message in response to the CHALLENGE message that was received from the server. +// The options parameter allows specifying additional settings for the message, it can be nil to use defaults. +func NewAuthenticateMessage(challenge []byte, username, password string, options *AuthenticateMessageOptions) ([]byte, error) { + if username == "" && password == "" { + return nil, errors.New("anonymous authentication not supported") } var cm challengeMessage - if err := cm.UnmarshalBinary(challengeMessageData); err != nil { + if err := cm.UnmarshalBinary(challenge); err != nil { return nil, err } if cm.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATELMKEY) { - return nil, errors.New("Only NTLM v2 is supported, but server requested v1 (NTLMSSP_NEGOTIATE_LM_KEY)") + return nil, errors.New("only NTLM v2 is supported, but server requested v1 (NTLMSSP_NEGOTIATE_LM_KEY)") } if cm.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEKEYEXCH) { - return nil, errors.New("Key exchange requested but not supported (NTLMSSP_NEGOTIATE_KEY_EXCH)") + return nil, errors.New("key exchange requested but not supported (NTLMSSP_NEGOTIATE_KEY_EXCH)") } am := authenicateMessage{ - UserName: user, - TargetName: cm.TargetName, NegotiateFlags: cm.NegotiateFlags, } + am.UserName, am.DomainName = splitNameForAuth(username) + if options != nil { + am.Workstation = options.WorkstationName + } timestamp := cm.TargetInfo[avIDMsvAvTimestamp] if timestamp == nil { // no time sent, take current time @@ -164,17 +142,24 @@ func ProcessChallengeWithHash(challengeMessageData []byte, user, hash string) ([ } clientChallenge := make([]byte, 8) - rand.Reader.Read(clientChallenge) - - hashParts := strings.Split(hash, ":") - if len(hashParts) > 1 { - hash = hashParts[1] - } - hashBytes, err := hex.DecodeString(hash) - if err != nil { + if _, err := rand.Reader.Read(clientChallenge); err != nil { return nil, err } - ntlmV2Hash := hmacMd5(hashBytes, toUnicode(strings.ToUpper(user)+cm.TargetName)) + + var ntlmV2Hash []byte + if options != nil && options.PasswordHashed { + hashParts := strings.Split(password, ":") + if len(hashParts) > 1 { + password = hashParts[1] + } + hashBytes, err := hex.DecodeString(password) + if err != nil { + return nil, err + } + ntlmV2Hash = getNtlmV2Hashed(hashBytes, am.UserName, am.DomainName) + } else { + ntlmV2Hash = getNtlmV2Hash(password, am.UserName, am.DomainName) + } am.NtChallengeResponse = computeNtlmV2Response(ntlmV2Hash, cm.ServerChallenge[:], clientChallenge, timestamp, cm.TargetInfoRaw) @@ -185,3 +170,25 @@ func ProcessChallengeWithHash(challengeMessageData []byte, user, hash string) ([ } return am.MarshalBinary() } + +// ProcessChallenge crafts an AUTHENTICATE message in response to the CHALLENGE message that was received from the server. +// DomainNeeded is ignored, as the function extracts the domain from the username if needed. +// +// Deprecated: Use [NewAuthenticateMessage] instead. +// +//go:fix inline +func ProcessChallenge(challengeMessageData []byte, username, password string, domainNeeded bool) ([]byte, error) { + return NewAuthenticateMessage(challengeMessageData, username, password, nil) +} + +// ProcessChallengeWithHash is like ProcessChallenge but expects the password to be already hashed. +// The hash should be provided in hexadecimal format. +// +// Deprecated: Use [NewAuthenticateMessage] with [AuthenticateMessageOptions.PasswordHashed] instead. +// +//go:fix inline +func ProcessChallengeWithHash(challengeMessageData []byte, username, hash string) ([]byte, error) { + return NewAuthenticateMessage(challengeMessageData, username, hash, &AuthenticateMessageOptions{ + PasswordHashed: true, + }) +} diff --git a/vendor/github.com/Azure/go-ntlmssp/authheader.go b/vendor/github.com/Azure/go-ntlmssp/authheader.go index c9d30d3242..3830b63541 100644 --- a/vendor/github.com/Azure/go-ntlmssp/authheader.go +++ b/vendor/github.com/Azure/go-ntlmssp/authheader.go @@ -1,66 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + package ntlmssp import ( "encoding/base64" + "net/http" "strings" ) -type authheader []string +var schemaPreference = [...]string{"NTLM", "Negotiate", "Basic"} -func (h authheader) IsBasic() bool { - for _, s := range h { - if strings.HasPrefix(string(s), "Basic ") { - return true - } - } - return false +type authheader struct { + schema string + data string } -func (h authheader) Basic() string { - for _, s := range h { - if strings.HasPrefix(string(s), "Basic ") { - return s +// newAuthHeader extracts the authheader from the provided HTTP headers. +// It selects the most preferred authentication scheme. +// If no supported scheme is found, it returns an empty authheader. +func newAuthHeader(req http.Header) authheader { + auth := req.Values("Www-Authenticate") + preferred, idx := -1, -1 + for i, s := range auth { + for j, schema := range schemaPreference { + if s == schema || strings.HasPrefix(s, schema+" ") { + if preferred == -1 || j < preferred { + preferred = j + idx = i + break + } + } } } - return "" -} - -func (h authheader) IsNegotiate() bool { - for _, s := range h { - if strings.HasPrefix(string(s), "Negotiate") { - return true - } + if idx == -1 { + return authheader{} + } + schema, data, _ := strings.Cut(auth[idx], " ") + return authheader{ + schema: schema, + data: data, } - return false } -func (h authheader) IsNTLM() bool { - for _, s := range h { - if strings.HasPrefix(string(s), "NTLM") { - return true - } - } - return false +// isNTLM returns true if the authheader schema is NTLM or Negotiate. +func (h authheader) isNTLM() bool { + return h.schema == "NTLM" || h.schema == "Negotiate" } -func (h authheader) GetData() ([]byte, error) { - for _, s := range h { - if strings.HasPrefix(string(s), "NTLM") || strings.HasPrefix(string(s), "Negotiate") || strings.HasPrefix(string(s), "Basic ") { - p := strings.Split(string(s), " ") - if len(p) < 2 { - return nil, nil - } - return base64.StdEncoding.DecodeString(string(p[1])) - } - } - return nil, nil +// isBasic returns true if the authheader schema is Basic. +func (h authheader) isBasic() bool { + return h.schema == "Basic" } -func (h authheader) GetBasicCreds() (username, password string, err error) { - d, err := h.GetData() - if err != nil { - return "", "", err +// token extracts and decodes the base64 token from the authheader. +// It returns nil if the schema is not NTLM or Negotiate. +func (h authheader) token() ([]byte, error) { + if !h.isNTLM() { + // Schema not supported for token extraction + return nil, nil } - parts := strings.SplitN(string(d), ":", 2) - return parts[0], parts[1], nil + // RFC4559 4.2 - The token is a base64-encoded value + return base64.StdEncoding.DecodeString(h.data) } diff --git a/vendor/github.com/Azure/go-ntlmssp/avids.go b/vendor/github.com/Azure/go-ntlmssp/avids.go index 196b5f1316..f8d55a9cc5 100644 --- a/vendor/github.com/Azure/go-ntlmssp/avids.go +++ b/vendor/github.com/Azure/go-ntlmssp/avids.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + package ntlmssp type avID uint16 diff --git a/vendor/github.com/Azure/go-ntlmssp/challenge_message.go b/vendor/github.com/Azure/go-ntlmssp/challenge_message.go index 053b55e4ad..cf283a33f6 100644 --- a/vendor/github.com/Azure/go-ntlmssp/challenge_message.go +++ b/vendor/github.com/Azure/go-ntlmssp/challenge_message.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + package ntlmssp import ( @@ -32,8 +35,8 @@ func (m *challengeMessage) UnmarshalBinary(data []byte) error { if err != nil { return err } - if !m.challengeMessageFields.IsValid() { - return fmt.Errorf("Message is not a valid challenge message: %+v", m.challengeMessageFields.messageHeader) + if !m.IsValid() { + return fmt.Errorf("message is not a valid challenge message: %+v", m.messageHeader) } if m.challengeMessageFields.TargetName.Len > 0 { @@ -72,7 +75,7 @@ func (m *challengeMessage) UnmarshalBinary(data []byte) error { return err } if n != int(l) { - return fmt.Errorf("Expected to read %d bytes, got only %d", l, n) + return fmt.Errorf("expected to read %d bytes, got only %d", l, n) } m.TargetInfo[id] = value } diff --git a/vendor/github.com/Azure/go-ntlmssp/internal/md4/README.md b/vendor/github.com/Azure/go-ntlmssp/internal/md4/README.md new file mode 100644 index 0000000000..2da0ed58ad --- /dev/null +++ b/vendor/github.com/Azure/go-ntlmssp/internal/md4/README.md @@ -0,0 +1,21 @@ +# MD4 Implementation + +This package contains an identical copy of the MD4 hash implementation from Go's extended cryptography package (`golang.org/x/crypto/md4`). + +## Why Vendored? + +This MD4 implementation is vendored locally to avoid depending on the `golang.org/x/crypto` package, which can introduce version conflicts and dependency management issues in `go.mod`. By maintaining our own copy, we ensure: + +- **Stability**: No external dependency version conflicts +- **Simplicity**: Cleaner `go.mod` file without xcrypto dependency +- **Control**: Full control over the implementation without external changes + +## Source + +The original implementation can be found at: +- Package: `golang.org/x/crypto/md4` +- Repository: https://github.com/golang/crypto + +## Usage + +This package is intended for internal use within the go-ntlmssp library only. The MD4 hash algorithm is required for NTLM authentication but should not be used for general cryptographic purposes as MD4 is considered cryptographically broken. diff --git a/vendor/github.com/Azure/go-ntlmssp/internal/md4/md4.go b/vendor/github.com/Azure/go-ntlmssp/internal/md4/md4.go new file mode 100644 index 0000000000..dfd23b8246 --- /dev/null +++ b/vendor/github.com/Azure/go-ntlmssp/internal/md4/md4.go @@ -0,0 +1,113 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package md4 implements the MD4 hash algorithm as defined in RFC 1320. +package md4 + +import ( + "hash" +) + +// The size of an MD4 checksum in bytes. +const Size = 16 + +// The blocksize of MD4 in bytes. +const BlockSize = 64 + +const ( + _Chunk = 64 + _Init0 = 0x67452301 + _Init1 = 0xEFCDAB89 + _Init2 = 0x98BADCFE + _Init3 = 0x10325476 +) + +// digest represents the partial evaluation of a checksum. +type digest struct { + s [4]uint32 + x [_Chunk]byte + nx int + len uint64 +} + +func (d *digest) Reset() { + d.s[0] = _Init0 + d.s[1] = _Init1 + d.s[2] = _Init2 + d.s[3] = _Init3 + d.nx = 0 + d.len = 0 +} + +// New returns a new hash.Hash computing the MD4 checksum. +func New() hash.Hash { + d := new(digest) + d.Reset() + return d +} + +func (d *digest) Size() int { return Size } + +func (d *digest) BlockSize() int { return BlockSize } + +func (d *digest) Write(p []byte) (nn int, err error) { + nn = len(p) + d.len += uint64(nn) + if d.nx > 0 { + n := len(p) + if n > _Chunk-d.nx { + n = _Chunk - d.nx + } + for i := 0; i < n; i++ { + d.x[d.nx+i] = p[i] + } + d.nx += n + if d.nx == _Chunk { + _Block(d, d.x[0:]) + d.nx = 0 + } + p = p[n:] + } + n := _Block(d, p) + p = p[n:] + if len(p) > 0 { + d.nx = copy(d.x[:], p) + } + return +} + +func (d0 *digest) Sum(in []byte) []byte { + // Make a copy of d0, so that caller can keep writing and summing. + d := new(digest) + *d = *d0 + + // Padding. Add a 1 bit and 0 bits until 56 bytes mod 64. + len := d.len + var tmp [64]byte + tmp[0] = 0x80 + if len%64 < 56 { + d.Write(tmp[0 : 56-len%64]) + } else { + d.Write(tmp[0 : 64+56-len%64]) + } + + // Length in bits. + len <<= 3 + for i := uint(0); i < 8; i++ { + tmp[i] = byte(len >> (8 * i)) + } + d.Write(tmp[0:8]) + + if d.nx != 0 { + panic("d.nx != 0") + } + + for _, s := range d.s { + in = append(in, byte(s>>0)) + in = append(in, byte(s>>8)) + in = append(in, byte(s>>16)) + in = append(in, byte(s>>24)) + } + return in +} diff --git a/vendor/github.com/Azure/go-ntlmssp/internal/md4/md4block.go b/vendor/github.com/Azure/go-ntlmssp/internal/md4/md4block.go new file mode 100644 index 0000000000..5ea1ba966e --- /dev/null +++ b/vendor/github.com/Azure/go-ntlmssp/internal/md4/md4block.go @@ -0,0 +1,91 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// MD4 block step. +// In its own file so that a faster assembly or C version +// can be substituted easily. + +package md4 + +import "math/bits" + +var shift1 = []int{3, 7, 11, 19} +var shift2 = []int{3, 5, 9, 13} +var shift3 = []int{3, 9, 11, 15} + +var xIndex2 = []uint{0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15} +var xIndex3 = []uint{0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15} + +func _Block(dig *digest, p []byte) int { + a := dig.s[0] + b := dig.s[1] + c := dig.s[2] + d := dig.s[3] + n := 0 + var X [16]uint32 + for len(p) >= _Chunk { + aa, bb, cc, dd := a, b, c, d + + j := 0 + for i := 0; i < 16; i++ { + X[i] = uint32(p[j]) | uint32(p[j+1])<<8 | uint32(p[j+2])<<16 | uint32(p[j+3])<<24 + j += 4 + } + + // If this needs to be made faster in the future, + // the usual trick is to unroll each of these + // loops by a factor of 4; that lets you replace + // the shift[] lookups with constants and, + // with suitable variable renaming in each + // unrolled body, delete the a, b, c, d = d, a, b, c + // (or you can let the optimizer do the renaming). + // + // The index variables are uint so that % by a power + // of two can be optimized easily by a compiler. + + // Round 1. + for i := uint(0); i < 16; i++ { + x := i + s := shift1[i%4] + f := ((c ^ d) & b) ^ d + a += f + X[x] + a = bits.RotateLeft32(a, s) + a, b, c, d = d, a, b, c + } + + // Round 2. + for i := uint(0); i < 16; i++ { + x := xIndex2[i] + s := shift2[i%4] + g := (b & c) | (b & d) | (c & d) + a += g + X[x] + 0x5a827999 + a = bits.RotateLeft32(a, s) + a, b, c, d = d, a, b, c + } + + // Round 3. + for i := uint(0); i < 16; i++ { + x := xIndex3[i] + s := shift3[i%4] + h := b ^ c ^ d + a += h + X[x] + 0x6ed9eba1 + a = bits.RotateLeft32(a, s) + a, b, c, d = d, a, b, c + } + + a += aa + b += bb + c += cc + d += dd + + p = p[_Chunk:] + n += _Chunk + } + + dig.s[0] = a + dig.s[1] = b + dig.s[2] = c + dig.s[3] = d + return n +} diff --git a/vendor/github.com/Azure/go-ntlmssp/messageheader.go b/vendor/github.com/Azure/go-ntlmssp/messageheader.go index 247e284652..21e82c8301 100644 --- a/vendor/github.com/Azure/go-ntlmssp/messageheader.go +++ b/vendor/github.com/Azure/go-ntlmssp/messageheader.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + package ntlmssp import ( diff --git a/vendor/github.com/Azure/go-ntlmssp/negotiate_flags.go b/vendor/github.com/Azure/go-ntlmssp/negotiate_flags.go index 5905c023d6..d78ae71404 100644 --- a/vendor/github.com/Azure/go-ntlmssp/negotiate_flags.go +++ b/vendor/github.com/Azure/go-ntlmssp/negotiate_flags.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + package ntlmssp type negotiateFlags uint32 @@ -48,5 +51,5 @@ func (field negotiateFlags) Has(flags negotiateFlags) bool { } func (field *negotiateFlags) Unset(flags negotiateFlags) { - *field = *field ^ (*field & flags) + *field ^= *field & flags } diff --git a/vendor/github.com/Azure/go-ntlmssp/negotiate_message.go b/vendor/github.com/Azure/go-ntlmssp/negotiate_message.go index e466a9861d..a0746cd6ed 100644 --- a/vendor/github.com/Azure/go-ntlmssp/negotiate_message.go +++ b/vendor/github.com/Azure/go-ntlmssp/negotiate_message.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + package ntlmssp import ( @@ -23,27 +26,33 @@ var defaultFlags = negotiateFlagNTLMSSPNEGOTIATETARGETINFO | negotiateFlagNTLMSSPNEGOTIATE56 | negotiateFlagNTLMSSPNEGOTIATE128 | negotiateFlagNTLMSSPNEGOTIATEUNICODE | - negotiateFlagNTLMSSPNEGOTIATEEXTENDEDSESSIONSECURITY + negotiateFlagNTLMSSPNEGOTIATEEXTENDEDSESSIONSECURITY | + negotiateFlagNTLMSSPNEGOTIATENTLM | + negotiateFlagNTLMSSPNEGOTIATEALWAYSSIGN -//NewNegotiateMessage creates a new NEGOTIATE message with the -//flags that this package supports. -func NewNegotiateMessage(domainName, workstationName string) ([]byte, error) { +// NewNegotiateMessage creates a new NEGOTIATE message with the flags that this package supports. +// Note that domain and workstation refer to the client machine, not the user that is authenticating. +// It is recommended to leave them empty unless you know which are their correct values. +// +// The server may ignore these values, or may use them to infer that the client if running on the +// same machine. +func NewNegotiateMessage(domain, workstation string) ([]byte, error) { payloadOffset := expMsgBodyLen flags := defaultFlags - if domainName != "" { + if domain != "" { flags |= negotiateFlagNTLMSSPNEGOTIATEOEMDOMAINSUPPLIED } - if workstationName != "" { + if workstation != "" { flags |= negotiateFlagNTLMSSPNEGOTIATEOEMWORKSTATIONSUPPLIED } msg := negotiateMessageFields{ messageHeader: newMessageHeader(1), NegotiateFlags: flags, - Domain: newVarField(&payloadOffset, len(domainName)), - Workstation: newVarField(&payloadOffset, len(workstationName)), + Domain: newVarField(&payloadOffset, len(domain)), + Workstation: newVarField(&payloadOffset, len(workstation)), Version: DefaultVersion(), } @@ -55,7 +64,7 @@ func NewNegotiateMessage(domainName, workstationName string) ([]byte, error) { return nil, errors.New("incorrect body length") } - payload := strings.ToUpper(domainName + workstationName) + payload := strings.ToUpper(domain + workstation) if _, err := b.WriteString(payload); err != nil { return nil, err } diff --git a/vendor/github.com/Azure/go-ntlmssp/negotiator.go b/vendor/github.com/Azure/go-ntlmssp/negotiator.go index cce4955df1..80514dd981 100644 --- a/vendor/github.com/Azure/go-ntlmssp/negotiator.go +++ b/vendor/github.com/Azure/go-ntlmssp/negotiator.go @@ -1,151 +1,331 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + package ntlmssp import ( "bytes" "encoding/base64" "io" - "io/ioutil" "net/http" "strings" ) -// GetDomain : parse domain name from based on slashes in the input -// Need to check for upn as well -func GetDomain(user string) (string, string, bool) { - domain := "" - domainNeeded := false +// negotiatorBody wraps an io.ReadSeeker to allow waiting for its closure +// before rewinding and reusing it. +type negotiatorBody struct { + body io.ReadSeeker + closed chan struct{} + startPos int64 +} + +// newNegotiatorBody creates a negotiatorBody from the provided io.Reader. +// If the body is nil, it returns nil. +// If the body is already an io.ReadSeeker, it uses it directly. +// Otherwise, it reads the entire body into memory to allow rewinding. +func newNegotiatorBody(body io.Reader) (*negotiatorBody, error) { + if body == nil { + return nil, nil + } + // Check if body is already seekable to avoid buffering large bodies + if seeker, ok := body.(io.ReadSeeker); ok { + // Remember the current position + startPos, err := seeker.Seek(0, io.SeekCurrent) + if err == nil { + // Seeking succeeded, use the seekable body directly + return &negotiatorBody{ + body: seeker, + closed: make(chan struct{}, 1), + startPos: startPos, + }, nil + } + // Seeking failed (e.g., pipes), fallback to buffering + } + // For non-seekable bodies, buffer in memory as required + data, err := io.ReadAll(body) + if err != nil { + return nil, err + } + return &negotiatorBody{ + body: bytes.NewReader(data), + closed: make(chan struct{}, 1), + }, nil +} + +func (b *negotiatorBody) Read(p []byte) (n int, err error) { + if b == nil { + return 0, io.EOF + } + return b.body.Read(p) +} + +// Close signals that the body is no longer needed for the current request. +// It allows the negotiator to rewind the body for potential reuse. +// The underlying body is not closed here; use close() for that. +func (b *negotiatorBody) Close() error { + if b == nil { + return nil + } + select { + case b.closed <- struct{}{}: + default: + // Already signaled + } + return nil +} + +// close closes the underlying body if it implements io.Closer. +func (b *negotiatorBody) close() { + if b == nil { + return + } + if closer, ok := b.body.(io.Closer); ok { + _ = closer.Close() + } +} + +// rewind rewinds the body to the start position for reuse. +func (b *negotiatorBody) rewind() error { + if b == nil { + return nil + } + // Wait for the body to be closed before rewinding + <-b.closed + _, err := b.body.Seek(b.startPos, io.SeekStart) + return err +} - if strings.Contains(user, "\\") { - ucomponents := strings.SplitN(user, "\\", 2) +// GetDomain extracts the user domain from the username if present. +// +// Deprecated: Pass the username directly to [ProcessChallenge], it will handle domain extraction. +// Don't pass the resulting domain to [NewNegotiateMessage], that function expects the client +// machine domain, not the user domain. +func GetDomain(username string) (user string, domain string, domainNeeded bool) { + if strings.Contains(username, "\\") { + ucomponents := strings.SplitN(username, "\\", 2) domain = ucomponents[0] user = ucomponents[1] domainNeeded = true - } else if strings.Contains(user, "@") { + } else if strings.Contains(username, "@") { + user = username domainNeeded = false } else { + user = username domainNeeded = true } return user, domain, domainNeeded } -//Negotiator is a http.Roundtripper decorator that automatically -//converts basic authentication to NTLM/Negotiate authentication when appropriate. -type Negotiator struct{ http.RoundTripper } +// Negotiator is a [net/http.RoundTripper] decorator that automatically +// converts basic authentication to NTLM/Negotiate authentication when appropriate. +// +// The credentials must be set using [net/http.Request.SetBasicAuth] on a per-request basis. +// +// By default, no credentials will be sent to the server unless it requests +// Basic authentication and [Negotiator.AllowBasicAuth] is set to true. +type Negotiator struct { + // RoundTripper is the underlying round tripper to use. + // If nil, http.DefaultTransport is used. + http.RoundTripper + + // AllowBasicAuth controls whether to send Basic authentication credentials + // if the server requests it. + // + // If false (default), Basic authentication requests are ignored + // and only NTLM/Negotiate authentication is performed. + // If true, Basic authentication requests are honored. + // + // Only set this to true if you trust the server you are connecting to. + // Basic authentication sends the credentials in clear text and may be + // vulnerable to man-in-the-middle attacks and compromised servers. + AllowBasicAuth bool -//RoundTrip sends the request to the server, handling any authentication -//re-sends as needed. -func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) { + // WorkstationDomain is the domain of the client machine. + // It is normally not needed to set this field. + // It is passed to the negotiate message. + WorkstationDomain string + + // WorkstationName is the workstation name of the client machine. + // It is passed to the negotiate and authenticate messages. + // Useful for auditing purposes on the server side. + WorkstationName string +} + +// RoundTrip sends the request to the server, handling any authentication +// re-sends as needed. +func (l Negotiator) RoundTrip(req *http.Request) (*http.Response, error) { // Use default round tripper if not provided rt := l.RoundTripper if rt == nil { rt = http.DefaultTransport } + // If it is not basic auth, just round trip the request as usual - reqauth := authheader(req.Header.Values("Authorization")) - if !reqauth.IsBasic() { + username, password, ok := req.BasicAuth() + if !ok { return rt.RoundTrip(req) } - reqauthBasic := reqauth.Basic() - // Save request body - body := bytes.Buffer{} - if req.Body != nil { - _, err = body.ReadFrom(req.Body) - if err != nil { - return nil, err - } + id := identity{ + username: username, + password: password, + } + + req = req.Clone(req.Context()) // Clone the request to avoid modifying the original - req.Body.Close() - req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes())) + // We need to buffer or seek the request body to handle authentication challenges + // that require resending the body multiple times during the NTLM handshake. + body, err := newNegotiatorBody(req.Body) + if err != nil { + if req.Body != nil { + _ = req.Body.Close() + } + return nil, err } - // first try anonymous, in case the server still finds us - // authenticated from previous traffic + defer body.close() + + // First try anonymous, in case the server still finds us authenticated from previous traffic + req.Body = body req.Header.Del("Authorization") - res, err = rt.RoundTrip(req) + resp, err := rt.RoundTrip(req) if err != nil { return nil, err } - if res.StatusCode != http.StatusUnauthorized { - return res, err + if resp.StatusCode != http.StatusUnauthorized { + // No authentication required, return the response as is + return resp, nil } - resauth := authheader(res.Header.Values("Www-Authenticate")) - if !resauth.IsNegotiate() && !resauth.IsNTLM() { - // Unauthorized, Negotiate not requested, let's try with basic auth - req.Header.Set("Authorization", string(reqauthBasic)) - io.Copy(ioutil.Discard, res.Body) - res.Body.Close() - req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes())) - res, err = rt.RoundTrip(req) + // Note that from here on, the response returned in case of error or unsuccessful + // negotiation is the one we just got from the server. This is to allow the caller + // to do its own handling in case we can't do it in this roundtrip. + originalResp := resp + + resauth := newAuthHeader(resp.Header) + if l.AllowBasicAuth && resauth.isBasic() { + // Basic auth requested instead of NTLM/Negotiate. + // + // Rewind the body, we will resend it. + if body.rewind() != nil { + return originalResp, nil + } + req.SetBasicAuth(id.username, id.password) + resp, err := rt.RoundTrip(req) if err != nil { - return nil, err + return originalResp, nil + } + if resp.StatusCode != http.StatusUnauthorized { + // Basic auth succeeded, return the new response + drainResponse(originalResp) + return resp, nil } - if res.StatusCode != http.StatusUnauthorized { - return res, err + resauth = newAuthHeader(resp.Header) + if !resauth.isNTLM() { + // No NTLM/Negotiate requested, return the response as is + return resp, nil } - resauth = authheader(res.Header.Values("Www-Authenticate")) + // Server upgraded from Basic to NTLM/Negotiate (rare but possible) + drainResponse(resp) + // After Basic-to-NTLM upgrade, update originalResp to the NTLM-triggering response + originalResp = resp + } else if !resauth.isNTLM() { + // No NTLM/Negotiate requested, return the response as is + return originalResp, nil } - if resauth.IsNegotiate() || resauth.IsNTLM() { - // 401 with request:Basic and response:Negotiate - io.Copy(ioutil.Discard, res.Body) - res.Body.Close() + // Server requested Negotiate/NTLM, start handshake - // recycle credentials - u, p, err := reqauth.GetBasicCreds() - if err != nil { - return nil, err - } + // First step: send negotiate message + resp = clientHandshake(rt, req, resauth.schema, l.WorkstationDomain, l.WorkstationName) + if resp == nil { + return originalResp, nil + } + if resp.StatusCode != http.StatusUnauthorized { + // We are expecting a 401 with challenge, but the server responded differently, + // maybe it even accepted our negotiate message without further challenge, which is + // valid per the spec (RFC 4559 Section 5). + // Return the response as is, negotiation is over. + drainResponse(originalResp) + return resp, nil + } + resauth = newAuthHeader(resp.Header) + drainResponse(resp) - // get domain from username - domain := "" - u, domain, domainNeeded := GetDomain(u) + // Second step: process challenge and resend the original body with the authenticate message + resp = completeHandshake(rt, resauth, req, id, l.WorkstationName) + if resp == nil { + return originalResp, nil + } + // We could return the original response in case of 401 again, but at this point + // it's better to return the latest response from the server, as it might be the case + // that we are really not authorized. + drainResponse(originalResp) // Done with the original response + return resp, nil +} - // send negotiate - negotiateMessage, err := NewNegotiateMessage(domain, "") - if err != nil { - return nil, err - } - if resauth.IsNTLM() { - req.Header.Set("Authorization", "NTLM "+base64.StdEncoding.EncodeToString(negotiateMessage)) - } else { - req.Header.Set("Authorization", "Negotiate "+base64.StdEncoding.EncodeToString(negotiateMessage)) - } +type identity struct { + username string + password string +} - req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes())) +func drainResponse(res *http.Response) { + // Drain body and close it to allow reusing the connection + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() +} - res, err = rt.RoundTrip(req) - if err != nil { - return nil, err - } +func rewindBody(req *http.Request) error { + if req.Body == nil { + return nil + } + if nb, ok := req.Body.(*negotiatorBody); ok { + return nb.rewind() + } + return nil +} - // receive challenge? - resauth = authheader(res.Header.Values("Www-Authenticate")) - challengeMessage, err := resauth.GetData() - if err != nil { - return nil, err - } - if !(resauth.IsNegotiate() || resauth.IsNTLM()) || len(challengeMessage) == 0 { - // Negotiation failed, let client deal with response - return res, nil - } - io.Copy(ioutil.Discard, res.Body) - res.Body.Close() +func clientHandshake(rt http.RoundTripper, req *http.Request, schema string, domain, workstation string) *http.Response { + if rewindBody(req) != nil { + return nil + } + auth, err := NewNegotiateMessage(domain, workstation) + if err != nil { + return nil + } + req.Header.Set("Authorization", schema+" "+base64.StdEncoding.EncodeToString(auth)) + res, err := rt.RoundTrip(req) + if err != nil { + return nil + } + return res +} - // send authenticate - authenticateMessage, err := ProcessChallenge(challengeMessage, u, p, domainNeeded) - if err != nil { - return nil, err - } - if resauth.IsNTLM() { - req.Header.Set("Authorization", "NTLM "+base64.StdEncoding.EncodeToString(authenticateMessage)) - } else { - req.Header.Set("Authorization", "Negotiate "+base64.StdEncoding.EncodeToString(authenticateMessage)) +func completeHandshake(rt http.RoundTripper, resauth authheader, req *http.Request, id identity, workstation string) *http.Response { + if rewindBody(req) != nil { + return nil + } + challenge, err := resauth.token() + if err != nil { + return nil + } + if !resauth.isNTLM() || len(challenge) == 0 { + // The only expected schema here is NTLM/Negotiate with a challenge token, + // otherwise the negotiation is over. + return nil + } + var opts *AuthenticateMessageOptions + if workstation != "" { + opts = &AuthenticateMessageOptions{ + WorkstationName: workstation, } - - req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes())) - - return rt.RoundTrip(req) } - - return res, err + auth, err := NewAuthenticateMessage(challenge, id.username, id.password, opts) + if err != nil { + return nil + } + req.Header.Set("Authorization", resauth.schema+" "+base64.StdEncoding.EncodeToString(auth)) + resp, err := rt.RoundTrip(req) + if err != nil { + return nil + } + return resp } diff --git a/vendor/github.com/Azure/go-ntlmssp/nlmp.go b/vendor/github.com/Azure/go-ntlmssp/nlmp.go index 1e65abe8b5..ceffabc07d 100644 --- a/vendor/github.com/Azure/go-ntlmssp/nlmp.go +++ b/vendor/github.com/Azure/go-ntlmssp/nlmp.go @@ -1,5 +1,6 @@ -// Package ntlmssp provides NTLM/Negotiate authentication over HTTP -// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + // Protocol details from https://msdn.microsoft.com/en-us/library/cc236621.aspx, // implementation hints from http://davenport.sourceforge.net/ntlm.html . // This package only implements authentication, no key exchange or encryption. It @@ -10,12 +11,17 @@ package ntlmssp import ( "crypto/hmac" "crypto/md5" - "golang.org/x/crypto/md4" "strings" + + "github.com/Azure/go-ntlmssp/internal/md4" ) -func getNtlmV2Hash(password, username, target string) []byte { - return hmacMd5(getNtlmHash(password), toUnicode(strings.ToUpper(username)+target)) +func getNtlmV2Hash(password, username, domain string) []byte { + return getNtlmV2Hashed(getNtlmHash(password), username, domain) +} + +func getNtlmV2Hashed(ntlmHash []byte, username, domain string) []byte { + return hmacMd5(ntlmHash, toUnicode(strings.ToUpper(username)+domain)) } func getNtlmHash(password string) []byte { @@ -25,8 +31,8 @@ func getNtlmHash(password string) []byte { } func computeNtlmV2Response(ntlmV2Hash, serverChallenge, clientChallenge, - timestamp, targetInfo []byte) []byte { - + timestamp, targetInfo []byte, +) []byte { temp := []byte{1, 1, 0, 0, 0, 0, 0, 0} temp = append(temp, timestamp...) temp = append(temp, clientChallenge...) diff --git a/vendor/github.com/Azure/go-ntlmssp/unicode.go b/vendor/github.com/Azure/go-ntlmssp/unicode.go index 7b4f47163d..ebb9f379c8 100644 --- a/vendor/github.com/Azure/go-ntlmssp/unicode.go +++ b/vendor/github.com/Azure/go-ntlmssp/unicode.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + package ntlmssp import ( @@ -11,7 +14,7 @@ import ( func fromUnicode(d []byte) (string, error) { if len(d)%2 > 0 { - return "", errors.New("Unicode (UTF 16 LE) specified, but uneven data length") + return "", errors.New("unicode (UTF 16 LE) specified, but uneven data length") } s := make([]uint16, len(d)/2) err := binary.Read(bytes.NewReader(d), binary.LittleEndian, &s) @@ -24,6 +27,6 @@ func fromUnicode(d []byte) (string, error) { func toUnicode(s string) []byte { uints := utf16.Encode([]rune(s)) b := bytes.Buffer{} - binary.Write(&b, binary.LittleEndian, &uints) + _ = binary.Write(&b, binary.LittleEndian, &uints) return b.Bytes() } diff --git a/vendor/github.com/Azure/go-ntlmssp/varfield.go b/vendor/github.com/Azure/go-ntlmssp/varfield.go index 15f9aa113d..7e2433216d 100644 --- a/vendor/github.com/Azure/go-ntlmssp/varfield.go +++ b/vendor/github.com/Azure/go-ntlmssp/varfield.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + package ntlmssp import ( @@ -12,7 +15,7 @@ type varField struct { func (f varField) ReadFrom(buffer []byte) ([]byte, error) { if len(buffer) < int(f.BufferOffset+uint32(f.Len)) { - return nil, errors.New("Error reading data, varField extends beyond buffer") + return nil, errors.New("error reading data, varField extends beyond buffer") } return buffer[f.BufferOffset : f.BufferOffset+uint32(f.Len)], nil } diff --git a/vendor/github.com/Azure/go-ntlmssp/version.go b/vendor/github.com/Azure/go-ntlmssp/version.go index 6d84892124..50cebb99f2 100644 --- a/vendor/github.com/Azure/go-ntlmssp/version.go +++ b/vendor/github.com/Azure/go-ntlmssp/version.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + package ntlmssp // Version is a struct representing https://msdn.microsoft.com/en-us/library/cc236654.aspx diff --git a/vendor/github.com/go-ldap/ldap/v3/control.go b/vendor/github.com/go-ldap/ldap/v3/control.go index a879e9d48e..1f93b38025 100644 --- a/vendor/github.com/go-ldap/ldap/v3/control.go +++ b/vendor/github.com/go-ldap/ldap/v3/control.go @@ -1,6 +1,7 @@ package ldap import ( + "encoding/binary" "fmt" "strconv" @@ -880,7 +881,16 @@ func (c *ControlDirSync) Encode() *ber.Packet { val := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (DirSync)") seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "DirSync Control Value") - seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.Flags), "Flags")) + + // Note: Active Directory expects a 4-byte unsigned integer for flags, but ASN.1 uses signed integers by default. + // As a result, the BER encoder may encode flags as a 5-byte signed integer; we force 4-byte encoding here. + flagsPacket := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, nil, "Flags") + flagsPacket.Value = int64(c.Flags) + flagsBytes := make([]byte, 4) + binary.BigEndian.PutUint32(flagsBytes, uint32(c.Flags)) + flagsPacket.Data.Write(flagsBytes) + seq.AppendChild(flagsPacket) + seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.MaxAttrCount), "MaxAttrCount")) seq.AppendChild(cookie) val.AppendChild(seq) diff --git a/vendor/github.com/go-ldap/ldap/v3/error.go b/vendor/github.com/go-ldap/ldap/v3/error.go index 0014ffe2f2..1cf09c4f3f 100644 --- a/vendor/github.com/go-ldap/ldap/v3/error.go +++ b/vendor/github.com/go-ldap/ldap/v3/error.go @@ -210,6 +210,10 @@ func GetLDAPError(packet *ber.Packet) error { } if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 { if ber.Type(response.Children[0].Tag) == ber.Type(ber.TagInteger) || ber.Type(response.Children[0].Tag) == ber.Type(ber.TagEnumerated) { + if response.Children[0].Value == nil { + return &Error{ResultCode: ErrorNetwork, Err: fmt.Errorf("Invalid result code in packet"), Packet: packet} + } + resultCode := uint16(response.Children[0].Value.(int64)) if resultCode == 0 { // No error return nil @@ -217,6 +221,9 @@ func GetLDAPError(packet *ber.Packet) error { if ber.Type(response.Children[1].Tag) == ber.Type(ber.TagOctetString) && ber.Type(response.Children[2].Tag) == ber.Type(ber.TagOctetString) { + if response.Children[1].Value == nil { + return &Error{ResultCode: ErrorNetwork, Err: fmt.Errorf("Invalid matchedDN in packet"), Packet: packet} + } return &Error{ ResultCode: resultCode, MatchedDN: response.Children[1].Value.(string), diff --git a/vendor/github.com/go-ldap/ldap/v3/extended.go b/vendor/github.com/go-ldap/ldap/v3/extended.go index 921238febd..84cffbeea0 100644 --- a/vendor/github.com/go-ldap/ldap/v3/extended.go +++ b/vendor/github.com/go-ldap/ldap/v3/extended.go @@ -76,18 +76,27 @@ func (l *Conn) Extended(er *ExtendedRequest) (*ExtendedResponse, error) { return nil, err } - if len(packet.Children[1].Children) < 4 { + extResp := packet.Children[1] + if len(extResp.Children) < 3 { return nil, fmt.Errorf( - "ldap: malformed extended response: expected 4 children, got %d", + "ldap: malformed extended response: expected at least 3 children, got %d", len(packet.Children), ) } response := &ExtendedResponse{ - Name: packet.Children[1].Children[3].Data.String(), Controls: make([]Control, 0), } + for _, child := range extResp.Children { + switch child.Tag { + case ber.TagEnumerated: + response.Name = child.Data.String() + case ber.TagEmbeddedPDV: + response.Value = child + } + } + if len(packet.Children) == 3 { for _, child := range packet.Children[2].Children { decodedChild, decodeErr := DecodeControl(child) @@ -98,9 +107,5 @@ func (l *Conn) Extended(er *ExtendedRequest) (*ExtendedResponse, error) { } } - if len(packet.Children[1].Children) == 5 { - response.Value = packet.Children[1].Children[4] - } - return response, nil } diff --git a/vendor/github.com/go-ldap/ldap/v3/postaladdress.go b/vendor/github.com/go-ldap/ldap/v3/postaladdress.go new file mode 100644 index 0000000000..978478b507 --- /dev/null +++ b/vendor/github.com/go-ldap/ldap/v3/postaladdress.go @@ -0,0 +1,130 @@ +package ldap + +import ( + "errors" + "fmt" + "strings" +) + +var ErrEmptyPostalAddress = errors.New("ldap: postal address cannot be empty") + +// PostalAddress represents an RFC 4517 Postal Address +// A postal address is a sequence of strings of one or more arbitrary UCS +// characters, which form the lines of the address. +type PostalAddress struct { + lines []string +} + +// NewPostalAddress creates a new PostalAddress by copying non-empty lines from the provided slice of strings. +func NewPostalAddress(lines []string) (*PostalAddress, error) { + copiedLines := make([]string, 0, len(lines)) + for _, line := range lines { + if line == "" { + continue + } + copiedLines = append(copiedLines, line) + } + + if len(copiedLines) == 0 { + return nil, ErrEmptyPostalAddress + } + + return &PostalAddress{lines: copiedLines}, nil +} + +// Lines returns a copy of the address lines as a slice of strings. +func (p *PostalAddress) Lines() []string { + copiedLines := make([]string, len(p.lines)) + copy(copiedLines, p.lines) + return copiedLines +} + +// String returns the postal address as a single string, with lines joined by newline characters. +func (p *PostalAddress) String() string { + return strings.Join(p.lines, "\n") +} + +// Escape encodes special characters in the PostalAddress lines as per RFC 4517 and appends a `$` at the end of each line. +func (p *PostalAddress) Escape() string { + builder := &strings.Builder{} + + for _, line := range p.lines { + for _, char := range line { + switch char { + case '\\': + builder.WriteString("\\5C") + case '$': + builder.WriteString("\\24") + default: + builder.WriteRune(char) + } + } + + builder.WriteRune('$') + } + + return builder.String() +} + +// ParsePostalAddress parses an RFC 4517 escaped postal address string into a PostalAddress object or returns an error. +func ParsePostalAddress(escaped string) (*PostalAddress, error) { + lines := strings.Split(escaped, "$") + parsedLines := make([]string, 0, len(lines)) + const totalEscapeLen = 3 + + for _, line := range lines { + if line == "" { + // Skip empty lines + continue + } + + builder := &strings.Builder{} + for i := 0; i < len(line); i++ { + char := line[i] + if char == '\\' && i+totalEscapeLen <= len(line) { + escapeSeq := line[i+1 : i+totalEscapeLen] + switch escapeSeq { + case "5C", "5c": + builder.WriteRune('\\') + i += 2 + case "24": + builder.WriteRune('$') + i += 2 + default: + return nil, fmt.Errorf("invalid escape sequence: \\%s at position %d", escapeSeq, i) + } + } else if char == '\\' { + return nil, fmt.Errorf("incomplete escape sequence at position %d", i) + } else { + builder.WriteByte(char) + } + } + parsedLines = append(parsedLines, builder.String()) + } + + if len(parsedLines) == 0 { + return nil, ErrEmptyPostalAddress + } + + return &PostalAddress{lines: parsedLines}, nil +} + +// Equal compares the current PostalAddress with another PostalAddress and returns true if they are identical. +func (p *PostalAddress) Equal(other *PostalAddress) bool { + if p == other { + return true + } + if p == nil || other == nil { + return false + } + + if len(p.lines) != len(other.lines) { + return false + } + for i := range p.lines { + if p.lines[i] != other.lines[i] { + return false + } + } + return true +} diff --git a/vendor/github.com/go-ldap/ldap/v3/search.go b/vendor/github.com/go-ldap/ldap/v3/search.go index 151817c211..e1c684e12b 100644 --- a/vendor/github.com/go-ldap/ldap/v3/search.go +++ b/vendor/github.com/go-ldap/ldap/v3/search.go @@ -623,7 +623,7 @@ func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) { // SearchAsync performs a search request and returns all search results asynchronously. // This means you get all results until an error happens (or the search successfully finished), -// e.g. for size / time limited requests all are recieved until the limit is reached. +// e.g. for size / time limited requests all are received until the limit is reached. // To stop the search, call cancel function of the context. func (l *Conn) SearchAsync( ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response { diff --git a/vendor/github.com/go-ldap/ldap/v3/whoami.go b/vendor/github.com/go-ldap/ldap/v3/whoami.go index 10c523d082..0d743d2243 100644 --- a/vendor/github.com/go-ldap/ldap/v3/whoami.go +++ b/vendor/github.com/go-ldap/ldap/v3/whoami.go @@ -4,88 +4,20 @@ package ldap // // https://tools.ietf.org/html/rfc4532 -import ( - "errors" - "fmt" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -type whoAmIRequest bool - // WhoAmIResult is returned by the WhoAmI() call type WhoAmIResult struct { AuthzID string } -func (r whoAmIRequest) encode() (*ber.Packet, error) { - request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Who Am I? Extended Operation") - request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, ControlTypeWhoAmI, "Extended Request Name: Who Am I? OID")) - return request, nil -} - // WhoAmI returns the authzId the server thinks we are, you may pass controls // like a Proxied Authorization control func (l *Conn) WhoAmI(controls []Control) (*WhoAmIResult, error) { - 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")) - req := whoAmIRequest(true) - encodedWhoAmIRequest, err := req.encode() - if err != nil { - return nil, err - } - packet.AppendChild(encodedWhoAmIRequest) - - if len(controls) != 0 { - packet.AppendChild(encodeControls(controls)) - } - - l.Debug.PrintPacket(packet) - - msgCtx, err := l.sendMessage(packet) - if err != nil { - return nil, err - } - defer l.finishMessage(msgCtx) - - result := &WhoAmIResult{} - - l.Debug.Printf("%d: waiting for response", msgCtx.id) - 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) + extendedRequest := NewExtendedRequest(ControlTypeWhoAmI, nil) + extendedRequest.Controls = controls + resp, err := l.Extended(extendedRequest) if err != nil { return nil, err } - if packet == nil { - return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) - } - - if l.Debug { - if err := addLDAPDescriptions(packet); err != nil { - return nil, err - } - ber.PrintPacket(packet) - } - - if packet.Children[1].Tag == ApplicationExtendedResponse { - if err := GetLDAPError(packet); err != nil { - return nil, err - } - } else { - return nil, NewError(ErrorUnexpectedResponse, fmt.Errorf("Unexpected Response: %d", packet.Children[1].Tag)) - } - - extendedResponse := packet.Children[1] - for _, child := range extendedResponse.Children { - if child.Tag == 11 { - result.AuthzID = ber.DecodeString(child.Data.Bytes()) - } - } - - return result, nil + return &WhoAmIResult{AuthzID: resp.Value.Data.String()}, nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index 0e52d53515..8335fe9e91 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -12,9 +12,10 @@ filippo.io/edwards25519/field ## explicit; go 1.16 github.com/Azure/go-ansiterm github.com/Azure/go-ansiterm/winterm -# github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 -## explicit +# github.com/Azure/go-ntlmssp v0.1.0 +## explicit; go 1.24 github.com/Azure/go-ntlmssp +github.com/Azure/go-ntlmssp/internal/md4 # github.com/BurntSushi/toml v1.6.0 ## explicit; go 1.18 github.com/BurntSushi/toml @@ -570,8 +571,8 @@ github.com/go-jose/go-jose/v4/json ## explicit; go 1.17 github.com/go-kit/log github.com/go-kit/log/level -# github.com/go-ldap/ldap/v3 v3.4.12 -## explicit; go 1.23.0 +# github.com/go-ldap/ldap/v3 v3.4.13 +## explicit; go 1.24.0 github.com/go-ldap/ldap/v3 # github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3 ## explicit; go 1.14