def verify(self, data, sig): """Verifies data and its signature. If verification fails, an sspi.error will be raised. """ sigbuf = win32security.PySecBufferDescType() sigbuf.append( win32security.PySecBufferType(len(data), sspicon.SECBUFFER_DATA)) sigbuf.append( win32security.PySecBufferType(len(sig), sspicon.SECBUFFER_TOKEN)) sigbuf[0].Buffer = data sigbuf[1].Buffer = sig self.ctxt.VerifySignature(sigbuf, self._get_next_seq_num())
def encrypt(self, data): """Encrypt a string, returning a tuple of (encrypted_data, encryption_data). These can be passed to decrypt to get back the original string. """ pkg_size_info=self.ctxt.QueryContextAttributes(sspicon.SECPKG_ATTR_SIZES) trailersize=pkg_size_info['SecurityTrailer'] encbuf=win32security.PySecBufferDescType() encbuf.append(win32security.PySecBufferType(len(data), sspicon.SECBUFFER_DATA)) encbuf.append(win32security.PySecBufferType(trailersize, sspicon.SECBUFFER_TOKEN)) encbuf[0].Buffer=data self.ctxt.EncryptMessage(0,encbuf,self._get_next_seq_num()) return encbuf[0].Buffer, encbuf[1].Buffer
def authorize(self, sec_buffer_in): """Perform *one* step of the server authentication process.""" if sec_buffer_in is not None and type( sec_buffer_in) != win32security.PySecBufferDescType: # User passed us the raw data - wrap it into a SecBufferDesc sec_buffer_new = win32security.PySecBufferDescType() tokenbuf = win32security.PySecBufferType(self.pkg_info['MaxToken'], sspicon.SECBUFFER_TOKEN) tokenbuf.Buffer = sec_buffer_in sec_buffer_new.append(tokenbuf) sec_buffer_in = sec_buffer_new sec_buffer_out = win32security.PySecBufferDescType() tokenbuf = win32security.PySecBufferType(self.pkg_info['MaxToken'], sspicon.SECBUFFER_TOKEN) sec_buffer_out.append(tokenbuf) ## input context handle is None initially, then handle returned from last call thereafter ctxtin = self.ctxt if self.ctxt is None: self.ctxt = win32security.PyCtxtHandleType() err, attr, exp = win32security.AcceptSecurityContext( self.credentials, ctxtin, sec_buffer_in, self.scflags, self.datarep, self.ctxt, sec_buffer_out) # Stash these away incase someone needs to know the state from the # final call. self.ctxt_attr = attr self.ctxt_expiry = exp if err in (sspicon.SEC_I_COMPLETE_NEEDED, sspicon.SEC_I_COMPLETE_AND_CONTINUE): self.ctxt.CompleteAuthToken(sec_buffer_out) self.authenticated = err == 0 if self.authenticated: self._amend_ctx_name() return err, sec_buffer_out
def sign(self, data): """sign a string suitable for transmission, returning the signature. Passing the data and signature to verify will determine if the data is unchanged. """ pkg_size_info=self.ctxt.QueryContextAttributes(sspicon.SECPKG_ATTR_SIZES) sigsize=pkg_size_info['MaxSignature'] sigbuf=win32security.PySecBufferDescType() sigbuf.append(win32security.PySecBufferType(len(data), sspicon.SECBUFFER_DATA)) sigbuf.append(win32security.PySecBufferType(sigsize, sspicon.SECBUFFER_TOKEN)) sigbuf[0].Buffer=data self.ctxt.MakeSignature(0,sigbuf,self._get_next_seq_num()) return sigbuf[1].Buffer
def testSecBufferRepr(self): desc = win32security.PySecBufferDescType() assert re.match('PySecBufferDesc\(ulVersion: 0 \| cBuffers: 0 \| pBuffers: 0x[\da-fA-F]{8,16}\)', repr(desc)) buffer1 = win32security.PySecBufferType(0, sspicon.SECBUFFER_TOKEN) assert re.match('PySecBuffer\(cbBuffer: 0 \| BufferType: 2 \| pvBuffer: 0x[\da-fA-F]{8,16}\)', repr(buffer1)) 'PySecBuffer(cbBuffer: 0 | BufferType: 2 | pvBuffer: 0x000001B8CC6D8020)' desc.append(buffer1) assert re.match('PySecBufferDesc\(ulVersion: 0 \| cBuffers: 1 \| pBuffers: 0x[\da-fA-F]{8,16}\)', repr(desc)) buffer2 = win32security.PySecBufferType(4, sspicon.SECBUFFER_DATA) assert re.match('PySecBuffer\(cbBuffer: 4 \| BufferType: 1 \| pvBuffer: 0x[\da-fA-F]{8,16}\)', repr(buffer2)) desc.append(buffer2) assert re.match('PySecBufferDesc\(ulVersion: 0 \| cBuffers: 2 \| pBuffers: 0x[\da-fA-F]{8,16}\)', repr(desc))
def _step(self, token): success_codes = [ sspicon.SEC_E_OK, sspicon.SEC_I_COMPLETE_AND_CONTINUE, sspicon.SEC_I_COMPLETE_NEEDED, sspicon.SEC_I_CONTINUE_NEEDED ] sec_tokens = [] if token is not None: sec_token = win32security.PySecBufferType( self._context.pkg_info['MaxToken'], sspicon.SECBUFFER_TOKEN ) sec_token.Buffer = token sec_tokens.append(sec_token) if self.cbt_app_data is not None: sec_token = win32security.PySecBufferType( len(self.cbt_app_data), sspicon.SECBUFFER_CHANNEL_BINDINGS ) sec_token.Buffer = self.cbt_app_data sec_tokens.append(sec_token) if len(sec_tokens) > 0: sec_buffer = win32security.PySecBufferDescType() for sec_token in sec_tokens: sec_buffer.append(sec_token) else: sec_buffer = None rc, out_buffer = self._context.authorize(sec_buffer_in=sec_buffer) self._call_counter += 1 if rc not in success_codes: rc_name = "Unknown Error" for name, value in vars(sspicon).items(): if isinstance(value, int) and name.startswith("SEC_") and \ value == rc: rc_name = name break raise AuthenticationError( "InitializeSecurityContext failed on call %d: (%d) %s 0x%s" % (self._call_counter, rc, rc_name, format(rc, 'x')) ) return out_buffer[0].Buffer
def _create_buffer_array( self, max_token_size: int, server_sasl_creds: Optional[bytes], peercert: Optional[X509], ): """ Creates a buffer array to be passed to an SSPI client authenticator. If serverSaslCreds are provided, they will be appended to the created buffer. If a peercert is provided, a channel binding token will be appended to the buffer Args: max_token_size: As defined by ClientAuth this value is the maximum size of a token for the handshake server_sasl_creds: The serverSaslCreds received from the server that need to be included on the next call to authorize() peercert: Peer SSL certificate taken off of a transport Returns: PySecBufferDescType: The array of PySecBufferTypes """ buffer_array = win32security.PySecBufferDescType() if server_sasl_creds: server_sasl_creds_buffer = win32security.PySecBufferType( max_token_size, sspicon.SECBUFFER_TOKEN) server_sasl_creds_buffer.Buffer = server_sasl_creds buffer_array.append(server_sasl_creds_buffer) # To support servers that have turned on LdapEnforceChannelBinding we add this token if peercert: try: appdata = util.create_appdata_from_peercert(peercert) except UnsupportedAlgorithm: log.msg( "Skipping the creation of the CBT due to unsupported hash algorithm" ) else: cbt_buffer = self._create_sspi_channel_binding_token( max_token_size, appdata) buffer_array.append(cbt_buffer) return buffer_array
def _doTestEncrypt(self, pkg_name): sspiclient, sspiserver = self._doAuth(pkg_name) pkg_size_info=sspiclient.ctxt.QueryContextAttributes(sspicon.SECPKG_ATTR_SIZES) msg=str2bytes('some data to be encrypted ......') trailersize=pkg_size_info['SecurityTrailer'] encbuf=win32security.PySecBufferDescType() encbuf.append(win32security.PySecBufferType(len(msg), sspicon.SECBUFFER_DATA)) encbuf.append(win32security.PySecBufferType(trailersize, sspicon.SECBUFFER_TOKEN)) encbuf[0].Buffer=msg sspiclient.ctxt.EncryptMessage(0,encbuf,1) sspiserver.ctxt.DecryptMessage(encbuf,1) self.failUnlessEqual(msg, encbuf[0].Buffer) # and test.py the higher-level functions data_in = str2bytes("hello") data, sig = sspiclient.encrypt(data_in) self.assertEqual(sspiserver.decrypt(data, sig), data_in) data, sig = sspiserver.encrypt(data_in) self.assertEqual(sspiclient.decrypt(data, sig), data_in)
def wrap(self, msg, encrypt=False): """ GSSAPI's wrap with SSPI. https://docs.microsoft.com/en-us/windows/win32/secauthn/sspi-kerberos-interoperability-with-gssapi Usable mainly with Kerberos SSPI package, but this is not enforced. Wrap a message to be sent to the other side. Encrypted if encrypt is True. """ size_info = self.ctxt.QueryContextAttributes(sspicon.SECPKG_ATTR_SIZES) trailer_size = size_info['SecurityTrailer'] block_size = size_info['BlockSize'] buffer = win32security.PySecBufferDescType() # This buffer will contain unencrypted data to wrap, and maybe encrypt. buffer.append( win32security.PySecBufferType(len(msg), sspicon.SECBUFFER_DATA)) buffer[0].Buffer = msg # Will receive the token that forms the beginning of the msg buffer.append( win32security.PySecBufferType(trailer_size, sspicon.SECBUFFER_TOKEN)) # The trailer is needed in case of block encryption buffer.append( win32security.PySecBufferType(block_size, sspicon.SECBUFFER_PADDING)) fQOP = 0 if encrypt else sspicon.SECQOP_WRAP_NO_ENCRYPT self.ctxt.EncryptMessage(fQOP, buffer, self._get_next_seq_num()) # Sec token, then data, then padding r = buffer[1].Buffer + buffer[0].Buffer + buffer[2].Buffer return r
def unwrap(self, token): """ GSSAPI's unwrap with SSPI. https://docs.microsoft.com/en-us/windows/win32/secauthn/sspi-kerberos-interoperability-with-gssapi Usable mainly with Kerberos SSPI package, but this is not enforced. Return the clear text, and a boolean that is True if the token was encrypted. """ buffer = win32security.PySecBufferDescType() # This buffer will contain a "stream", which is the token coming from the other side buffer.append( win32security.PySecBufferType(len(token), sspicon.SECBUFFER_STREAM)) buffer[0].Buffer = token # This buffer will receive the clear, or just unwrapped text if no encryption was used. # Will be resized by the lib. buffer.append(win32security.PySecBufferType(0, sspicon.SECBUFFER_DATA)) pfQOP = self.ctxt.DecryptMessage(buffer, self._get_next_seq_num()) r = buffer[1].Buffer return r, not (pfQOP == sspicon.SECQOP_WRAP_NO_ENCRYPT)
# The server can now impersonate the client. In this demo the 2 users will # always be the same. sspiserver.ctxt.ImpersonateSecurityContext() print('Impersonated user: '******'Reverted to self: ', win32api.GetUserName()) pkg_size_info = sspiclient.ctxt.QueryContextAttributes( sspicon.SECPKG_ATTR_SIZES) # Now sign some data msg = 'some data to be encrypted ......' sigsize = pkg_size_info['MaxSignature'] sigbuf = win32security.PySecBufferDescType() sigbuf.append(win32security.PySecBufferType(len(msg), sspicon.SECBUFFER_DATA)) sigbuf.append(win32security.PySecBufferType(sigsize, sspicon.SECBUFFER_TOKEN)) sigbuf[0].Buffer = msg sspiclient.ctxt.MakeSignature(0, sigbuf, 1) sspiserver.ctxt.VerifySignature(sigbuf, 1) # And finally encrypt some. trailersize = pkg_size_info['SecurityTrailer'] encbuf = win32security.PySecBufferDescType() encbuf.append(win32security.PySecBufferType(len(msg), sspicon.SECBUFFER_DATA)) encbuf.append( win32security.PySecBufferType(trailersize, sspicon.SECBUFFER_TOKEN)) encbuf[0].Buffer = msg sspiclient.ctxt.EncryptMessage(0, encbuf, 1) print('Encrypted data:', repr(encbuf[0].Buffer))
def _retry_using_http_Negotiate_auth(self, response, scheme, args): if 'Authorization' in response.request.headers: return response if self._host is None: targeturl = urlparse(response.request.url) self._host = targeturl.hostname try: self._host = socket.getaddrinfo(self._host, None, 0, 0, 0, socket.AI_CANONNAME)[0][3] except socket.gaierror as e: _logger.info( 'Skipping canonicalization of name %s due to error: %s', self._host, e) targetspn = '{}/{}'.format(self._service, self._host) # Set up SSPI connection structure pkg_info = win32security.QuerySecurityPackageInfo(scheme) clientauth = sspi.ClientAuth(scheme, targetspn=targetspn, auth_info=self._auth_info) sec_buffer = win32security.PySecBufferDescType() # Channel Binding Hash (aka Extended Protection for Authentication) # If this is a SSL connection, we need to hash the peer certificate, prepend the RFC5929 channel binding type, # and stuff it into a SEC_CHANNEL_BINDINGS structure. # This should be sent along in the initial handshake or Kerberos auth will fail. if hasattr(response, 'peercert') and response.peercert is not None: md = hashlib.sha256() md.update(response.peercert) appdata = 'tls-server-end-point:'.encode('ASCII') + md.digest() cbtbuf = win32security.PySecBufferType( pkg_info['MaxToken'], sspicon.SECBUFFER_CHANNEL_BINDINGS) cbtbuf.Buffer = struct.pack('LLLLLLLL{}s'.format(len(appdata)), 0, 0, 0, 0, 0, 0, len(appdata), 32, appdata) sec_buffer.append(cbtbuf) content_length = int(response.request.headers.get( 'Content-Length', '0'), base=10) if hasattr(response.request.body, 'seek'): if content_length > 0: response.request.body.seek(-content_length, 1) else: response.request.body.seek(0, 0) # Consume content and release the original connection # to allow our new request to reuse the same one. response.content response.raw.release_conn() request = response.request.copy() # this is important for some web applications that store # authentication-related info in cookies if response.headers.get('set-cookie'): request.headers['Cookie'] = response.headers.get('set-cookie') # Send initial challenge auth header try: error, auth = clientauth.authorize(sec_buffer) request.headers['Authorization'] = '{} {}'.format( scheme, base64.b64encode(auth[0].Buffer).decode('ASCII')) _logger.debug( 'Sending Initial Context Token - error={} authenticated={}'. format(error, clientauth.authenticated)) except pywintypes.error as e: _logger.error('Error calling {}: {}'.format(e[1], e[2]), exc_info=e) return response # A streaming response breaks authentication. # This can be fixed by not streaming this request, which is safe # because the returned response3 will still have stream=True set if # specified in args. In addition, we expect this request to give us a # challenge and not the real content, so the content will be short # anyway. args_nostream = dict(args, stream=False) response2 = response.connection.send(request, **args_nostream) # Should get another 401 if we are doing challenge-response (NTLM) if response2.status_code != 401: if response2.status_code == 200: # Kerberos may have succeeded; if so, finalize our auth context final = response2.headers.get('WWW-Authenticate') if final is not None: try: # Sometimes Windows seems to forget to prepend 'Negotiate' to the success response, # and we get just a bare chunk of base64 token. Not sure why. final = final.replace(scheme, '', 1).lstrip() tokenbuf = win32security.PySecBufferType( pkg_info['MaxToken'], sspicon.SECBUFFER_TOKEN) tokenbuf.Buffer = base64.b64decode(final) sec_buffer.append(tokenbuf) error, auth = clientauth.authorize(sec_buffer) _logger.debug( 'Kerberos Authentication succeeded - error={} authenticated={}' .format(error, clientauth.authenticated)) except TypeError: pass # Regardless of whether or not we finalized our auth context, # without a 401 we've got nothing to do. Update the history and return. response2.history.append(response) return response2 # Consume content and release the original connection # to allow our new request to reuse the same one. response2.content response2.raw.release_conn() request = response2.request.copy() # Keep passing the cookies along if response2.headers.get('set-cookie'): request.headers['Cookie'] = response2.headers.get('set-cookie') # Extract challenge message from server challenge = [ val[len(scheme) + 1:] for val in response2.headers.get( 'WWW-Authenticate', '').split(', ') if scheme in val ] if len(challenge) != 1: raise HTTPError( 'Did not get exactly one {} challenge from server.'.format( scheme)) # Add challenge to security buffer tokenbuf = win32security.PySecBufferType(pkg_info['MaxToken'], sspicon.SECBUFFER_TOKEN) tokenbuf.Buffer = base64.b64decode(challenge[0]) sec_buffer.append(tokenbuf) _logger.debug('Got Challenge Token (NTLM)') # Perform next authorization step error, auth = clientauth.authorize(sec_buffer) request.headers['Authorization'] = '{} {}'.format( scheme, base64.b64encode(auth[0].Buffer).decode('ASCII')) _logger.debug('Sending Response - error={} authenticated={}'.format( error, clientauth.authenticated)) response3 = response2.connection.send(request, **args) # Update the history and return response3.history.append(response) response3.history.append(response2) return response3
def retry_using_http_NTLM_auth(self, auth_header_field, auth_header, response, auth_type, args): """Attempt to authenticate using HTTP NTLM challenge/response.""" if auth_header in response.request.headers: return response content_length = int(response.request.headers.get( 'Content-Length', '0'), base=10) if hasattr(response.request.body, 'seek'): if content_length > 0: response.request.body.seek(-content_length, 1) else: response.request.body.seek(0, 0) # Consume content and release the original connection # to allow our new request to reuse the same one. response.content response.raw.release_conn() request = response.request.copy() # initial auth header with username. will result in challenge if self._use_default_credentials: pkg_info = win32security.QuerySecurityPackageInfo(_package) clientauth = sspi.ClientAuth(_package) sec_buffer = win32security.PySecBufferDescType() error, auth = clientauth.authorize(sec_buffer) request.headers[auth_header] = '{} {}'.format( _package, b64encode(auth[0].Buffer).decode('ascii')) else: msg = "%s\\%s" % (self.domain, self.username) if self.domain else self.username # ntlm returns the headers as a base64 encoded bytestring. Convert to # a string. auth = '%s %s' % (auth_type, ntlm.create_NTLM_NEGOTIATE_MESSAGE( msg).decode('ascii')) request.headers[auth_header] = auth # A streaming response breaks authentication. # This can be fixed by not streaming this request, which is safe # because the returned response3 will still have stream=True set if # specified in args. In addition, we expect this request to give us a # challenge and not the real content, so the content will be short # anyway. args_nostream = dict(args, stream=False) response2 = response.connection.send(request, **args_nostream) # needed to make NTLM auth compatible with requests-2.3.0 # Consume content and release the original connection # to allow our new request to reuse the same one. response2.content response2.raw.release_conn() request = response2.request.copy() # this is important for some web applications that store # authentication-related info in cookies (it took a long time to # figure out) if response2.headers.get('set-cookie'): request.headers['Cookie'] = response2.headers.get('set-cookie') # get the challenge auth_header_value = response2.headers[auth_header_field] auth_strip = auth_type + ' ' ntlm_header_value = next( s for s in (val.lstrip() for val in auth_header_value.split(',')) if s.startswith(auth_strip)).strip() challenge_value = ntlm_header_value[len(auth_strip):] # build response if self._use_default_credentials: # Add challenge to security buffer tokenbuf = win32security.PySecBufferType(pkg_info['MaxToken'], sspicon.SECBUFFER_TOKEN) tokenbuf.Buffer = b64decode(challenge_value) sec_buffer.append(tokenbuf) # Perform next authorization step error, auth = clientauth.authorize(sec_buffer) request.headers[auth_header] = '{} {}'.format( _package, b64encode(auth[0].Buffer).decode('ascii')) else: ServerChallenge, NegotiateFlags = ntlm.parse_NTLM_CHALLENGE_MESSAGE( challenge_value) # ntlm returns the headers as a base64 encoded bytestring. Convert to a # string. auth = '%s %s' % (auth_type, ntlm.create_NTLM_AUTHENTICATE_MESSAGE( ServerChallenge, self.username, self.domain, self.password, NegotiateFlags).decode('ascii')) request.headers[auth_header] = auth response3 = response2.connection.send(request, **args) # Update the history. response3.history.append(response) response3.history.append(response2) return response3