mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-21 21:30:50 +01:00
Add experimental NTLMv2 authentication support
Introduced NTLMv2 authentication mechanism with tests in smtp package. Utilized the go-ntlmssp library to handle negotiation and response processing for NTLMv2 protocol.
This commit is contained in:
parent
50a4d0cc6f
commit
f883358a29
4 changed files with 204 additions and 0 deletions
1
go.mod
1
go.mod
|
@ -7,6 +7,7 @@ module github.com/wneessen/go-mail
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
|
||||||
golang.org/x/crypto v0.29.0
|
golang.org/x/crypto v0.29.0
|
||||||
golang.org/x/text v0.20.0
|
golang.org/x/text v0.20.0
|
||||||
)
|
)
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -1,3 +1,5 @@
|
||||||
|
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/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
|
52
smtp/auth_ntlm.go
Normal file
52
smtp/auth_ntlm.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Azure/go-ntlmssp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNTLMChallangeEmpty is returned when the NTLMv2 ChallengeMessage received from the server is empty.
|
||||||
|
var ErrNTLMChallangeEmpty = fmt.Errorf("NTLMv2 ChallengeMessage is empty")
|
||||||
|
|
||||||
|
// ntlmAuth represents a NTLM client and satisfies the smtp.Auth interface.
|
||||||
|
type ntlmAuth struct {
|
||||||
|
domain, password, username, workstation string
|
||||||
|
domainNeeded bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NTLMv2Auth creates and returns a new NTLMv2 authentication mechanism with the given
|
||||||
|
// username and password.
|
||||||
|
func NTLMv2Auth(username, password, workstation string) Auth {
|
||||||
|
user, domain, domainNeeded := ntlmssp.GetDomain(username)
|
||||||
|
return &ntlmAuth{
|
||||||
|
domain: domain,
|
||||||
|
password: password,
|
||||||
|
username: user,
|
||||||
|
workstation: workstation,
|
||||||
|
domainNeeded: domainNeeded,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initializes the NTLMv2 authentication process and returns the algorithm, the negotiation data, and
|
||||||
|
// a potential error
|
||||||
|
func (a *ntlmAuth) Start(_ *ServerInfo) (string, []byte, error) {
|
||||||
|
negotiateMessage, err := ntlmssp.NewNegotiateMessage(a.domain, a.workstation)
|
||||||
|
return "NTLM", negotiateMessage, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next processes the server's challenge and returns the client's response for NTLMv2 authentication.
|
||||||
|
func (a *ntlmAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||||
|
if more {
|
||||||
|
if len(fromServer) == 0 {
|
||||||
|
return nil, ErrNTLMChallangeEmpty
|
||||||
|
}
|
||||||
|
authenticateMessage, err := ntlmssp.ProcessChallenge(fromServer, a.username, a.password, a.domainNeeded)
|
||||||
|
return authenticateMessage, err
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
|
@ -1462,6 +1462,155 @@ func TestCRAMMD5Auth(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNTLMv2Auth(t *testing.T) {
|
||||||
|
var (
|
||||||
|
username = "user"
|
||||||
|
password = "SecREt01"
|
||||||
|
//ntlmHashHex = "CD06CA7C7E10C99B1D33B7485A2ED808" // NT Hash in hex of "SecREt01"
|
||||||
|
//target = "DOMAIN"
|
||||||
|
domain = "MYDOMAIN"
|
||||||
|
workstation = "MYPC"
|
||||||
|
//challenge = []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}
|
||||||
|
)
|
||||||
|
tests := []struct {
|
||||||
|
user string
|
||||||
|
workstation string
|
||||||
|
wantUser string
|
||||||
|
wantDomain string
|
||||||
|
wantNegotiate []byte
|
||||||
|
}{
|
||||||
|
{username, "", username, "", []byte{
|
||||||
|
0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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}},
|
||||||
|
}
|
||||||
|
t.Run("different user, domain and workstation combinations", func(t *testing.T) {
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run("NTLMv2 init for "+tt.workstation+`\`+tt.user, func(t *testing.T) {
|
||||||
|
auth, ok := NTLMv2Auth(tt.user, password, tt.workstation).(*ntlmAuth)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("failed to initialize NTLMv2 authentication")
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(auth.username, tt.wantUser) {
|
||||||
|
t.Errorf("NTLMv2 authentication username failed, expected: %s, got: %s", tt.wantUser, auth.username)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(auth.domain, tt.wantDomain) {
|
||||||
|
t.Errorf("NTLMv2 authentication domain failed, expected: %s, got: %s", tt.wantDomain, auth.domain)
|
||||||
|
}
|
||||||
|
method, negotiation, err := auth.Start(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to start NTLMv2 authentication: %s", err)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(method, "NTLM") {
|
||||||
|
t.Errorf("NTLMv2 authentication method failed, expected method: NTLM, got: %s", method)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(negotiation, tt.wantNegotiate) {
|
||||||
|
t.Errorf("NTLMv2 authentication negotiation failed, expected: %s, got: %s", tt.wantNegotiate,
|
||||||
|
negotiation)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("authenticate with NTLMv2 ", func(t *testing.T) {
|
||||||
|
auth := NTLMv2Auth("username", "password", "")
|
||||||
|
client, err := Dial("127.0.0.1:2525")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to dial to test server: %s", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err = client.Close(); err != nil {
|
||||||
|
t.Fatalf("failed to close client: %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err = client.Hello("127.0.0.1"); err != nil {
|
||||||
|
t.Errorf("failed to send hello to test server: %s", err)
|
||||||
|
}
|
||||||
|
if err = client.Auth(auth); err != nil {
|
||||||
|
t.Errorf("failed to authenticate with NTLMv2: %s", err)
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run("NTLMv2 init for "+tt.workstation+`\`+tt.user, func(t *testing.T) {
|
||||||
|
_, negotiation, err := auth.Start(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to start NTLMv2 authentication: %s", err)
|
||||||
|
}
|
||||||
|
resp, err := auth.Next(challenge, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("server challange failed: %s", err)
|
||||||
|
}
|
||||||
|
_ = negotiation
|
||||||
|
t.Log(resp)
|
||||||
|
hashBytes, err := hex.DecodeString(ntlmHashHex)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to hex decode hash: %s", ntlmHashHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientChallenge := []byte{0xff, 0xff, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44}
|
||||||
|
timestamp := []byte{0x00, 0x90, 0xd3, 0x36, 0xb7, 0x34, 0xc3, 0x01}
|
||||||
|
targetInfo := []byte{0x02, 0x00, 0x0c, 0x00, 0x44, 0x00, 0x4f, 0x00, 0x4d, 0x00, 0x41, 0x00, 0x49, 0x00, 0x4e, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x53, 0x00, 0x45, 0x00, 0x52, 0x00, 0x56, 0x00, 0x45, 0x00, 0x52, 0x00, 0x04, 0x00, 0x14, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x03, 0x00, 0x22, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||||
|
|
||||||
|
mac := hmac.New(md5.New, hashBytes)
|
||||||
|
|
||||||
|
uints := utf16.Encode([]rune(strings.ToUpper(username) + target))
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
if err = binary.Write(buf, binary.LittleEndian, &uints); err != nil {
|
||||||
|
t.Fatalf("failed to convert username to utf16: %s", err)
|
||||||
|
}
|
||||||
|
mac.Write(buf.Bytes())
|
||||||
|
ntlmhash := mac.Sum(nil)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
mac = hmac.New(md5.New, ntlmhash)
|
||||||
|
mac.Write(challenge)
|
||||||
|
mac.Write(temp)
|
||||||
|
proofString := mac.Sum(nil)
|
||||||
|
proofString = append(proofString, temp...)
|
||||||
|
|
||||||
|
expected := []byte{
|
||||||
|
0xcb, 0xab, 0xbc, 0xa7, 0x13, 0xeb, 0x79, 0x5d, 0x04, 0xc9, 0x7a, 0xbc, 0x01, 0xee, 0x49, 0x83,
|
||||||
|
0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xd3, 0x36, 0xb7, 0x34, 0xc3, 0x01,
|
||||||
|
0xff, 0xff, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x0c, 0x00,
|
||||||
|
0x44, 0x00, 0x4f, 0x00, 0x4d, 0x00, 0x41, 0x00, 0x49, 0x00, 0x4e, 0x00, 0x01, 0x00, 0x0c, 0x00,
|
||||||
|
0x53, 0x00, 0x45, 0x00, 0x52, 0x00, 0x56, 0x00, 0x45, 0x00, 0x52, 0x00, 0x04, 0x00, 0x14, 0x00,
|
||||||
|
0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00,
|
||||||
|
0x6f, 0x00, 0x6d, 0x00, 0x03, 0x00, 0x22, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00,
|
||||||
|
0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00,
|
||||||
|
0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
}
|
||||||
|
if !bytes.Equal(expected, proofString) {
|
||||||
|
t.Errorf("failed to match expected %x, %x", expected, proofString)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewClient(t *testing.T) {
|
func TestNewClient(t *testing.T) {
|
||||||
t.Run("new client via Dial succeeds", func(t *testing.T) {
|
t.Run("new client via Dial succeeds", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
Loading…
Reference in a new issue