|
| 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 | +} |
0 commit comments