Skip to content

Commit 7d3bd2d

Browse files
committed
Implement SSPI Support on Windows (oc Kerberos)
This change is highly experimental and includes no tests (because you need an automated extended test with a fully configured Windows Active Directory server to actually test this). Signed-off-by: Monis Khan <[email protected]>
1 parent bfb500a commit 7d3bd2d

7 files changed

+251
-30
lines changed

pkg/oc/cli/cmd/version.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,13 @@ func (o VersionOptions) RunVersion() error {
111111
}
112112
if tokencmd.GSSAPIEnabled() {
113113
features = append(features, "GSSAPI")
114-
features = append(features, "Kerberos") // GSSAPI or SSPI
115-
features = append(features, "SPNEGO") // GSSAPI or SSPI
114+
}
115+
if tokencmd.SSPIEnabled() {
116+
features = append(features, "SSPI")
117+
}
118+
if tokencmd.GSSAPIEnabled() || tokencmd.SSPIEnabled() {
119+
features = append(features, "Kerberos")
120+
features = append(features, "SPNEGO")
116121
}
117122
fmt.Printf("features: %s\n", strings.Join(features, " "))
118123
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package tokencmd
2+
3+
import (
4+
"errors"
5+
"net/url"
6+
)
7+
8+
func getServiceName(sep rune, requestURL string) (string, error) {
9+
u, err := url.Parse(requestURL)
10+
if err != nil {
11+
return "", err
12+
}
13+
14+
return "HTTP" + string(sep) + u.Hostname(), nil
15+
}
16+
17+
type negotiateUnsupported struct {
18+
error
19+
}
20+
21+
func newUnsupportedNegotiator(name string) Negotiator {
22+
return &negotiateUnsupported{error: errors.New(name + " support is not enabled")}
23+
}
24+
25+
func (n *negotiateUnsupported) Load() error {
26+
return n
27+
}
28+
29+
func (n *negotiateUnsupported) InitSecContext(requestURL string, challengeToken []byte) ([]byte, error) {
30+
return nil, n
31+
}
32+
33+
func (*negotiateUnsupported) IsComplete() bool {
34+
return false
35+
}
36+
37+
func (n *negotiateUnsupported) Release() error {
38+
return n
39+
}

pkg/oc/util/tokencmd/negotiator_gssapi.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ package tokencmd
44

55
import (
66
"errors"
7-
"net"
8-
"net/url"
97
"runtime"
108
"sync"
119
"time"
@@ -90,17 +88,11 @@ func (g *gssapiNegotiator) InitSecContext(requestURL string, challengeToken []by
9088
g.cred = lib.GSS_C_NO_CREDENTIAL
9189
}
9290

93-
u, err := url.Parse(requestURL)
91+
serviceName, err := getServiceName('@', requestURL)
9492
if err != nil {
9593
return nil, err
9694
}
9795

98-
hostname := u.Host
99-
if h, _, err := net.SplitHostPort(u.Host); err == nil {
100-
hostname = h
101-
}
102-
103-
serviceName := "HTTP@" + hostname
10496
glog.V(5).Infof("importing service name %s", serviceName)
10597
nameBuf, err := lib.MakeBufferString(serviceName)
10698
if err != nil {

pkg/oc/util/tokencmd/negotiator_gssapi_unsupported.go

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,10 @@
22

33
package tokencmd
44

5-
import "errors"
6-
75
func GSSAPIEnabled() bool {
86
return false
97
}
108

11-
type gssapiUnsupported struct{}
12-
13-
func NewGSSAPINegotiator(principalName string) Negotiater {
14-
return &gssapiUnsupported{}
15-
}
16-
17-
func (g *gssapiUnsupported) Load() error {
18-
return errors.New("GSSAPI support is not enabled")
19-
}
20-
func (g *gssapiUnsupported) InitSecContext(requestURL string, challengeToken []byte) (tokenToSend []byte, err error) {
21-
return nil, errors.New("GSSAPI support is not enabled")
22-
}
23-
func (g *gssapiUnsupported) IsComplete() bool {
24-
return false
25-
}
26-
func (g *gssapiUnsupported) Release() error {
27-
return errors.New("GSSAPI support is not enabled")
9+
func NewGSSAPINegotiator(string) Negotiator {
10+
return newUnsupportedNegotiator("GSSAPI")
2811
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// +build windows
2+
3+
package tokencmd
4+
5+
import (
6+
"fmt"
7+
"strings"
8+
9+
"k8s.io/apimachinery/pkg/util/errors"
10+
"k8s.io/apimachinery/pkg/util/runtime"
11+
12+
"github.com/alexbrainman/sspi"
13+
"github.com/alexbrainman/sspi/negotiate"
14+
"github.com/golang/glog"
15+
)
16+
17+
const (
18+
// sane set of default flags, see sspiNegotiator.flags
19+
// TODO make configurable?
20+
flags = sspi.ISC_REQ_CONFIDENTIALITY |
21+
sspi.ISC_REQ_INTEGRITY |
22+
sspi.ISC_REQ_MUTUAL_AUTH |
23+
sspi.ISC_REQ_REPLAY_DETECT |
24+
sspi.ISC_REQ_SEQUENCE_DETECT
25+
26+
// separator used in fully qualified user name format
27+
domainSeparator = `\`
28+
29+
// max lengths for various fields, see sspiNegotiator.principalName
30+
maxUsername = 256
31+
maxPassword = 256
32+
maxDomain = 15
33+
)
34+
35+
func SSPIEnabled() bool {
36+
return true
37+
}
38+
39+
// sspiNegotiator handles negotiate flows on windows via SSPI
40+
// It expects sspiNegotiator.InitSecContext to be called until sspiNegotiator.IsComplete returns true
41+
type sspiNegotiator struct {
42+
// optional DOMAIN\Username and password
43+
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa374714(v=vs.85).aspx
44+
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa380131(v=vs.85).aspx
45+
// pAuthData [in]: If credentials are supplied, they are passed via a pointer to a sspi.SEC_WINNT_AUTH_IDENTITY
46+
// structure that includes those credentials.
47+
// When using the Negotiate package, the maximum character lengths for user name, password, and domain are
48+
// 256, 256, and 15, respectively.
49+
principalName string
50+
password string
51+
52+
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms721572(v=vs.85).aspx#_security_credentials_gly
53+
// phCredential [in, optional]: A handle to the credentials returned by AcquireCredentialsHandle (Negotiate).
54+
// This handle is used to build the security context. sspi.SECPKG_CRED_OUTBOUND is used to request OUTBOUND credentials.
55+
cred *sspi.Credentials
56+
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms721625(v=vs.85).aspx#_security_security_context_gly
57+
// Manages all steps of the Negotiate negotiation.
58+
ctx *negotiate.ClientContext
59+
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa375509(v=vs.85).aspx
60+
// fContextReq [in]: Bit flags that indicate requests for the context.
61+
flags uint32
62+
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa375509(v=vs.85).aspx
63+
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa374764(v=vs.85).aspx
64+
// Set to true once InitializeSecurityContext or CompleteAuthToken return sspi.SEC_E_OK
65+
complete bool
66+
}
67+
68+
func NewSSPINegotiator(principalName, password string) Negotiator {
69+
return &sspiNegotiator{principalName: principalName, password: password, flags: flags}
70+
}
71+
72+
func (s *sspiNegotiator) Load() error {
73+
glog.V(5).Info("Attempt to load SSPI")
74+
// do nothing since SSPI uses lazy DLL loading
75+
return nil
76+
}
77+
78+
func (s *sspiNegotiator) InitSecContext(requestURL string, challengeToken []byte) ([]byte, error) {
79+
defer runtime.HandleCrash()
80+
if s.cred == nil || s.ctx == nil {
81+
glog.V(5).Infof("Start SSPI flow: %s", requestURL)
82+
83+
cred, err := s.getUserCredentials()
84+
if err != nil {
85+
glog.V(5).Infof("getUserCredentials returned error: %v", err)
86+
return nil, err
87+
}
88+
s.cred = cred
89+
glog.V(5).Info("getUserCredentials successful")
90+
91+
serviceName, err := getServiceName('/', requestURL)
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
glog.V(5).Infof("importing service name %s", serviceName)
97+
ctx, outputToken, err := negotiate.NewClientContext(s.cred, serviceName) // TODO send s.flags
98+
if err != nil {
99+
glog.V(5).Infof("NewClientContext returned error: %v", err)
100+
return nil, err
101+
}
102+
s.ctx = ctx
103+
glog.V(5).Info("NewClientContext successful")
104+
return outputToken, nil
105+
}
106+
107+
glog.V(5).Info("Continue SSPI flow")
108+
109+
complete, outputToken, err := s.ctx.Update(challengeToken)
110+
if err != nil {
111+
glog.V(5).Infof("context Update returned error: %v", err)
112+
return nil, err
113+
}
114+
// TODO we need a way to verify s.ctx.sctxt.EstablishedFlags matches s.ctx.sctxt.RequestedFlags (s.flags)
115+
// we will need to update upstream to add the verification or use reflection hacks here
116+
s.complete = complete
117+
glog.V(5).Infof("context Update successful, complete=%v", s.complete)
118+
return outputToken, nil
119+
}
120+
121+
func (s *sspiNegotiator) IsComplete() bool {
122+
return s.complete
123+
}
124+
125+
func (s *sspiNegotiator) Release() error {
126+
defer runtime.HandleCrash()
127+
glog.V(5).Info("Attempt to release SSPI")
128+
var errs []error
129+
if s.ctx != nil {
130+
if err := s.ctx.Release(); err != nil {
131+
glog.V(5).Infof("SSPI context release failed: %v", err)
132+
errs = append(errs, err)
133+
}
134+
}
135+
if s.cred != nil {
136+
if err := s.cred.Release(); err != nil {
137+
glog.V(5).Infof("SSPI credential release failed: %v", err)
138+
errs = append(errs, err)
139+
}
140+
}
141+
if len(errs) == 1 {
142+
return errs[0]
143+
}
144+
return errors.NewAggregate(errs)
145+
}
146+
147+
func (s *sspiNegotiator) getUserCredentials() (*sspi.Credentials, error) {
148+
// Try to use principalName if specified
149+
if len(s.principalName) > 0 {
150+
domain, username, err := s.splitDomainAndUsername()
151+
if err != nil {
152+
return nil, err
153+
}
154+
glog.V(5).Infof(
155+
"Using AcquireUserCredentials because principalName is not empty, principalName=%s, username=%s, domain=%s",
156+
s.principalName, username, domain)
157+
cred, err := negotiate.AcquireUserCredentials(domain, username, s.password)
158+
if err != nil {
159+
glog.V(5).Infof("AcquireUserCredentials failed: %v", err)
160+
return nil, err
161+
}
162+
glog.V(5).Info("AcquireUserCredentials successful")
163+
return cred, nil
164+
}
165+
glog.V(5).Info("Using AcquireCurrentUserCredentials because principalName is empty")
166+
return negotiate.AcquireCurrentUserCredentials()
167+
}
168+
169+
func (s *sspiNegotiator) splitDomainAndUsername() (string, string, error) {
170+
data := strings.Split(s.principalName, domainSeparator)
171+
if len(data) != 2 {
172+
return "", "", fmt.Errorf(`invalid username %s, must be in Fully Qualified User Name format (ex: DOMAIN\Username)`,
173+
s.principalName)
174+
}
175+
domain := data[0]
176+
username := data[1]
177+
if domainLen,
178+
usernameLen,
179+
passwordLen := len(domain),
180+
len(username),
181+
len(s.password); domainLen > maxDomain || usernameLen > maxUsername || passwordLen > maxPassword {
182+
return "", "", fmt.Errorf(
183+
"the maximum character lengths for user name, password, and domain are 256, 256, and 15, respectively:\n"+
184+
"fully qualifed username=%s username=%s,len=%d domain=%s,len=%d password=<redacted>,len=%d",
185+
s.principalName, username, usernameLen, domain, domainLen, passwordLen)
186+
}
187+
return domain, username, nil
188+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// +build !windows
2+
3+
package tokencmd
4+
5+
func SSPIEnabled() bool {
6+
return false
7+
}
8+
9+
func NewSSPINegotiator(string, string) Negotiator {
10+
return newUnsupportedNegotiator("SSPI")
11+
}

pkg/oc/util/tokencmd/request_token.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ func NewRequestTokenOptions(clientCfg *restclient.Config, reader io.Reader, defa
7979
if GSSAPIEnabled() {
8080
handlers = append(handlers, NewNegotiateChallengeHandler(NewGSSAPINegotiator(defaultUsername)))
8181
}
82+
if SSPIEnabled() {
83+
handlers = append(handlers, NewNegotiateChallengeHandler(NewSSPINegotiator(defaultUsername, defaultPassword)))
84+
}
8285
if BasicEnabled() {
8386
handlers = append(handlers, &BasicChallengeHandler{Host: clientCfg.Host, Reader: reader, Username: defaultUsername, Password: defaultPassword})
8487
}

0 commit comments

Comments
 (0)