17 Commits

Author SHA1 Message Date
adaea700ae blah 2025-03-17 11:28:30 +11:00
468f0240be more logging 2025-03-17 11:26:58 +11:00
f8cadb2287 debugging 2025-03-17 09:48:17 +11:00
befb826740 more logging 2025-03-17 09:30:04 +11:00
f1ae4c4171 add debugging 2025-03-17 08:42:11 +11:00
LaunchDarklyCI
5c0509ce5a Releasing version 1.0.1 2020-06-11 18:56:13 +00:00
Eli Bishop
057c1408b0 add release metadata 2020-06-11 11:55:47 -07:00
Eli Bishop
96a505e0be Merge branch 'master' of github.com:launchdarkly/go-ntlmssp 2020-06-11 11:52:50 -07:00
Eli Bishop
077ad4ac7e add changelog 2020-06-11 11:50:10 -07:00
Eli Bishop
65f42f02a9 add go.mod, build as module in Go 1.13+ (#3) 2020-06-11 11:49:10 -07:00
Eli Bishop
262fc3ccba Merge pull request #2 from launchdarkly/eb/ch79227/test-fix-and-readme
fix unit test, add CI build, update readme to clarify that this is a fork
2020-06-11 11:48:24 -07:00
Eli Bishop
28c7ea45cd fix copy-paste error again 2020-06-09 11:32:56 -07:00
Eli Bishop
1f3f29dec2 fix copy-paste error 2020-06-09 11:32:18 -07:00
Eli Bishop
8deb8dc7e5 change unit test expectation to match current encoding behavior 2020-06-09 11:20:01 -07:00
Eli Bishop
30c6b59265 add go get step 2020-06-09 11:12:03 -07:00
Eli Bishop
7b9f3e3dc3 add CircleCI config 2020-06-09 11:10:30 -07:00
Eli Bishop
bcccbed56d add fork explanation to readme 2020-06-09 11:00:57 -07:00
9 changed files with 234 additions and 23 deletions

14
CHANGELOG.md Normal file
View File

@@ -0,0 +1,14 @@
# Change log
All notable changes to the project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
## [1.0.1] - 2020-06-11
### Added:
- Added `go.mod` file so this package can now be consumed as a module. Since the major version is still `1` and it does not have any module-only dependencies, it can still be used by non-module projects as well.
- Added CircleCI build.
### Fixed:
- Fixed a broken unit test.
## [1.0.0] - 2019-05-31
Initial release of this fork. The only change from the upstream code is https://github.com/launchdarkly/go-ntlmssp/pull/1.

View File

@@ -1,7 +1,9 @@
# 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)
[![GoDoc](https://godoc.org/github.com/launchdarkly/go-ntlmssp?status.svg)](https://godoc.org/github.com/launchdarkly/go-ntlmssp) [![Circle CI](https://circleci.com/gh/launchdarkly/go-ntlmssp.svg?style=svg)](https://circleci.com/gh/launchdarkly/go-ntlmssp)
This is a fork of [github.com/Azure/go-ntlmssp](https://github.com/Azure/go-ntlmssp), with minor changes for use in the [LaunchDarkly Go SDK](https://github.com/launchdarkly/go-server-sdk).
Protocol details from https://msdn.microsoft.com/en-us/library/cc236621.aspx
Implementation hints from http://davenport.sourceforge.net/ntlm.html

View File

@@ -3,6 +3,7 @@ package ntlmssp
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"errors"
"time"
@@ -82,18 +83,26 @@ func (m authenicateMessage) MarshalBinary() ([]byte, error) {
// that was received from the server
func ProcessChallenge(challengeMessageData []byte, user, password string) ([]byte, error) {
if user == "" && password == "" {
PrintDebug("User %s, Pass Length %d", user, len(password))
return nil, errors.New("Anonymous authentication not supported")
}
// debugging
PrintDebug("Received NTLM Type 2 Challenge: %s", base64.StdEncoding.EncodeToString(challengeMessageData))
DecodeNTLMMessage(challengeMessageData)
var cm challengeMessage
if err := cm.UnmarshalBinary(challengeMessageData); err != nil {
PrintDebug("Failed to unmarshal challenge data")
return nil, err
}
if cm.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATELMKEY) {
PrintDebug("server requested NTLM v1")
return nil, errors.New("Only NTLM v2 is supported, but server requested v1 (NTLMSSP_NEGOTIATE_LM_KEY)")
}
if cm.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEKEYEXCH) {
PrintDebug("Key exchange requested but not supported")
return nil, errors.New("Key exchange requested but not supported (NTLMSSP_NEGOTIATE_KEY_EXCH)")
}
@@ -115,6 +124,7 @@ func ProcessChallenge(challengeMessageData []byte, user, password string) ([]byt
rand.Reader.Read(clientChallenge)
ntlmV2Hash := getNtlmV2Hash(password, user, cm.TargetName)
PrintDebug("NTLM V2 hash '%s'", base64.StdEncoding.EncodeToString(ntlmV2Hash))
am.NtChallengeResponse = computeNtlmV2Response(ntlmV2Hash,
cm.ServerChallenge[:], clientChallenge, timestamp, cm.TargetInfoRaw)
@@ -124,5 +134,7 @@ func ProcessChallenge(challengeMessageData []byte, user, password string) ([]byt
cm.ServerChallenge[:], clientChallenge)
}
PrintDebug("Challenge response:\nNT '%s';\nLM '%s'", base64.StdEncoding.EncodeToString(am.NtChallengeResponse), base64.StdEncoding.EncodeToString(am.LmChallengeResponse))
return am.MarshalBinary()
}

153
debug.go Normal file
View File

@@ -0,0 +1,153 @@
package ntlmssp
import (
"encoding/binary"
"fmt"
"strings"
)
// Debugging flag
var DebugMode = true
// PrintDebug logs debug messages when DebugMode is enabled
func PrintDebug(format string, args ...interface{}) {
if DebugMode {
fmt.Printf(format+"\n", args...)
}
}
// DecodeNTLMMessage decodes NTLM messages and prints details
func DecodeNTLMMessage(blob []byte) {
if len(blob) < 12 {
PrintDebug("Invalid NTLM message (too short)")
return
}
if string(blob[:8]) != "NTLMSSP\x00" {
PrintDebug("Invalid NTLM signature")
return
}
msgType := binary.LittleEndian.Uint32(blob[8:12])
switch msgType {
case 1:
DecodeType1Message(blob)
case 2:
DecodeType2Message(blob)
case 3:
DecodeType3Message(blob)
default:
PrintDebug("Unknown NTLM message type: %d", msgType)
}
}
// DecodeType1Message prints details of an NTLM Type 1 message
func DecodeType1Message(blob []byte) {
PrintDebug("==== Type1 ----")
PrintDebug("Signature: NTLMSSP")
PrintDebug("Type: 1")
if len(blob) < 32 {
PrintDebug("Invalid NTLM Type 1 message")
return
}
flags := binary.LittleEndian.Uint32(blob[12:16])
PrintDebug("Flags: %08X", flags)
PrintDebug(DecodeFlags(flags))
domainLen := binary.LittleEndian.Uint16(blob[16:18])
domainMaxLen := binary.LittleEndian.Uint16(blob[18:20])
domainOffset := binary.LittleEndian.Uint32(blob[20:24])
workstationLen := binary.LittleEndian.Uint16(blob[24:26])
workstationMaxLen := binary.LittleEndian.Uint16(blob[26:28])
workstationOffset := binary.LittleEndian.Uint32(blob[28:32])
if domainMaxLen > 0 && int(domainOffset+uint32(domainLen)) <= len(blob) {
domain := string(blob[domainOffset : domainOffset+uint32(domainLen)])
PrintDebug("Domain: %s", domain)
}
if workstationMaxLen > 0 && int(workstationOffset+uint32(workstationLen)) <= len(blob) {
workstation := string(blob[workstationOffset : workstationOffset+uint32(workstationLen)])
PrintDebug("Workstation: %s", workstation)
}
}
// DecodeType2Message prints details of an NTLM Type 2 message
func DecodeType2Message(blob []byte) {
PrintDebug("==== Type2 ----")
PrintDebug("Signature: NTLMSSP")
PrintDebug("Type: 2")
if len(blob) < 48 {
PrintDebug("Invalid NTLM Type 2 message")
return
}
flags := binary.LittleEndian.Uint32(blob[20:24])
PrintDebug("Flags: %08X", flags)
PrintDebug(DecodeFlags(flags))
challenge := blob[24:32]
PrintDebug("Challenge: %X", challenge)
context := blob[40:48]
PrintDebug("Context: %X:%X", context[:4], context[4:])
}
// DecodeType3Message prints details of an NTLM Type 3 message
func DecodeType3Message(blob []byte) {
PrintDebug("==== Type3 ----")
PrintDebug("Signature: NTLMSSP")
PrintDebug("Type: 3")
if len(blob) < 64 {
PrintDebug("Invalid NTLM Type 3 message")
return
}
flags := binary.LittleEndian.Uint32(blob[60:64])
PrintDebug("Flags: %08X", flags)
PrintDebug(DecodeFlags(flags))
}
// DecodeFlags returns a formatted string listing NTLM flag names
func DecodeFlags(flags uint32) string {
var flagStrings []string
flagMap := map[uint32]string{
0x00000001: "NTLMSSP_NEGOTIATE_UNICODE",
0x00000002: "NTLMSSP_NEGOTIATE_OEM",
0x00000004: "NTLMSSP_REQUEST_TARGET",
0x00000010: "NTLMSSP_NEGOTIATE_SIGN",
0x00000020: "NTLMSSP_NEGOTIATE_SEAL",
0x00000040: "NTLMSSP_NEGOTIATE_DATAGRAM",
0x00000080: "NTLMSSP_NEGOTIATE_LM_KEY",
0x00000100: "NTLMSSP_NEGOTIATE_NETWARE",
0x00000200: "NTLMSSP_NEGOTIATE_NTLM",
0x00000800: "NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED",
0x00001000: "NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED",
0x00002000: "NTLMSSP_NEGOTIATE_ALWAYS_SIGN",
0x00020000: "NTLMSSP_NEGOTIATE_TARGET_INFO",
0x00040000: "NTLMSSP_REQUEST_NON_NT_SESSION_KEY",
0x00080000: "NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY",
0x00100000: "NTLMSSP_NEGOTIATE_IDENTIFY",
0x00200000: "NTLMSSP_REQUEST_TARGET",
0x00800000: "NTLMSSP_TARGET_TYPE_DOMAIN",
0x01000000: "NTLMSSP_TARGET_TYPE_SERVER",
0x02000000: "NTLMSSP_TARGET_TYPE_SHARE",
0x08000000: "NTLMSSP_NEGOTIATE_KEY_EXCH",
0x20000000: "NTLMSSP_NEGOTIATE_128",
0x80000000: "NTLMSSP_NEGOTIATE_56",
}
for bit, name := range flagMap {
if flags&bit != 0 {
flagStrings = append(flagStrings, name)
}
}
return strings.Join(flagStrings, "\n")
}

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module git.coadcorp.com/nathan/go-ntlmssp
go 1.24.1
require golang.org/x/crypto v0.36.0

2
go.sum Normal file
View File

@@ -0,0 +1,2 @@
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=

View File

@@ -4,7 +4,6 @@ import (
"bytes"
"encoding/base64"
"io"
"io/ioutil"
"net/http"
"strings"
)
@@ -47,7 +46,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error)
}
req.Body.Close()
req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
req.Body = io.NopCloser(bytes.NewReader(body.Bytes()))
}
// first try anonymous, in case the server still finds us
// authenticated from previous traffic
@@ -64,9 +63,9 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error)
if !resauth.IsNegotiate() && !resauth.IsNTLM() {
// Unauthorized, Negotiate not requested, let's try with basic auth
req.Header.Set("Authorization", string(reqauth))
io.Copy(ioutil.Discard, res.Body)
io.Copy(io.Discard, res.Body)
res.Body.Close()
req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
req.Body = io.NopCloser(bytes.NewReader(body.Bytes()))
res, err = rt.RoundTrip(req)
if err != nil {
@@ -80,7 +79,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error)
if resauth.IsNegotiate() || resauth.IsNTLM() {
// 401 with request:Basic and response:Negotiate
io.Copy(ioutil.Discard, res.Body)
io.Copy(io.Discard, res.Body)
res.Body.Close()
// recycle credentials
@@ -98,13 +97,18 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error)
if err != nil {
return nil, err
}
// debugging
PrintDebug("Generated NTLM Type 1 Message: %s", base64.StdEncoding.EncodeToString(negotiateMessage))
DecodeNTLMMessage(negotiateMessage)
if resauth.IsNTLM() {
req.Header.Set("Authorization", "NTLM "+base64.StdEncoding.EncodeToString(negotiateMessage))
} else {
req.Header.Set("Authorization", "Negotiate "+base64.StdEncoding.EncodeToString(negotiateMessage))
}
req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
req.Body = io.NopCloser(bytes.NewReader(body.Bytes()))
res, err = rt.RoundTrip(req)
if err != nil {
@@ -117,11 +121,17 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error)
if err != nil {
return nil, err
}
// debugging
//PrintDebug("Received NTLM Type 2 Challenge: %s", base64.StdEncoding.EncodeToString(challengeMessage))
//DecodeNTLMMessage(challengeMessage)
if !(resauth.IsNegotiate() || resauth.IsNTLM()) || len(challengeMessage) == 0 {
// Negotiation failed, let client deal with response
PrintDebug("Negotiation failed")
return res, nil
}
io.Copy(ioutil.Discard, res.Body)
io.Copy(io.Discard, res.Body)
res.Body.Close()
// send authenticate
@@ -129,13 +139,18 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error)
if err != nil {
return nil, err
}
// debugging
PrintDebug("Generated NTLM Type 3 Response: %s", base64.StdEncoding.EncodeToString(authenticateMessage))
DecodeNTLMMessage(authenticateMessage)
if resauth.IsNTLM() {
req.Header.Set("Authorization", "NTLM "+base64.StdEncoding.EncodeToString(authenticateMessage))
} else {
req.Header.Set("Authorization", "Negotiate "+base64.StdEncoding.EncodeToString(authenticateMessage))
}
req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
req.Body = io.NopCloser(bytes.NewReader(body.Bytes()))
res, err = rt.RoundTrip(req)
}

10
nlmp.go
View File

@@ -10,8 +10,9 @@ package ntlmssp
import (
"crypto/hmac"
"crypto/md5"
"golang.org/x/crypto/md4"
"strings"
"golang.org/x/crypto/md4"
)
func getNtlmV2Hash(password, username, target string) []byte {
@@ -28,13 +29,20 @@ func computeNtlmV2Response(ntlmV2Hash, serverChallenge, clientChallenge,
timestamp, targetInfo []byte) []byte {
temp := []byte{1, 1, 0, 0, 0, 0, 0, 0}
PrintDebug("NTLMv2 response", temp)
temp = append(temp, timestamp...)
PrintDebug("NTLMv2 response", temp)
temp = append(temp, clientChallenge...)
PrintDebug("NTLMv2 response", temp)
temp = append(temp, 0, 0, 0, 0)
PrintDebug("NTLMv2 response", temp)
temp = append(temp, targetInfo...)
PrintDebug("NTLMv2 response", temp)
temp = append(temp, 0, 0, 0, 0)
PrintDebug("NTLMv2 response", temp)
NTProofStr := hmacMd5(ntlmV2Hash, serverChallenge, temp)
PrintDebug("Proof string", NTProofStr)
return append(NTProofStr, temp...)
}

View File

@@ -25,21 +25,21 @@ func TestUsernameDomainWorkstation(t *testing.T) {
xb []byte
}{
{username, "", username, "", []byte{
0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00,
0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x82,
0x88, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x28, 0x00, 0x00, 0x00, 0x06, 0x01, 0xb1, 0x1d, 0x00, 0x00, 0x00, 0x0f}},
{domain + "\\" + username, "", username, domain, []byte{
0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x10,
0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x92,
0x88, 0xa0, 0x08, 0x00, 0x08, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x30, 0x00, 0x00, 0x00, 0x06, 0x01, 0xb1, 0x1d, 0x00, 0x00, 0x00, 0x0f, 0x4d, 0x59,
0x44, 0x4f, 0x4d, 0x41, 0x49, 0x4e}},
{domain + "\\" + username, workstation, username, domain, []byte{
0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x30,
0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xb2,
0x88, 0xa0, 0x08, 0x00, 0x08, 0x00, 0x28, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00,
0x30, 0x00, 0x00, 0x00, 0x06, 0x01, 0xb1, 0x1d, 0x00, 0x00, 0x00, 0x0f, 0x4d, 0x59,
0x44, 0x4f, 0x4d, 0x41, 0x49, 0x4e, 0x4d, 0x59, 0x50, 0x43}},
{username, workstation, username, "", []byte{
0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x20,
0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xa2,
0x88, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00,
0x28, 0x00, 0x00, 0x00, 0x06, 0x01, 0xb1, 0x1d, 0x00, 0x00, 0x00, 0x0f, 0x4d, 0x59,
0x50, 0x43}},
@@ -48,10 +48,10 @@ func TestUsernameDomainWorkstation(t *testing.T) {
for _, table := range tables {
tuser, tdomain := GetDomain(table.u)
if tuser != table.xu {
t.Fatalf("username not correct, expected %v got %v", tuser, table.xu)
t.Fatalf("username not correct, expected %v got %v", table.xu, tuser)
}
if tdomain != table.xd {
t.Fatalf("domain not correct, expected %v got %v", tdomain, table.xd)
t.Fatalf("domain not correct, expected %v got %v", table.xd, tdomain)
}
tb, err := NewNegotiateMessage(tdomain, table.w)
@@ -60,7 +60,7 @@ func TestUsernameDomainWorkstation(t *testing.T) {
}
if !bytes.Equal(tb, table.xb) {
t.Fatalf("negotiate message bytes not correct, expected %v got %v", tb, table.xb)
t.Fatalf("negotiate message bytes not correct, expected %v got %v", table.xb, tb)
}
}