From 7d3bd2dd556c2962ab261a45d1b2521a880ede4e Mon Sep 17 00:00:00 2001 From: Monis Khan Date: Fri, 1 Jun 2018 11:11:38 -0400 Subject: [PATCH] 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 --- pkg/oc/cli/cmd/version.go | 9 +- pkg/oc/util/tokencmd/negotiate_helpers.go | 39 ++++ pkg/oc/util/tokencmd/negotiator_gssapi.go | 10 +- .../tokencmd/negotiator_gssapi_unsupported.go | 21 +- pkg/oc/util/tokencmd/negotiator_sspi.go | 188 ++++++++++++++++++ .../tokencmd/negotiator_sspi_unsupported.go | 11 + pkg/oc/util/tokencmd/request_token.go | 3 + 7 files changed, 251 insertions(+), 30 deletions(-) create mode 100644 pkg/oc/util/tokencmd/negotiate_helpers.go create mode 100644 pkg/oc/util/tokencmd/negotiator_sspi.go create mode 100644 pkg/oc/util/tokencmd/negotiator_sspi_unsupported.go diff --git a/pkg/oc/cli/cmd/version.go b/pkg/oc/cli/cmd/version.go index 7454553328a8..98c06816fb52 100644 --- a/pkg/oc/cli/cmd/version.go +++ b/pkg/oc/cli/cmd/version.go @@ -111,8 +111,13 @@ func (o VersionOptions) RunVersion() error { } if tokencmd.GSSAPIEnabled() { features = append(features, "GSSAPI") - features = append(features, "Kerberos") // GSSAPI or SSPI - features = append(features, "SPNEGO") // GSSAPI or SSPI + } + if tokencmd.SSPIEnabled() { + features = append(features, "SSPI") + } + if tokencmd.GSSAPIEnabled() || tokencmd.SSPIEnabled() { + features = append(features, "Kerberos") + features = append(features, "SPNEGO") } fmt.Printf("features: %s\n", strings.Join(features, " ")) } diff --git a/pkg/oc/util/tokencmd/negotiate_helpers.go b/pkg/oc/util/tokencmd/negotiate_helpers.go new file mode 100644 index 000000000000..e1d061db357e --- /dev/null +++ b/pkg/oc/util/tokencmd/negotiate_helpers.go @@ -0,0 +1,39 @@ +package tokencmd + +import ( + "errors" + "net/url" +) + +func getServiceName(sep rune, requestURL string) (string, error) { + u, err := url.Parse(requestURL) + if err != nil { + return "", err + } + + return "HTTP" + string(sep) + u.Hostname(), nil +} + +type negotiateUnsupported struct { + error +} + +func newUnsupportedNegotiator(name string) Negotiator { + return &negotiateUnsupported{error: errors.New(name + " support is not enabled")} +} + +func (n *negotiateUnsupported) Load() error { + return n +} + +func (n *negotiateUnsupported) InitSecContext(requestURL string, challengeToken []byte) ([]byte, error) { + return nil, n +} + +func (*negotiateUnsupported) IsComplete() bool { + return false +} + +func (n *negotiateUnsupported) Release() error { + return n +} diff --git a/pkg/oc/util/tokencmd/negotiator_gssapi.go b/pkg/oc/util/tokencmd/negotiator_gssapi.go index 0ecca91f893c..ab4956daf5a4 100644 --- a/pkg/oc/util/tokencmd/negotiator_gssapi.go +++ b/pkg/oc/util/tokencmd/negotiator_gssapi.go @@ -4,8 +4,6 @@ package tokencmd import ( "errors" - "net" - "net/url" "runtime" "sync" "time" @@ -90,17 +88,11 @@ func (g *gssapiNegotiator) InitSecContext(requestURL string, challengeToken []by g.cred = lib.GSS_C_NO_CREDENTIAL } - u, err := url.Parse(requestURL) + serviceName, err := getServiceName('@', requestURL) if err != nil { return nil, err } - hostname := u.Host - if h, _, err := net.SplitHostPort(u.Host); err == nil { - hostname = h - } - - serviceName := "HTTP@" + hostname glog.V(5).Infof("importing service name %s", serviceName) nameBuf, err := lib.MakeBufferString(serviceName) if err != nil { diff --git a/pkg/oc/util/tokencmd/negotiator_gssapi_unsupported.go b/pkg/oc/util/tokencmd/negotiator_gssapi_unsupported.go index 7fa35ab168ce..d8c7818a6201 100644 --- a/pkg/oc/util/tokencmd/negotiator_gssapi_unsupported.go +++ b/pkg/oc/util/tokencmd/negotiator_gssapi_unsupported.go @@ -2,27 +2,10 @@ package tokencmd -import "errors" - func GSSAPIEnabled() bool { return false } -type gssapiUnsupported struct{} - -func NewGSSAPINegotiator(principalName string) Negotiater { - return &gssapiUnsupported{} -} - -func (g *gssapiUnsupported) Load() error { - return errors.New("GSSAPI support is not enabled") -} -func (g *gssapiUnsupported) InitSecContext(requestURL string, challengeToken []byte) (tokenToSend []byte, err error) { - return nil, errors.New("GSSAPI support is not enabled") -} -func (g *gssapiUnsupported) IsComplete() bool { - return false -} -func (g *gssapiUnsupported) Release() error { - return errors.New("GSSAPI support is not enabled") +func NewGSSAPINegotiator(string) Negotiator { + return newUnsupportedNegotiator("GSSAPI") } diff --git a/pkg/oc/util/tokencmd/negotiator_sspi.go b/pkg/oc/util/tokencmd/negotiator_sspi.go new file mode 100644 index 000000000000..f18eae6a4fce --- /dev/null +++ b/pkg/oc/util/tokencmd/negotiator_sspi.go @@ -0,0 +1,188 @@ +// +build windows + +package tokencmd + +import ( + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/runtime" + + "github.com/alexbrainman/sspi" + "github.com/alexbrainman/sspi/negotiate" + "github.com/golang/glog" +) + +const ( + // sane set of default flags, see sspiNegotiator.flags + // TODO make configurable? + flags = sspi.ISC_REQ_CONFIDENTIALITY | + sspi.ISC_REQ_INTEGRITY | + sspi.ISC_REQ_MUTUAL_AUTH | + sspi.ISC_REQ_REPLAY_DETECT | + sspi.ISC_REQ_SEQUENCE_DETECT + + // separator used in fully qualified user name format + domainSeparator = `\` + + // max lengths for various fields, see sspiNegotiator.principalName + maxUsername = 256 + maxPassword = 256 + maxDomain = 15 +) + +func SSPIEnabled() bool { + return true +} + +// sspiNegotiator handles negotiate flows on windows via SSPI +// It expects sspiNegotiator.InitSecContext to be called until sspiNegotiator.IsComplete returns true +type sspiNegotiator struct { + // optional DOMAIN\Username and password + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa374714(v=vs.85).aspx + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa380131(v=vs.85).aspx + // pAuthData [in]: If credentials are supplied, they are passed via a pointer to a sspi.SEC_WINNT_AUTH_IDENTITY + // structure that includes those credentials. + // When using the Negotiate package, the maximum character lengths for user name, password, and domain are + // 256, 256, and 15, respectively. + principalName string + password string + + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms721572(v=vs.85).aspx#_security_credentials_gly + // phCredential [in, optional]: A handle to the credentials returned by AcquireCredentialsHandle (Negotiate). + // This handle is used to build the security context. sspi.SECPKG_CRED_OUTBOUND is used to request OUTBOUND credentials. + cred *sspi.Credentials + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms721625(v=vs.85).aspx#_security_security_context_gly + // Manages all steps of the Negotiate negotiation. + ctx *negotiate.ClientContext + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa375509(v=vs.85).aspx + // fContextReq [in]: Bit flags that indicate requests for the context. + flags uint32 + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa375509(v=vs.85).aspx + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa374764(v=vs.85).aspx + // Set to true once InitializeSecurityContext or CompleteAuthToken return sspi.SEC_E_OK + complete bool +} + +func NewSSPINegotiator(principalName, password string) Negotiator { + return &sspiNegotiator{principalName: principalName, password: password, flags: flags} +} + +func (s *sspiNegotiator) Load() error { + glog.V(5).Info("Attempt to load SSPI") + // do nothing since SSPI uses lazy DLL loading + return nil +} + +func (s *sspiNegotiator) InitSecContext(requestURL string, challengeToken []byte) ([]byte, error) { + defer runtime.HandleCrash() + if s.cred == nil || s.ctx == nil { + glog.V(5).Infof("Start SSPI flow: %s", requestURL) + + cred, err := s.getUserCredentials() + if err != nil { + glog.V(5).Infof("getUserCredentials returned error: %v", err) + return nil, err + } + s.cred = cred + glog.V(5).Info("getUserCredentials successful") + + serviceName, err := getServiceName('/', requestURL) + if err != nil { + return nil, err + } + + glog.V(5).Infof("importing service name %s", serviceName) + ctx, outputToken, err := negotiate.NewClientContext(s.cred, serviceName) // TODO send s.flags + if err != nil { + glog.V(5).Infof("NewClientContext returned error: %v", err) + return nil, err + } + s.ctx = ctx + glog.V(5).Info("NewClientContext successful") + return outputToken, nil + } + + glog.V(5).Info("Continue SSPI flow") + + complete, outputToken, err := s.ctx.Update(challengeToken) + if err != nil { + glog.V(5).Infof("context Update returned error: %v", err) + return nil, err + } + // TODO we need a way to verify s.ctx.sctxt.EstablishedFlags matches s.ctx.sctxt.RequestedFlags (s.flags) + // we will need to update upstream to add the verification or use reflection hacks here + s.complete = complete + glog.V(5).Infof("context Update successful, complete=%v", s.complete) + return outputToken, nil +} + +func (s *sspiNegotiator) IsComplete() bool { + return s.complete +} + +func (s *sspiNegotiator) Release() error { + defer runtime.HandleCrash() + glog.V(5).Info("Attempt to release SSPI") + var errs []error + if s.ctx != nil { + if err := s.ctx.Release(); err != nil { + glog.V(5).Infof("SSPI context release failed: %v", err) + errs = append(errs, err) + } + } + if s.cred != nil { + if err := s.cred.Release(); err != nil { + glog.V(5).Infof("SSPI credential release failed: %v", err) + errs = append(errs, err) + } + } + if len(errs) == 1 { + return errs[0] + } + return errors.NewAggregate(errs) +} + +func (s *sspiNegotiator) getUserCredentials() (*sspi.Credentials, error) { + // Try to use principalName if specified + if len(s.principalName) > 0 { + domain, username, err := s.splitDomainAndUsername() + if err != nil { + return nil, err + } + glog.V(5).Infof( + "Using AcquireUserCredentials because principalName is not empty, principalName=%s, username=%s, domain=%s", + s.principalName, username, domain) + cred, err := negotiate.AcquireUserCredentials(domain, username, s.password) + if err != nil { + glog.V(5).Infof("AcquireUserCredentials failed: %v", err) + return nil, err + } + glog.V(5).Info("AcquireUserCredentials successful") + return cred, nil + } + glog.V(5).Info("Using AcquireCurrentUserCredentials because principalName is empty") + return negotiate.AcquireCurrentUserCredentials() +} + +func (s *sspiNegotiator) splitDomainAndUsername() (string, string, error) { + data := strings.Split(s.principalName, domainSeparator) + if len(data) != 2 { + return "", "", fmt.Errorf(`invalid username %s, must be in Fully Qualified User Name format (ex: DOMAIN\Username)`, + s.principalName) + } + domain := data[0] + username := data[1] + if domainLen, + usernameLen, + passwordLen := len(domain), + len(username), + len(s.password); domainLen > maxDomain || usernameLen > maxUsername || passwordLen > maxPassword { + return "", "", fmt.Errorf( + "the maximum character lengths for user name, password, and domain are 256, 256, and 15, respectively:\n"+ + "fully qualifed username=%s username=%s,len=%d domain=%s,len=%d password=,len=%d", + s.principalName, username, usernameLen, domain, domainLen, passwordLen) + } + return domain, username, nil +} diff --git a/pkg/oc/util/tokencmd/negotiator_sspi_unsupported.go b/pkg/oc/util/tokencmd/negotiator_sspi_unsupported.go new file mode 100644 index 000000000000..b77d45d61a14 --- /dev/null +++ b/pkg/oc/util/tokencmd/negotiator_sspi_unsupported.go @@ -0,0 +1,11 @@ +// +build !windows + +package tokencmd + +func SSPIEnabled() bool { + return false +} + +func NewSSPINegotiator(string, string) Negotiator { + return newUnsupportedNegotiator("SSPI") +} diff --git a/pkg/oc/util/tokencmd/request_token.go b/pkg/oc/util/tokencmd/request_token.go index 587eb4b2b9e7..090e01210070 100644 --- a/pkg/oc/util/tokencmd/request_token.go +++ b/pkg/oc/util/tokencmd/request_token.go @@ -79,6 +79,9 @@ func NewRequestTokenOptions(clientCfg *restclient.Config, reader io.Reader, defa if GSSAPIEnabled() { handlers = append(handlers, NewNegotiateChallengeHandler(NewGSSAPINegotiator(defaultUsername))) } + if SSPIEnabled() { + handlers = append(handlers, NewNegotiateChallengeHandler(NewSSPINegotiator(defaultUsername, defaultPassword))) + } if BasicEnabled() { handlers = append(handlers, &BasicChallengeHandler{Host: clientCfg.Host, Reader: reader, Username: defaultUsername, Password: defaultPassword}) }