diff --git a/go.mod b/go.mod index 1eb8b0f4ede..00d37515b95 100644 --- a/go.mod +++ b/go.mod @@ -113,7 +113,7 @@ require ( require ( contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect filippo.io/edwards25519 v1.1.1 // indirect - github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/Azure/go-ntlmssp v0.1.1 // 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 82d69d00583..d585fdc5a92 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Acconut/go-httptest-recorder v1.0.0 h1:TAv2dfnqp/l+SUvIaMAUK4GeN4+wqb6KZsFFFTGhoJg= github.com/Acconut/go-httptest-recorder v1.0.0/go.mod h1:CwQyhTH1kq/gLyWiRieo7c0uokpu3PXeyF/nZjUNtmM= -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.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= +github.com/Azure/go-ntlmssp v0.1.1/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= @@ -742,12 +742,8 @@ github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HD github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 h1:JRidLTAKhnvyLMRtVtSF4lhBa0NSAOs6fof+d6JnKII= github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245/go.mod h1:z61VMGAJRtR1nbgXWiNoCkxUXP1B3Je9rMuJbnGd+Og= -github.com/owncloud/reva/v2 v2.0.0-20260324082555-823c2f1c2593 h1:RNHAod2gNBEac0KQJfJ6+PCX1t7g9hFmONTGrXFvFII= -github.com/owncloud/reva/v2 v2.0.0-20260324082555-823c2f1c2593/go.mod h1:+rCy6oGYb2/qs5gmQa8y/pHARw634vB73MZGDY2SBIQ= github.com/owncloud/reva/v2 v2.0.0-20260424082219-c88989902d1a h1:Td4vu7/+VJcah08Fei+gaFw6x3gkGtqcXoeirSHYhZ0= github.com/owncloud/reva/v2 v2.0.0-20260424082219-c88989902d1a/go.mod h1:PIquCu54dUEqq2El2bvmkXuyve/WSlgS5Q7V8TluSNo= -github.com/owncloud/reva/v2 v2.0.0-20260424094235-1edcc4f31948 h1:5LTRaguOogJNmEpdmIej+rr2jSbYGxadV0MfXkOxM0w= -github.com/owncloud/reva/v2 v2.0.0-20260424094235-1edcc4f31948/go.mod h1:PIquCu54dUEqq2El2bvmkXuyve/WSlgS5Q7V8TluSNo= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pablodz/inotifywaitgo v0.0.9 h1:njquRbBU7fuwIe5rEvtaniVBjwWzcpdUVptSgzFqZsw= diff --git a/vendor/github.com/Azure/go-ntlmssp/.gitignore b/vendor/github.com/Azure/go-ntlmssp/.gitignore new file mode 100644 index 00000000000..b8a713d11ad --- /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 00000000000..d01771965c8 --- /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 23c95fe951b..00000000000 --- 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 00000000000..a77a6c8d042 --- /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 55cdcefab70..879818390f2 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 ab183db6adc..291696fe83a 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 c9d30d32421..3830b635414 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 196b5f13163..f8d55a9cc5d 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 053b55e4adf..cf283a33f68 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 00000000000..2da0ed58ade --- /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 00000000000..dfd23b8246a --- /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 00000000000..5ea1ba966ea --- /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 247e284652c..21e82c83012 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 5905c023d69..d78ae714047 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 e466a9861d8..a0746cd6edb 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 cce4955df1c..80514dd981a 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 1e65abe8b53..ceffabc07d1 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 7b4f47163d0..ebb9f379c8e 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 15f9aa113d8..2900a083a1c 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 ( @@ -11,10 +14,14 @@ 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") + // f.Len is controlled by the sender, so we need to check that + // it doesn't cause an overflow when added to f.BufferOffset. + start := uint64(f.BufferOffset) + end := start + uint64(f.Len) + if end < start || end > uint64(len(buffer)) { + return nil, errors.New("error reading data, varField extends beyond buffer") } - return buffer[f.BufferOffset : f.BufferOffset+uint32(f.Len)], nil + return buffer[int(start):int(end)], nil } func (f varField) ReadStringFrom(buffer []byte, unicode bool) (string, error) { diff --git a/vendor/github.com/Azure/go-ntlmssp/version.go b/vendor/github.com/Azure/go-ntlmssp/version.go index 6d848921244..50cebb99f2d 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/modules.txt b/vendor/modules.txt index 4044a2276aa..b26fcf9bdfa 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -8,9 +8,10 @@ dario.cat/mergo ## explicit; go 1.20 filippo.io/edwards25519 filippo.io/edwards25519/field -# github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 -## explicit +# github.com/Azure/go-ntlmssp v0.1.1 +## 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