/
sspi.go
191 lines (164 loc) · 6.34 KB
/
sspi.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
//go:build windows
// +build windows
package gssapi
import (
"bytes"
"encoding/binary"
"fmt"
"github.com/alexbrainman/sspi"
"github.com/alexbrainman/sspi/kerberos"
)
// SSPIClient implements ldap.GSSAPIClient interface.
// Depends on secur32.dll.
type SSPIClient struct {
creds *sspi.Credentials
ctx *kerberos.ClientContext
}
// NewSSPIClient returns a client with credentials of the current user.
func NewSSPIClient() (*SSPIClient, error) {
creds, err := kerberos.AcquireCurrentUserCredentials()
if err != nil {
return nil, err
}
return NewSSPIClientWithCredentials(creds), nil
}
// NewSSPIClientWithCredentials returns a client with the provided credentials.
func NewSSPIClientWithCredentials(creds *sspi.Credentials) *SSPIClient {
return &SSPIClient{
creds: creds,
}
}
// NewSSPIClientWithUserCredentials returns a client using the provided user's
// credentials.
func NewSSPIClientWithUserCredentials(domain, username, password string) (*SSPIClient, error) {
creds, err := kerberos.AcquireUserCredentials(domain, username, password)
if err != nil {
return nil, err
}
return &SSPIClient{
creds: creds,
}, nil
}
// Close deletes any established secure context and closes the client.
func (c *SSPIClient) Close() error {
err1 := c.DeleteSecContext()
err2 := c.creds.Release()
if err1 != nil {
return err1
}
if err2 != nil {
return err2
}
return nil
}
// DeleteSecContext destroys any established secure context.
func (c *SSPIClient) DeleteSecContext() error {
return c.ctx.Release()
}
// InitSecContext initiates the establishment of a security context for
// GSS-API between the client and server.
// See RFC 4752 section 3.1.
func (c *SSPIClient) InitSecContext(target string, token []byte) ([]byte, bool, error) {
sspiFlags := uint32(sspi.ISC_REQ_INTEGRITY | sspi.ISC_REQ_CONFIDENTIALITY | sspi.ISC_REQ_MUTUAL_AUTH)
switch token {
case nil:
ctx, completed, output, err := kerberos.NewClientContextWithFlags(c.creds, target, sspiFlags)
if err != nil {
return nil, false, err
}
c.ctx = ctx
return output, !completed, nil
default:
completed, output, err := c.ctx.Update(token)
if err != nil {
return nil, false, err
}
if err := c.ctx.VerifyFlags(); err != nil {
return nil, false, fmt.Errorf("error verifying flags: %v", err)
}
return output, !completed, nil
}
}
// NegotiateSaslAuth performs the last step of the SASL handshake.
// See RFC 4752 section 3.1.
func (c *SSPIClient) NegotiateSaslAuth(token []byte, authzid string) ([]byte, error) {
// Using SSPI rather than of GSSAPI, relevant documentation of differences here:
// https://learn.microsoft.com/en-us/windows/win32/secauthn/sspi-kerberos-interoperability-with-gssapi
// KERB_WRAP_NO_ENCRYPT (SECQOP_WRAP_NO_ENCRYPT) flag indicates Wrap and Unwrap
// should only sign & verify (not encrypt & decrypt).
const KERB_WRAP_NO_ENCRYPT = 0x80000001
// https://learn.microsoft.com/en-us/windows/win32/api/sspi/nf-sspi-decryptmessage
flags, inputPayload, err := c.ctx.DecryptMessage(token, 0)
if err != nil {
return nil, fmt.Errorf("error decrypting message: %w", err)
}
if flags&KERB_WRAP_NO_ENCRYPT == 0 {
// Encrypted message, this is unexpected.
return nil, fmt.Errorf("message encrypted")
}
// `bytes` describes available security context:
// "the first octet of resulting cleartext as a
// bit-mask specifying the security layers supported by the server and
// the second through fourth octets as the maximum size output_message
// the server is able to receive (in network byte order). If the
// resulting cleartext is not 4 octets long, the client fails the
// negotiation. The client verifies that the server maximum buffer is 0
// if the server does not advertise support for any security layer."
// From https://www.rfc-editor.org/rfc/rfc4752#section-3.1
if len(inputPayload) != 4 {
return nil, fmt.Errorf("bad server token")
}
if inputPayload[0] == 0x0 && !bytes.Equal(inputPayload, []byte{0x0, 0x0, 0x0, 0x0}) {
return nil, fmt.Errorf("bad server token")
}
// Security layers https://www.rfc-editor.org/rfc/rfc4422#section-3.7
// https://www.rfc-editor.org/rfc/rfc4752#section-3.3
// supportNoSecurity := input[0] & 0b00000001
// supportIntegrity := input[0] & 0b00000010
// supportPrivacy := input[0] & 0b00000100
selectedSec := 0 // Disabled
var maxSecMsgSize uint32
if selectedSec != 0 {
maxSecMsgSize, _, _, _, err = c.ctx.Sizes()
if err != nil {
return nil, fmt.Errorf("error getting security context max message size: %w", err)
}
}
// https://learn.microsoft.com/en-us/windows/win32/api/sspi/nf-sspi-encryptmessage
inputPayload, err = c.ctx.EncryptMessage(handshakePayload(byte(selectedSec), maxSecMsgSize, []byte(authzid)), KERB_WRAP_NO_ENCRYPT, 0)
if err != nil {
return nil, fmt.Errorf("error encrypting message: %w", err)
}
return inputPayload, nil
}
func handshakePayload(secLayer byte, maxSize uint32, authzid []byte) []byte {
// construct payload and send unencrypted:
// "The client then constructs data, with the first octet containing the
// bit-mask specifying the selected security layer, the second through
// fourth octets containing in network byte order the maximum size
// output_message the client is able to receive (which MUST be 0 if the
// client does not support any security layer), and the remaining octets
// containing the UTF-8 [UTF8] encoded authorization identity.
// (Implementation note: The authorization identity is not terminated
// with the zero-valued (%x00) octet (e.g., the UTF-8 encoding of the
// NUL (U+0000) character)). The client passes the data to GSS_Wrap
// with conf_flag set to FALSE and responds with the generated
// output_message. The client can then consider the server
// authenticated."
// From https://www.rfc-editor.org/rfc/rfc4752#section-3.1
// Client picks security layer to use, 0 is disabled.
var selectedSecurity byte = secLayer
var truncatedSize uint32 // must be 0 if secLayer is 0
if selectedSecurity != 0 {
// Only 3 bytes to describe the max size, set the maximum.
truncatedSize = 0b00000000_11111111_11111111_11111111
if truncatedSize > maxSize {
truncatedSize = maxSize
}
}
payload := make([]byte, 4, 4+len(authzid))
binary.BigEndian.PutUint32(payload, truncatedSize)
payload[0] = selectedSecurity // Overwrites most significant byte of `maxSize`
payload = append(payload, []byte(authzid)...)
return payload
}