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] <support@github.com>
This commit is contained in:
dependabot[bot]
2026-04-07 10:52:13 +00:00
committed by Ralf Haferkamp
parent 87a9660157
commit 9123e88f10
29 changed files with 1043 additions and 370 deletions

4
go.mod
View File

@@ -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

8
go.sum
View File

@@ -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=

2
vendor/github.com/Azure/go-ntlmssp/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
.vscode
*.exe

39
vendor/github.com/Azure/go-ntlmssp/.golangci.yml generated vendored Normal file
View File

@@ -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

View File

@@ -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 ./...

107
vendor/github.com/Azure/go-ntlmssp/E2E_README.md generated vendored Normal file
View File

@@ -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.

View File

@@ -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.
# 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{},
},
}

View File

@@ -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,34 +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")
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
}
// AuthenticateMessageOptions contains optional parameters for the Authenticate message.
type AuthenticateMessageOptions struct {
WorkstationName string
// PasswordHashed indicates whether the provided password is already hashed.
// If true, the password is expected to be in hexadecimal format.
PasswordHashed bool
}
// 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)")
}
if !domainNeeded {
cm.TargetName = ""
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
@@ -118,9 +142,24 @@ func ProcessChallenge(challengeMessageData []byte, user, password string, domain
}
clientChallenge := make([]byte, 8)
rand.Reader.Read(clientChallenge)
if _, err := rand.Reader.Read(clientChallenge); err != nil {
return nil, err
}
ntlmV2Hash := getNtlmV2Hash(password, 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)
@@ -132,56 +171,24 @@ func ProcessChallenge(challengeMessageData []byte, user, password string, domain
return am.MarshalBinary()
}
func ProcessChallengeWithHash(challengeMessageData []byte, user, hash string) ([]byte, error) {
if user == "" && hash == "" {
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)")
}
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)
hashParts := strings.Split(hash, ":")
if len(hashParts) > 1 {
hash = hashParts[1]
}
hashBytes, err := hex.DecodeString(hash)
if err != nil {
return nil, err
}
ntlmV2Hash := hmacMd5(hashBytes, toUnicode(strings.ToUpper(user)+cm.TargetName))
am.NtChallengeResponse = computeNtlmV2Response(ntlmV2Hash,
cm.ServerChallenge[:], clientChallenge, timestamp, cm.TargetInfoRaw)
if cm.TargetInfoRaw == nil {
am.LmChallengeResponse = computeLmV2Response(ntlmV2Hash,
cm.ServerChallenge[:], clientChallenge)
}
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,
})
}

View File

@@ -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
}
}
return ""
}
func (h authheader) IsNegotiate() bool {
for _, s := range h {
if strings.HasPrefix(string(s), "Negotiate") {
return true
}
}
return false
}
func (h authheader) IsNTLM() bool {
for _, s := range h {
if strings.HasPrefix(string(s), "NTLM") {
return true
}
}
return false
}
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
// 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 base64.StdEncoding.DecodeString(string(p[1]))
}
}
return nil, nil
if idx == -1 {
return authheader{}
}
schema, data, _ := strings.Cut(auth[idx], " ")
return authheader{
schema: schema,
data: data,
}
}
func (h authheader) GetBasicCreds() (username, password string, err error) {
d, err := h.GetData()
if err != nil {
return "", "", err
}
parts := strings.SplitN(string(d), ":", 2)
return parts[0], parts[1], nil
// isNTLM returns true if the authheader schema is NTLM or Negotiate.
func (h authheader) isNTLM() bool {
return h.schema == "NTLM" || h.schema == "Negotiate"
}
// isBasic returns true if the authheader schema is Basic.
func (h authheader) isBasic() bool {
return h.schema == "Basic"
}
// 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
}
// RFC4559 4.2 - The token is a base64-encoded value
return base64.StdEncoding.DecodeString(h.data)
}

View File

@@ -1,3 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package ntlmssp
type avID uint16

View File

@@ -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
}

View File

@@ -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.

113
vendor/github.com/Azure/go-ntlmssp/internal/md4/md4.go generated vendored Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,3 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package ntlmssp
import (

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}
if strings.Contains(user, "\\") {
ucomponents := strings.SplitN(user, "\\", 2)
// 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
}
// 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
//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) {
// 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
// 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
}
req.Body.Close()
req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
id := identity{
username: username,
password: password,
}
// first try anonymous, in case the server still finds us
// authenticated from previous traffic
req = req.Clone(req.Context()) // Clone the request to avoid modifying the original
// 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
}
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
}
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)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusUnauthorized {
return res, err
}
resauth = authheader(res.Header.Values("Www-Authenticate"))
if resp.StatusCode != http.StatusUnauthorized {
// No authentication required, return the response as is
return resp, nil
}
if resauth.IsNegotiate() || resauth.IsNTLM() {
// 401 with request:Basic and response:Negotiate
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
// 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
// recycle credentials
u, p, err := reqauth.GetBasicCreds()
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
}
// get domain from username
domain := ""
u, domain, domainNeeded := GetDomain(u)
// send negotiate
negotiateMessage, err := NewNegotiateMessage(domain, "")
if err != nil {
return nil, err
if resp.StatusCode != http.StatusUnauthorized {
// Basic auth succeeded, return the new response
drainResponse(originalResp)
return resp, nil
}
if resauth.IsNTLM() {
req.Header.Set("Authorization", "NTLM "+base64.StdEncoding.EncodeToString(negotiateMessage))
} else {
req.Header.Set("Authorization", "Negotiate "+base64.StdEncoding.EncodeToString(negotiateMessage))
resauth = newAuthHeader(resp.Header)
if !resauth.isNTLM() {
// No NTLM/Negotiate requested, return the response as is
return resp, nil
}
req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
res, err = rt.RoundTrip(req)
if err != nil {
return nil, err
}
// 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()
// 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))
}
req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
return rt.RoundTrip(req)
// 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
}
return res, err
// Server requested Negotiate/NTLM, start handshake
// 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)
// 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
}
type identity struct {
username string
password string
}
func drainResponse(res *http.Response) {
// Drain body and close it to allow reusing the connection
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}
func rewindBody(req *http.Request) error {
if req.Body == nil {
return nil
}
if nb, ok := req.Body.(*negotiatorBody); ok {
return nb.rewind()
}
return nil
}
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
}
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,
}
}
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
}

View File

@@ -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...)

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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),

View File

@@ -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
}

130
vendor/github.com/go-ldap/ldap/v3/postaladdress.go generated vendored Normal file
View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

9
vendor/modules.txt vendored
View File

@@ -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