Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
f1ae4c4171 | |||
|
5c0509ce5a | ||
|
057c1408b0 | ||
|
96a505e0be | ||
|
077ad4ac7e | ||
|
65f42f02a9 | ||
|
262fc3ccba | ||
|
28c7ea45cd | ||
|
1f3f29dec2 | ||
|
8deb8dc7e5 | ||
|
30c6b59265 | ||
|
7b9f3e3dc3 | ||
|
bcccbed56d |
14
CHANGELOG.md
Normal file
14
CHANGELOG.md
Normal 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.
|
@@ -1,7 +1,9 @@
|
||||
# go-ntlmssp
|
||||
Golang package that provides NTLM/Negotiate authentication over HTTP
|
||||
|
||||
[](https://godoc.org/github.com/Azure/go-ntlmssp) [](https://travis-ci.org/Azure/go-ntlmssp)
|
||||
[](https://godoc.org/github.com/launchdarkly/go-ntlmssp) [](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
|
||||
|
@@ -3,6 +3,7 @@ package ntlmssp
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"time"
|
||||
@@ -78,13 +79,17 @@ 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
|
||||
// ProcessChallenge crafts an AUTHENTICATE message in response to the CHALLENGE message
|
||||
// that was received from the server
|
||||
func ProcessChallenge(challengeMessageData []byte, user, password string) ([]byte, error) {
|
||||
if user == "" && 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 {
|
||||
return nil, err
|
||||
|
153
debug.go
Normal file
153
debug.go
Normal 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
5
go.mod
Normal 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
2
go.sum
Normal 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=
|
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
@@ -21,12 +20,12 @@ func GetDomain(user string) (string, string) {
|
||||
return user, domain
|
||||
}
|
||||
|
||||
//Negotiator is a http.Roundtripper decorator that automatically
|
||||
//converts basic authentication to NTLM/Negotiate authentication when appropriate.
|
||||
// Negotiator is a http.Roundtripper decorator that automatically
|
||||
// converts basic authentication to NTLM/Negotiate authentication when appropriate.
|
||||
type Negotiator struct{ http.RoundTripper }
|
||||
|
||||
//RoundTrip sends the request to the server, handling any authentication
|
||||
//re-sends as needed.
|
||||
// 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) {
|
||||
// Use default round tripper if not provided
|
||||
rt := l.RoundTripper
|
||||
@@ -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 {
|
||||
@@ -121,7 +125,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error)
|
||||
// Negotiation failed, let client deal with response
|
||||
return res, nil
|
||||
}
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
io.Copy(io.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
|
||||
// send authenticate
|
||||
@@ -129,13 +133,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)
|
||||
}
|
||||
|
14
nlmp_test.go
14
nlmp_test.go
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user