Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
.vscode
*.exe
+39
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
+107
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.
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Microsoft
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+39
View File
@@ -0,0 +1,39 @@
# 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)
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{},
},
}
req, _ := http.NewRequest("GET", url, nil)
req.SetBasicAuth(user, password)
res, _ := client.Do(req)
```
-----
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
+41
View File
@@ -0,0 +1,41 @@
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.8 BLOCK -->
## Security
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
## Reporting Security Issues
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
## Preferred Languages
We prefer all communications to be in English.
## Policy
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
<!-- END MICROSOFT SECURITY.MD BLOCK -->
+194
View File
@@ -0,0 +1,194 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package ntlmssp
import (
"bytes"
"crypto/rand"
"encoding/binary"
"encoding/hex"
"errors"
"strings"
"time"
)
type authenicateMessage struct {
LmChallengeResponse []byte
NtChallengeResponse []byte
DomainName string
UserName string
Workstation string
// only set if negotiateFlag_NTLMSSP_NEGOTIATE_KEY_EXCH
EncryptedRandomSessionKey []byte
NegotiateFlags negotiateFlags
MIC []byte
}
type authenticateMessageFields struct {
messageHeader
LmChallengeResponse varField
NtChallengeResponse varField
DomainName varField
UserName varField
Workstation varField
_ [8]byte
NegotiateFlags negotiateFlags
}
func (m *authenicateMessage) MarshalBinary() ([]byte, error) {
if !m.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEUNICODE) {
return nil, errors.New("only unicode is supported")
}
domain, user := toUnicode(m.DomainName), toUnicode(m.UserName)
workstation := toUnicode(m.Workstation)
ptr := binary.Size(&authenticateMessageFields{})
f := authenticateMessageFields{
messageHeader: newMessageHeader(3),
NegotiateFlags: m.NegotiateFlags,
LmChallengeResponse: newVarField(&ptr, len(m.LmChallengeResponse)),
NtChallengeResponse: newVarField(&ptr, len(m.NtChallengeResponse)),
DomainName: newVarField(&ptr, len(domain)),
UserName: newVarField(&ptr, len(user)),
Workstation: newVarField(&ptr, len(workstation)),
}
f.NegotiateFlags.Unset(negotiateFlagNTLMSSPNEGOTIATEVERSION)
b := bytes.Buffer{}
if err := binary.Write(&b, binary.LittleEndian, &f); err != nil {
return nil, err
}
if err := binary.Write(&b, binary.LittleEndian, &m.LmChallengeResponse); err != nil {
return nil, err
}
if err := binary.Write(&b, binary.LittleEndian, &m.NtChallengeResponse); err != nil {
return nil, err
}
if err := binary.Write(&b, binary.LittleEndian, &domain); err != nil {
return nil, err
}
if err := binary.Write(&b, binary.LittleEndian, &user); err != nil {
return nil, err
}
if err := binary.Write(&b, binary.LittleEndian, &workstation); err != nil {
return nil, err
}
return b.Bytes(), nil
}
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(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)")
}
if cm.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEKEYEXCH) {
return nil, errors.New("key exchange requested but not supported (NTLMSSP_NEGOTIATE_KEY_EXCH)")
}
am := authenicateMessage{
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
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)
if _, err := rand.Reader.Read(clientChallenge); err != nil {
return nil, err
}
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)
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,
})
}
+65
View File
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package ntlmssp
import (
"encoding/base64"
"net/http"
"strings"
)
var schemaPreference = [...]string{"NTLM", "Negotiate", "Basic"}
type authheader struct {
schema string
data string
}
// 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
}
}
}
}
if idx == -1 {
return authheader{}
}
schema, data, _ := strings.Cut(auth[idx], " ")
return authheader{
schema: schema,
data: data,
}
}
// 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)
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package ntlmssp
type avID uint16
const (
avIDMsvAvEOL avID = iota
avIDMsvAvNbComputerName
avIDMsvAvNbDomainName
avIDMsvAvDNSComputerName
avIDMsvAvDNSDomainName
avIDMsvAvDNSTreeName
avIDMsvAvFlags
avIDMsvAvTimestamp
avIDMsvAvSingleHost
avIDMsvAvTargetName
avIDMsvChannelBindings
)
+85
View File
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package ntlmssp
import (
"bytes"
"encoding/binary"
"fmt"
)
type challengeMessageFields struct {
messageHeader
TargetName varField
NegotiateFlags negotiateFlags
ServerChallenge [8]byte
_ [8]byte
TargetInfo varField
}
func (m challengeMessageFields) IsValid() bool {
return m.messageHeader.IsValid() && m.MessageType == 2
}
type challengeMessage struct {
challengeMessageFields
TargetName string
TargetInfo map[avID][]byte
TargetInfoRaw []byte
}
func (m *challengeMessage) UnmarshalBinary(data []byte) error {
r := bytes.NewReader(data)
err := binary.Read(r, binary.LittleEndian, &m.challengeMessageFields)
if err != nil {
return err
}
if !m.IsValid() {
return fmt.Errorf("message is not a valid challenge message: %+v", m.messageHeader)
}
if m.challengeMessageFields.TargetName.Len > 0 {
m.TargetName, err = m.challengeMessageFields.TargetName.ReadStringFrom(data, m.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEUNICODE))
if err != nil {
return err
}
}
if m.challengeMessageFields.TargetInfo.Len > 0 {
d, err := m.challengeMessageFields.TargetInfo.ReadFrom(data)
m.TargetInfoRaw = d
if err != nil {
return err
}
m.TargetInfo = make(map[avID][]byte)
r := bytes.NewReader(d)
for {
var id avID
var l uint16
err = binary.Read(r, binary.LittleEndian, &id)
if err != nil {
return err
}
if id == avIDMsvAvEOL {
break
}
err = binary.Read(r, binary.LittleEndian, &l)
if err != nil {
return err
}
value := make([]byte, l)
n, err := r.Read(value)
if err != nil {
return err
}
if n != int(l) {
return fmt.Errorf("expected to read %d bytes, got only %d", l, n)
}
m.TargetInfo[id] = value
}
}
return nil
}
+21
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
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
}
+91
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
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package ntlmssp
import (
"bytes"
)
var signature = [8]byte{'N', 'T', 'L', 'M', 'S', 'S', 'P', 0}
type messageHeader struct {
Signature [8]byte
MessageType uint32
}
func (h messageHeader) IsValid() bool {
return bytes.Equal(h.Signature[:], signature[:]) &&
h.MessageType > 0 && h.MessageType < 4
}
func newMessageHeader(messageType uint32) messageHeader {
return messageHeader{signature, messageType}
}
+55
View File
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package ntlmssp
type negotiateFlags uint32
const (
/*A*/ negotiateFlagNTLMSSPNEGOTIATEUNICODE negotiateFlags = 1 << 0
/*B*/ negotiateFlagNTLMNEGOTIATEOEM = 1 << 1
/*C*/ negotiateFlagNTLMSSPREQUESTTARGET = 1 << 2
/*D*/
negotiateFlagNTLMSSPNEGOTIATESIGN = 1 << 4
/*E*/ negotiateFlagNTLMSSPNEGOTIATESEAL = 1 << 5
/*F*/ negotiateFlagNTLMSSPNEGOTIATEDATAGRAM = 1 << 6
/*G*/ negotiateFlagNTLMSSPNEGOTIATELMKEY = 1 << 7
/*H*/
negotiateFlagNTLMSSPNEGOTIATENTLM = 1 << 9
/*J*/
negotiateFlagANONYMOUS = 1 << 11
/*K*/ negotiateFlagNTLMSSPNEGOTIATEOEMDOMAINSUPPLIED = 1 << 12
/*L*/ negotiateFlagNTLMSSPNEGOTIATEOEMWORKSTATIONSUPPLIED = 1 << 13
/*M*/
negotiateFlagNTLMSSPNEGOTIATEALWAYSSIGN = 1 << 15
/*N*/ negotiateFlagNTLMSSPTARGETTYPEDOMAIN = 1 << 16
/*O*/ negotiateFlagNTLMSSPTARGETTYPESERVER = 1 << 17
/*P*/
negotiateFlagNTLMSSPNEGOTIATEEXTENDEDSESSIONSECURITY = 1 << 19
/*Q*/ negotiateFlagNTLMSSPNEGOTIATEIDENTIFY = 1 << 20
/*R*/
negotiateFlagNTLMSSPREQUESTNONNTSESSIONKEY = 1 << 22
/*S*/ negotiateFlagNTLMSSPNEGOTIATETARGETINFO = 1 << 23
/*T*/
negotiateFlagNTLMSSPNEGOTIATEVERSION = 1 << 25
/*U*/
negotiateFlagNTLMSSPNEGOTIATE128 = 1 << 29
/*V*/ negotiateFlagNTLMSSPNEGOTIATEKEYEXCH = 1 << 30
/*W*/ negotiateFlagNTLMSSPNEGOTIATE56 = 1 << 31
)
func (field negotiateFlags) Has(flags negotiateFlags) bool {
return field&flags == flags
}
func (field *negotiateFlags) Unset(flags negotiateFlags) {
*field ^= *field & flags
}
+73
View File
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package ntlmssp
import (
"bytes"
"encoding/binary"
"errors"
"strings"
)
const expMsgBodyLen = 40
type negotiateMessageFields struct {
messageHeader
NegotiateFlags negotiateFlags
Domain varField
Workstation varField
Version
}
var defaultFlags = negotiateFlagNTLMSSPNEGOTIATETARGETINFO |
negotiateFlagNTLMSSPNEGOTIATE56 |
negotiateFlagNTLMSSPNEGOTIATE128 |
negotiateFlagNTLMSSPNEGOTIATEUNICODE |
negotiateFlagNTLMSSPNEGOTIATEEXTENDEDSESSIONSECURITY |
negotiateFlagNTLMSSPNEGOTIATENTLM |
negotiateFlagNTLMSSPNEGOTIATEALWAYSSIGN
// 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 domain != "" {
flags |= negotiateFlagNTLMSSPNEGOTIATEOEMDOMAINSUPPLIED
}
if workstation != "" {
flags |= negotiateFlagNTLMSSPNEGOTIATEOEMWORKSTATIONSUPPLIED
}
msg := negotiateMessageFields{
messageHeader: newMessageHeader(1),
NegotiateFlags: flags,
Domain: newVarField(&payloadOffset, len(domain)),
Workstation: newVarField(&payloadOffset, len(workstation)),
Version: DefaultVersion(),
}
b := bytes.Buffer{}
if err := binary.Write(&b, binary.LittleEndian, &msg); err != nil {
return nil, err
}
if b.Len() != expMsgBodyLen {
return nil, errors.New("incorrect body length")
}
payload := strings.ToUpper(domain + workstation)
if _, err := b.WriteString(payload); err != nil {
return nil, err
}
return b.Bytes(), nil
}
+331
View File
@@ -0,0 +1,331 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package ntlmssp
import (
"bytes"
"encoding/base64"
"io"
"net/http"
"strings"
)
// 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
}
// 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(username, "@") {
user = username
domainNeeded = false
} else {
user = username
domainNeeded = true
}
return user, domain, domainNeeded
}
// 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
// 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
username, password, ok := req.BasicAuth()
if !ok {
return rt.RoundTrip(req)
}
id := identity{
username: username,
password: password,
}
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")
resp, err := rt.RoundTrip(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusUnauthorized {
// No authentication required, return the response as is
return resp, nil
}
// 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 originalResp, nil
}
if resp.StatusCode != http.StatusUnauthorized {
// Basic auth succeeded, return the new response
drainResponse(originalResp)
return resp, nil
}
resauth = newAuthHeader(resp.Header)
if !resauth.isNTLM() {
// No NTLM/Negotiate requested, return the response as is
return resp, nil
}
// 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
}
// 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
}
+57
View File
@@ -0,0 +1,57 @@
// 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
// only supports Unicode (UTF16LE) encoding of protocol strings, no OEM encoding.
// This package implements NTLMv2.
package ntlmssp
import (
"crypto/hmac"
"crypto/md5"
"strings"
"github.com/Azure/go-ntlmssp/internal/md4"
)
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 {
hash := md4.New()
hash.Write(toUnicode(password))
return hash.Sum(nil)
}
func computeNtlmV2Response(ntlmV2Hash, serverChallenge, clientChallenge,
timestamp, targetInfo []byte,
) []byte {
temp := []byte{1, 1, 0, 0, 0, 0, 0, 0}
temp = append(temp, timestamp...)
temp = append(temp, clientChallenge...)
temp = append(temp, 0, 0, 0, 0)
temp = append(temp, targetInfo...)
temp = append(temp, 0, 0, 0, 0)
NTProofStr := hmacMd5(ntlmV2Hash, serverChallenge, temp)
return append(NTProofStr, temp...)
}
func computeLmV2Response(ntlmV2Hash, serverChallenge, clientChallenge []byte) []byte {
return append(hmacMd5(ntlmV2Hash, serverChallenge, clientChallenge), clientChallenge...)
}
func hmacMd5(key []byte, data ...[]byte) []byte {
mac := hmac.New(md5.New, key)
for _, d := range data {
mac.Write(d)
}
return mac.Sum(nil)
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package ntlmssp
import (
"bytes"
"encoding/binary"
"errors"
"unicode/utf16"
)
// helper func's for dealing with Windows Unicode (UTF16LE)
func fromUnicode(d []byte) (string, error) {
if len(d)%2 > 0 {
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)
if err != nil {
return "", err
}
return string(utf16.Decode(s)), nil
}
func toUnicode(s string) []byte {
uints := utf16.Encode([]rune(s))
b := bytes.Buffer{}
_ = binary.Write(&b, binary.LittleEndian, &uints)
return b.Bytes()
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package ntlmssp
import (
"errors"
)
type varField struct {
Len uint16
MaxLen uint16
BufferOffset uint32
}
func (f varField) ReadFrom(buffer []byte) ([]byte, error) {
// 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[int(start):int(end)], nil
}
func (f varField) ReadStringFrom(buffer []byte, unicode bool) (string, error) {
d, err := f.ReadFrom(buffer)
if err != nil {
return "", err
}
if unicode { // UTF-16LE encoding scheme
return fromUnicode(d)
}
// OEM encoding, close enough to ASCII, since no code page is specified
return string(d), err
}
func newVarField(ptr *int, fieldsize int) varField {
f := varField{
Len: uint16(fieldsize),
MaxLen: uint16(fieldsize),
BufferOffset: uint32(*ptr),
}
*ptr += fieldsize
return f
}
+23
View File
@@ -0,0 +1,23 @@
// 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
type Version struct {
ProductMajorVersion uint8
ProductMinorVersion uint8
ProductBuild uint16
_ [3]byte
NTLMRevisionCurrent uint8
}
// DefaultVersion returns a Version with "sensible" defaults (Windows 7)
func DefaultVersion() Version {
return Version{
ProductMajorVersion: 6,
ProductMinorVersion: 1,
ProductBuild: 7601,
NTLMRevisionCurrent: 15,
}
}