Skip to content

Commit ad3fa74

Browse files
Chrizpycpuschma
andauthored
Add RFC 5929 channel binding support for SSPI client (#565)
* Add RFC 5929 channel binding support for SSPI client * go get github.com/alexbrainman/sspi --------- Co-authored-by: Christopher Puschmann <[email protected]>
1 parent 9feb1c8 commit ad3fa74

File tree

4 files changed

+206
-6
lines changed

4 files changed

+206
-6
lines changed

v3/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.23.0
44

55
require (
66
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
7-
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa
7+
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e
88
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667
99
github.com/google/uuid v1.6.0
1010
github.com/jcmturner/gokrb5/v8 v8.4.4

v3/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
22
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
33
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
44
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
5+
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
6+
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
57
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
68
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
79
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

v3/gssapi/sspi.go

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package gssapi
55

66
import (
77
"bytes"
8+
"crypto"
9+
"crypto/x509"
810
"encoding/binary"
911
"fmt"
1012

@@ -15,8 +17,9 @@ import (
1517
// SSPIClient implements ldap.GSSAPIClient interface.
1618
// Depends on secur32.dll.
1719
type SSPIClient struct {
18-
creds *sspi.Credentials
19-
ctx *kerberos.ClientContext
20+
creds *sspi.Credentials
21+
ctx *kerberos.ClientContext
22+
channelBindings []byte
2023
}
2124

2225
// NewSSPIClient returns a client with credentials of the current user.
@@ -49,6 +52,26 @@ func NewSSPIClientWithUserCredentials(domain, username, password string) (*SSPIC
4952
}, nil
5053
}
5154

55+
// NewSSPIClientWithChannelBinding creates an RFC 5929 compliant client.
56+
func NewSSPIClientWithChannelBinding(cert *x509.Certificate) (*SSPIClient, error) {
57+
creds, err := kerberos.AcquireCurrentUserCredentials()
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
certHash := calculateCertificateHash(cert)
63+
if certHash == nil {
64+
return nil, fmt.Errorf("failed to calculate certificate hash")
65+
}
66+
67+
tlsChannelBinding := append([]byte("tls-server-end-point:"), certHash...)
68+
69+
return &SSPIClient{
70+
creds: creds,
71+
channelBindings: createChannelBindingsStructure(tlsChannelBinding),
72+
}, nil
73+
}
74+
5275
// Close deletes any established secure context and closes the client.
5376
func (c *SSPIClient) Close() error {
5477
err1 := c.DeleteSecContext()
@@ -82,15 +105,25 @@ func (c *SSPIClient) InitSecContextWithOptions(target string, token []byte, APOp
82105

83106
switch token {
84107
case nil:
85-
ctx, completed, output, err := kerberos.NewClientContextWithFlags(c.creds, target, sspiFlags)
108+
// Use channel bindings if available, otherwise fall back to the standard method.
109+
var ctx *kerberos.ClientContext
110+
var completed bool
111+
var output []byte
112+
var err error
113+
114+
if len(c.channelBindings) > 0 {
115+
ctx, completed, output, err = kerberos.NewClientContextWithChannelBindings(c.creds, target, sspiFlags, c.channelBindings)
116+
} else {
117+
ctx, completed, output, err = kerberos.NewClientContextWithFlags(c.creds, target, sspiFlags)
118+
}
119+
86120
if err != nil {
87121
return nil, false, err
88122
}
89123
c.ctx = ctx
90124

91125
return output, !completed, nil
92126
default:
93-
94127
completed, output, err := c.ctx.Update(token)
95128
if err != nil {
96129
return nil, false, err
@@ -99,7 +132,6 @@ func (c *SSPIClient) InitSecContextWithOptions(target string, token []byte, APOp
99132
return nil, false, fmt.Errorf("error verifying flags: %v", err)
100133
}
101134
return output, !completed, nil
102-
103135
}
104136
}
105137

@@ -196,3 +228,61 @@ func handshakePayload(secLayer byte, maxSize uint32, authzid []byte) []byte {
196228

197229
return payload
198230
}
231+
232+
// createChannelBindingsStructure creates a Windows SEC_CHANNEL_BINDINGS structure.
233+
// This is the format that Windows SSPI expects for channel binding tokens.
234+
// https://learn.microsoft.com/en-us/windows/win32/api/sspi/ns-sspi-sec_channel_bindings
235+
func createChannelBindingsStructure(applicationData []byte) []byte {
236+
const headerSize = 32 // 8 DWORDs * 4 bytes each
237+
appDataLen := uint32(len(applicationData))
238+
appDataOffset := uint32(headerSize)
239+
240+
buf := make([]byte, headerSize+len(applicationData))
241+
242+
// All initiator and acceptor fields are 0 for TLS channel binding.
243+
binary.LittleEndian.PutUint32(buf[24:], appDataLen) // cbApplicationDataLength
244+
binary.LittleEndian.PutUint32(buf[28:], appDataOffset) // dwApplicationDataOffset
245+
246+
copy(buf[headerSize:], applicationData)
247+
248+
return buf
249+
}
250+
251+
// calculateCertificateHash implements RFC 5929 certificate hash calculation.
252+
// https://www.rfc-editor.org/rfc/rfc5929.html#section-4.1
253+
func calculateCertificateHash(cert *x509.Certificate) []byte {
254+
var hashFunc crypto.Hash
255+
256+
switch cert.SignatureAlgorithm {
257+
case x509.SHA256WithRSA,
258+
x509.SHA256WithRSAPSS,
259+
x509.ECDSAWithSHA256,
260+
x509.DSAWithSHA256:
261+
262+
hashFunc = crypto.SHA256
263+
case x509.SHA384WithRSA,
264+
x509.SHA384WithRSAPSS,
265+
x509.ECDSAWithSHA384:
266+
267+
hashFunc = crypto.SHA384
268+
case x509.SHA512WithRSA,
269+
x509.SHA512WithRSAPSS,
270+
x509.ECDSAWithSHA512:
271+
272+
hashFunc = crypto.SHA512
273+
case x509.MD5WithRSA,
274+
x509.SHA1WithRSA,
275+
x509.ECDSAWithSHA1,
276+
x509.DSAWithSHA1:
277+
278+
hashFunc = crypto.SHA256
279+
default:
280+
return nil
281+
}
282+
283+
hasher := hashFunc.New()
284+
285+
// Important to hash cert in DER format.
286+
hasher.Write(cert.Raw)
287+
return hasher.Sum(nil)
288+
}

v3/gssapi/sspi_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//go:build windows
2+
// +build windows
3+
4+
package gssapi
5+
6+
import (
7+
"crypto/rand"
8+
"crypto/rsa"
9+
"crypto/x509"
10+
"crypto/x509/pkix"
11+
"math/big"
12+
"strings"
13+
"testing"
14+
"time"
15+
)
16+
17+
// createTestCertificate creates a test certificate with the specified signature algorithm.
18+
func createTestCertificate(sigAlg x509.SignatureAlgorithm) (*x509.Certificate, error) {
19+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
template := x509.Certificate{
25+
SignatureAlgorithm: sigAlg,
26+
SerialNumber: big.NewInt(1),
27+
Subject: pkix.Name{
28+
Organization: []string{"Test Company"},
29+
Country: []string{"US"},
30+
Province: []string{""},
31+
Locality: []string{"San Francisco"},
32+
StreetAddress: []string{""},
33+
PostalCode: []string{""},
34+
},
35+
NotBefore: time.Now(),
36+
NotAfter: time.Now().Add(365 * 24 * time.Hour),
37+
SubjectKeyId: []byte{1, 2, 3, 4, 6},
38+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
39+
KeyUsage: x509.KeyUsageDigitalSignature,
40+
}
41+
42+
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
cert, err := x509.ParseCertificate(certDER)
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
return cert, nil
53+
}
54+
55+
func TestNewSSPIClientWithChannelBinding(t *testing.T) {
56+
tests := []struct {
57+
name string
58+
sigAlg x509.SignatureAlgorithm
59+
}{
60+
{
61+
name: x509.SHA256WithRSA.String(),
62+
sigAlg: x509.SHA256WithRSA,
63+
},
64+
{
65+
name: x509.SHA384WithRSA.String(),
66+
sigAlg: x509.SHA384WithRSA,
67+
},
68+
{
69+
name: x509.SHA512WithRSA.String(),
70+
sigAlg: x509.SHA512WithRSA,
71+
},
72+
{
73+
name: x509.SHA1WithRSA.String() + " (should fallback to SHA256)",
74+
sigAlg: x509.SHA1WithRSA,
75+
},
76+
}
77+
78+
for _, tt := range tests {
79+
t.Run(tt.name, func(t *testing.T) {
80+
cert, err := createTestCertificate(tt.sigAlg)
81+
if err != nil {
82+
t.Fatalf("Failed to create test certificate: %v", err)
83+
}
84+
85+
client, err := NewSSPIClientWithChannelBinding(cert)
86+
t.Cleanup(func() {
87+
client.Close()
88+
})
89+
90+
if err != nil {
91+
t.Errorf("Expected no error but got: %v", err)
92+
}
93+
94+
if client == nil {
95+
t.Error("Expected client but got nil")
96+
}
97+
if len(client.channelBindings) == 0 {
98+
t.Error("Expected channel bindings to be set")
99+
}
100+
101+
applicationData := client.channelBindings[32:]
102+
expectedPrefix := "tls-server-end-point:"
103+
if !strings.HasPrefix(string(applicationData), expectedPrefix) {
104+
t.Errorf("Expected application data to start with %q, got %q", expectedPrefix, string(applicationData))
105+
}
106+
})
107+
}
108+
}

0 commit comments

Comments
 (0)