Skip to content

Commit

Permalink
new SSPI code
Browse files Browse the repository at this point in the history
Signed-off-by: Monis Khan <[email protected]>
  • Loading branch information
enj committed Jun 14, 2018
1 parent 1b69a9e commit 77f53f9
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 84 deletions.
284 changes: 202 additions & 82 deletions pkg/oc/util/tokencmd/negotiator_sspi.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,51 @@ package tokencmd

import (
"fmt"
"io"
"os"
"strings"
"syscall"

"k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/runtime"

"github.com/openshift/origin/pkg/cmd/util/term"

"github.com/alexbrainman/sspi"
"github.com/alexbrainman/sspi/negotiate"
"github.com/golang/glog"
)

const (
// sane set of default flags, see sspiNegotiator.flags
// sane set of default flags, see sspiNegotiator.desiredFlags
// TODO make configurable?
flags = sspi.ISC_REQ_CONFIDENTIALITY |
desiredFlags = sspi.ISC_REQ_CONFIDENTIALITY |
sspi.ISC_REQ_INTEGRITY |
sspi.ISC_REQ_MUTUAL_AUTH |
sspi.ISC_REQ_REPLAY_DETECT |
sspi.ISC_REQ_SEQUENCE_DETECT
// subset of desiredFlags that must be set, see sspiNegotiator.requiredFlags
// TODO make configurable?
requiredFlags = sspi.ISC_REQ_CONFIDENTIALITY |
sspi.ISC_REQ_INTEGRITY |
sspi.ISC_REQ_MUTUAL_AUTH

// separator used in fully qualified user name format
// various windows user name formats
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa380525(v=vs.85).aspx
// https://msdn.microsoft.com/en-us/library/ms724268(VS.85).aspx
// separator used in fully qualified user name or down-level logon name format (DOMAIN\Username)
domainSeparator = `\`
// https://msdn.microsoft.com/en-us/library/ms677605(v=vs.85).aspx#userPrincipalName
// separator used in user principal name (UPN) format ([email protected])
upnSeparator = "@"
// https://msdn.microsoft.com/en-us/library/system.environment.userdomainname(v=vs.110).aspx
// environment variable that holds the network domain name associated with the current user
// this is the NetBIOS domain name which should fit within the length requirement (see maxDomain)
shortDomainEnvVar = "USERDOMAIN"

// max lengths for various fields, see sspiNegotiator.principalName
// max lengths for various fields, see sspiNegotiator.getDomainAndUsername and sspiNegotiator.getPassword
// When using the Negotiate package, the maximum character lengths for user name, password, and domain are
// 256, 256, and 15, respectively.
maxUsername = 256
maxPassword = 256
maxDomain = 15
Expand All @@ -39,15 +61,22 @@ func SSPIEnabled() bool {
// 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
// principalName is an optional username (in fully qualified, user principal name or short format).
// 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
// password is an optional password used to log into a specific account if principalName is non-empty.
// This allows logging in via username and password even when basic auth is not enabled.
password string

// reader is used to prompt for a password if principalName is non-empty and password is empty.
reader io.Reader
// writer is used to output prompts when prompting for password.
writer io.Writer
// host is the server being authenticated to. Used only for displaying messages when prompting for password.
host 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).
Expand All @@ -58,15 +87,25 @@ type sspiNegotiator struct {
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
desiredFlags uint32
// requiredFlags is the subset of desiredFlags that must be set for flag verification to succeed
requiredFlags 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 NewSSPINegotiator(principalName, password, host string, reader io.Reader) Negotiator {
return &sspiNegotiator{
principalName: principalName,
password: password,
reader: reader,
writer: os.Stdout,
host: host,
desiredFlags: desiredFlags,
requiredFlags: requiredFlags,
}
}

func (s *sspiNegotiator) Load() error {
Expand All @@ -75,47 +114,16 @@ func (s *sspiNegotiator) Load() error {
return nil
}

func (s *sspiNegotiator) InitSecContext(requestURL string, challengeToken []byte) ([]byte, error) {
func (s *sspiNegotiator) InitSecContext(requestURL string, challengeToken []byte) (tokenToSend []byte, err 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
if needsInit := s.cred == nil || s.ctx == nil; needsInit {
logSSPI("Start SSPI flow: %s", requestURL)
return s.initContext(requestURL)
}

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
return s.updateContext(challengeToken)
}

func (s *sspiNegotiator) IsComplete() bool {
Expand All @@ -126,63 +134,175 @@ 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 err := s.ctx.Release(); err != nil {
logSSPI("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 err := s.cred.Release(); err != nil {
logSSPI("SSPI credential release failed: %v", err)
errs = append(errs, err)
}
if len(errs) == 1 {
return errs[0]
return errors.Reduce(errors.NewAggregate(errs))
}

func (s *sspiNegotiator) initContext(requestURL string) (outputToken []byte, err error) {
cred, err := s.getUserCredentials()
if err != nil {
logSSPI("getUserCredentials failed: %v", err)
return nil, err
}
return errors.NewAggregate(errs)
s.cred = cred
glog.V(5).Info("getUserCredentials successful")

serviceName, err := getServiceName('/', requestURL)
if err != nil {
return nil, err
}

logSSPI("importing service name %s", serviceName)
ctx, outputToken, err := negotiate.NewClientContextWithFlags(s.cred, serviceName, s.desiredFlags)
if err != nil {
logSSPI("NewClientContextWithFlags failed: %v", err)
return nil, err
}
s.ctx = ctx
glog.V(5).Info("NewClientContextWithFlags successful")
return outputToken, nil
}

func (s *sspiNegotiator) getUserCredentials() (*sspi.Credentials, error) {
if len(s.principalName) == 0 && len(s.password) > 0 {
return nil, fmt.Errorf("username cannot be empty with non-empty password")
}

// Try to use principalName if specified
if len(s.principalName) > 0 {
domain, username, err := s.splitDomainAndUsername()
domain, username, err := s.getDomainAndUsername()
if err != nil {
return nil, err
}
password, err := s.getPassword(domain, username)
if err != nil {
return nil, err
}
glog.V(5).Infof(
"Using AcquireUserCredentials because principalName is not empty, principalName=%s, username=%s, domain=%s",

logSSPI("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)
// this call seems to never fail, even when domain / username / password are nonsense
cred, err := negotiate.AcquireUserCredentials(domain, username, password)
if err != nil {
glog.V(5).Infof("AcquireUserCredentials failed: %v", err)
logSSPI("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=<redacted>,len=%d",
s.principalName, username, usernameLen, domain, domainLen, passwordLen)
func (s *sspiNegotiator) getDomainAndUsername() (domain, username string, err error) {
switch {
case strings.Contains(s.principalName, domainSeparator):
data := strings.Split(s.principalName, domainSeparator)
// try to provide useful error messages
if len(data) != 2 || len(data[1]) == 0 {
return "", "", fmt.Errorf(
`invalid username %s, fully qualified user name format must have single backslash and non-empty user (ex: DOMAIN\Username)`,
s.principalName)
}
domain = data[0]
username = data[1]

case strings.Contains(s.principalName, upnSeparator):
// leave domain empty and assume it is qualified in the username (UPN format)
username = s.principalName

default:
// this is a short name meaning we will need to lookup the current user's domain
// TODO should we use syscall.NetGetJoinInformation first and then fallback to the env var?
domain, _ = os.LookupEnv(shortDomainEnvVar)
username = s.principalName
}

// try to provide useful error messages
if domainLen, usernameLen := len(domain), len(username); domainLen > maxDomain || usernameLen > maxUsername {
return "", "",
fmt.Errorf("the maximum character lengths for user name and domain are %d and %d, respectively:\n"+
"input username=%s username=%s,len=%d domain=%s,len=%d",
maxUsername, maxDomain, s.principalName, username, usernameLen, domain, domainLen)
}

return domain, username, nil
}

func (s *sspiNegotiator) getPassword(domain, username string) (string, error) {
password := s.password

if missingPassword := len(password) == 0; missingPassword {
// mimic output from basic auth prompt
if hasDomain := len(domain) > 0; hasDomain {
fmt.Fprintf(s.writer, "Authentication required for %s (%s)\n", s.host, domain)
} else {
fmt.Fprintf(s.writer, "Authentication required for %s\n", s.host)
}
fmt.Fprintf(s.writer, "Username: %s\n", username)
// empty password from prompt is ok
// we do not need to worry about being stuck in a prompt loop because ClientContext.Update
// will fail if the password is incorrect and that will end the challenge flow
password = term.PromptForPasswordString(s.reader, s.writer, "Password: ")
}

// try to provide useful error messages
if passwordLen := len(password); passwordLen > maxPassword {
return "", fmt.Errorf("the maximum character length for password is %d: password=<redacted>,len=%d",
maxPassword, passwordLen)
}

return password, nil
}

func (s *sspiNegotiator) updateContext(challengeToken []byte) (outputToken []byte, err error) {
// ClientContext.Update does not return errors for success codes:
// 1. sspi.SEC_E_OK (complete=true and err=nil)
// 2. sspi.SEC_I_CONTINUE_NEEDED (complete=false and err=nil)
// 3. sspi.SEC_I_COMPLETE_AND_CONTINUE and sspi.SEC_I_COMPLETE_NEEDED
// complete=false and err=nil as long as sspi.CompleteAuthToken returns sspi.SEC_E_OK
// Thus we can safely assume that any error returned here is an error code
authCompleted, outputToken, err := s.ctx.Update(challengeToken)
if err != nil {
logSSPI("ClientContext.Update failed: %v", err)
return nil, err
}
s.complete = authCompleted
logSSPI("ClientContext.Update successful, complete=%v", s.complete)

// TODO should we skip the flag check if complete = true?
if nonFatalErr := s.ctx.VerifyFlags(); nonFatalErr == nil {
glog.V(5).Info("ClientContext.VerifyFlags successful")
} else {
logSSPI("ClientContext.VerifyFlags failed: %v", nonFatalErr)
if fatalErr := s.ctx.VerifySelectiveFlags(s.requiredFlags); fatalErr != nil {
logSSPI("ClientContext.VerifySelectiveFlags failed: %v", fatalErr)
return nil, fatalErr
}
glog.V(5).Info("ClientContext.VerifySelectiveFlags successful")
}

return outputToken, nil
}

// logSSPI is the equivalent of glog.V(5).Infof(format, args) except it
// includes error code information for any syscall.Errno contained in args
func logSSPI(format string, args ...interface{}) {
if glog.V(5) {
for i, arg := range args {
if errno, ok := arg.(syscall.Errno); ok {
args[i] = fmt.Sprintf("%v, code=%#v", errno, errno)
}
}
s := fmt.Sprintf(format, args...)
glog.InfoDepth(1, s)
}
}
4 changes: 3 additions & 1 deletion pkg/oc/util/tokencmd/negotiator_sspi_unsupported.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

package tokencmd

import "io"

func SSPIEnabled() bool {
return false
}

func NewSSPINegotiator(string, string) Negotiator {
func NewSSPINegotiator(string, string, string, io.Reader) Negotiator {
return newUnsupportedNegotiator("SSPI")
}
2 changes: 1 addition & 1 deletion pkg/oc/util/tokencmd/request_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func NewRequestTokenOptions(clientCfg *restclient.Config, reader io.Reader, defa
handlers = append(handlers, NewNegotiateChallengeHandler(NewGSSAPINegotiator(defaultUsername)))
}
if SSPIEnabled() {
handlers = append(handlers, NewNegotiateChallengeHandler(NewSSPINegotiator(defaultUsername, defaultPassword)))
handlers = append(handlers, NewNegotiateChallengeHandler(NewSSPINegotiator(defaultUsername, defaultPassword, clientCfg.Host, reader)))
}
if BasicEnabled() {
handlers = append(handlers, &BasicChallengeHandler{Host: clientCfg.Host, Reader: reader, Username: defaultUsername, Password: defaultPassword})
Expand Down

0 comments on commit 77f53f9

Please sign in to comment.