Hello community,

here is the log from the commit of package python-ntlm-auth for 
openSUSE:Factory checked in at 2019-01-03 18:07:17
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-ntlm-auth (Old)
 and      /work/SRC/openSUSE:Factory/.python-ntlm-auth.new.28833 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-ntlm-auth"

Thu Jan  3 18:07:17 2019 rev:4 rq:662307 version:1.2.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-ntlm-auth/python-ntlm-auth.changes        
2018-12-24 11:40:26.317502869 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-ntlm-auth.new.28833/python-ntlm-auth.changes 
    2019-01-03 18:07:21.104101779 +0100
@@ -1,0 +2,10 @@
+Fri Dec 28 15:12:54 UTC 2018 - [email protected]
+
+- Update to version 1.2.0
+  * Deprecated ntlm_auth.ntlm.Ntlm in favour of
+    ntlm_auth.ntlm.NtlmContext
+    This is because Ntlm is heavily geared towards HTTP auth which
+    is not always the case, `NtlmContext` makes things more generic
+    Updated docs and tests to reflect this
+
+-------------------------------------------------------------------

Old:
----
  python-ntlm-auth-1.1.0.tar.gz

New:
----
  python-ntlm-auth-1.2.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-ntlm-auth.spec ++++++
--- /var/tmp/diff_new_pack.jabN2w/_old  2019-01-03 18:07:21.644101299 +0100
+++ /var/tmp/diff_new_pack.jabN2w/_new  2019-01-03 18:07:21.648101296 +0100
@@ -12,13 +12,13 @@
 # license that conforms to the Open Source Definition (Version 1.9)
 # published by the Open Source Initiative.
 
-# Please submit bugfixes or comments via https://bugs.opensuse.org/
+# Please submit bugfixes or comments via http://bugs.opensuse.org/
 #
 
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-ntlm-auth
-Version:        1.1.0
+Version:        1.2.0
 Release:        0
 Summary:        NTLM low-level Python library
 License:        MIT

++++++ python-ntlm-auth-1.1.0.tar.gz -> python-ntlm-auth-1.2.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ntlm-auth-1.1.0/.travis.yml 
new/ntlm-auth-1.2.0/.travis.yml
--- old/ntlm-auth-1.1.0/.travis.yml     2018-03-06 23:38:38.000000000 +0100
+++ new/ntlm-auth-1.2.0/.travis.yml     2018-06-07 09:32:00.000000000 +0200
@@ -3,7 +3,6 @@
 python:
 - "2.6"
 - "2.7"
-- "3.3"
 - "3.4"
 - "3.5"
 - "3.6"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ntlm-auth-1.1.0/CHANGES.md 
new/ntlm-auth-1.2.0/CHANGES.md
--- old/ntlm-auth-1.1.0/CHANGES.md      2018-03-06 23:38:38.000000000 +0100
+++ new/ntlm-auth-1.2.0/CHANGES.md      2018-06-07 09:32:00.000000000 +0200
@@ -1,5 +1,12 @@
 # Changes
 
+## 1.2.0 (Jun 7, 2018)
+
+* Deprecated ntlm_auth.ntlm.Ntlm in favour of ntlm_auth.ntlm.NtlmContext
+* This is because `Ntlm` is heavily geared towards HTTP auth which is not 
always the case, `NtlmContext` makes things more generic
+* Updated docs and tests to reflect this
+* Dropped support for Python 3.3
+
 ## 1.1.0 (Mar 7, 2018)
 
 * Removed DES code as the license was found to be incorrect from the source
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ntlm-auth-1.1.0/README.md 
new/ntlm-auth-1.2.0/README.md
--- old/ntlm-auth-1.1.0/README.md       2018-03-06 23:38:38.000000000 +0100
+++ new/ntlm-auth-1.2.0/README.md       2018-06-07 09:32:00.000000000 +0200
@@ -2,11 +2,6 @@
 =========
 [![Build 
Status](https://travis-ci.org/jborean93/ntlm-auth.svg?branch=master)](https://travis-ci.org/jborean93/ntlm-auth)[![Build
 
status](https://ci.appveyor.com/api/projects/status/osvvfgmhfk4anvu0/branch/master?svg=true)](https://ci.appveyor.com/project/jborean93/ntlm-auth/branch/master)[![Coverage
 
Status](https://coveralls.io/repos/github/jborean93/ntlm-auth/badge.svg?branch=master)](https://coveralls.io/github/jborean93/ntlm-auth?branch=master)
 
-This was originally a fork of
-[python-ntlm3](https://github.com/trustrachel/python-ntlm3) but has changed
-substantially since to support newer features like NTLMv2 and encryption
-that was not present before.
-
 About this library
 ------------------
 
@@ -56,11 +51,11 @@
 Extended Session Security is a security feature designed to increase the 
security of LM and NTLMv1 auth. It is no substitution for NTLMv2 but is better 
than nothing and should be used if possible when you need NTLMv1 compatibility.
 
 The variables required are outlined below;
-* `user_name` - The username to authenticate with, should not have the domain 
prefix, i.e. USER not DOMAIN\\USER
+* `username` - The username to authenticate with, should not have the domain 
prefix, i.e. USER not DOMAIN\\USER
 * `password` - The password of the user to authenticate with
-* `domain_name` - The domain of the user, i.e. DOMAIN. Can be blank if not in 
a domain environment
+* `domain` - The domain of the user, i.e. DOMAIN. Can be blank if not in a 
domain environment
 * `workstation` - The workstation you are running on. Can be blank if you do 
not wish to send this
-* `server_certificate_hash` - (NTLMv2 only) The SHA256 hash of the servers DER 
encoded certificate. Used to calculate the Channel Binding Tokens and should be 
added even if it isn't required. Can be blank but auth will fail if the server 
requires this hash.
+* `cbt_data` - (NTLMv2 only) The 
`gss_channel_bindings.GssChannelBindingsStruct` used to bind with the auth 
response. Can be None if no binding needs to occur
 
 
 #### LM Auth/NTLMv1 Auth
@@ -70,20 +65,20 @@
 ```python
 import socket
 
-from ntlm_auth.ntlm import Ntlm
+from ntlm_auth.ntlm import NtlmContext
 
-user_name = 'User'
+username = 'User'
 password = 'Password'
-domain_name = 'Domain' # Can be blank if you are not in a domain
+domain = 'Domain' # Can be blank if you are not in a domain
 workstation = socket.gethostname().upper() # Can be blank if you wish to not 
send this info
 
-ntlm_context = Ntlm(ntlm_compatibility=0) # Put the ntlm_compatibility level 
here, 0-2 for LM Auth/NTLMv1 Auth
-negotiate_message = ntlm_context.create_negotiate_message(domain_name, 
workstation).decode()
+ntlm_context = NtlmContext(username, password, domain, workstation, 
ntlm_compatibility=0) # Put the ntlm_compatibility level here, 0-2 for LM 
Auth/NTLMv1 Auth
+negotiate_message = ntlm_context.step()
 
 # Attach the negotiate_message to your NTLM/NEGOTIATE HTTP header and send to 
the server. Get the challenge response back from the server
 challenge_message = http.response.headers['HEADERFIELD']
 
-authenticate_message = ntlm_context.create_authenticate_message(user_name, 
password, domain_name, workstation).decode()
+authenticate_message = ntlm_context.step(challenge_message)
 
 # Attach the authenticate_message ot your NTLM_NEGOTIATE HTTP header and send 
to the server. You are now authenticated with NTLMv1
 ```
@@ -93,23 +88,30 @@
 NTLMv2 Auth is the newest NTLM auth method from Microsoft and should be the 
option chosen by default unless you require an older auth method. The 
implementation is the same as NTLMv1 but with the addition of the optional 
`server_certificate_hash` variable and the `ntlm_compatibility` is not 
specified.
 
 ```python
+import base64
 import socket
 
-from ntlm_auth.ntlm import Ntlm
+from ntlm_auth.gss_channel_bindings import GssChannelBindingsStruct
+from ntlm_auth.ntlm import NtlmContext
 
-user_name = 'User'
+username = 'User'
 password = 'Password'
-domain_name = 'Domain' # Can be blank if you are not in a domain
+domain = 'Domain' # Can be blank if you are not in a domain
 workstation = socket.gethostname().upper() # Can be blank if you wish to not 
send this info
-server_certificate_hash = 
'96B2FC1EC30792619286A0C7FD62863E81A6564E72829CBC0A46F7B1D5D92A18' # Can be 
blank if you don't want CBT sent
 
-ntlm_context = Ntlm()
-negotiate_message = ntlm_context.create_negotiate_message(domain_name, 
workstation).decode()
+# create the CBT struct if you wish to bind it with the auth response
+server_certificate_hash = 
'96B2FC1EC30792619286A0C7FD62863E81A6564E72829CBC0A46F7B1D5D92A18'
+certificate_digest = base64.b16decode(server_certificate_hash)
+cbt_data = GssChannelBindingsStruct()
+cbt_data[cbt_data.APPLICATION_DATA] = b'tls-server-end-point:' + 
certificate_digest
+
+ntlm_context = NtlmContext(username, password, domain, workstation, cbt_data, 
ntlm_compatibility=3)
+negotiate_message = ntlm_context.step()
 
 # Attach the negotiate_message to your NTLM/NEGOTIATE HTTP header and send to 
the server. Get the challenge response back from the server
 challenge_message = http.response.headers['HEADERFIELD']
 
-authenticate_message = ntlm_context.create_authenticate_message(user_name, 
password, domain_name, workstation, server_certificate_hash).decode()
+authenticate_message = ntlm_context.step(challenge_message)
 
 # Attach the authenticate_message ot your NTLM_NEGOTIATE HTTP header and send 
to the server. You are now authenticated with NTLMv1
 ```
@@ -119,40 +121,40 @@
 All version of NTLM supports signing (integrity) and sealing (confidentiality) 
of message content. This function can add these improvements to a message that 
is sent and received from the server. While it does encrypt the data if 
supported by the server it is only done with RC4 with a 128-bit key which is 
not very secure and on older systems this key length could be 56 or 40 bit. 
This functionality while tested and conforms with the Microsoft documentation 
has yet to be fully tested in an integrated environment. Once again this has 
not been thoroughly tested and has only passed unit tests and their expections.
 
 ```python
+import base64
 import socket
 
-from ntlm_auth.ntlm import Ntlm
+from ntlm_auth.ntlm import NtlmContext
 
-user_name = 'User'
+username = 'User'
 password = 'Password'
-domain_name = 'Domain' # Can be blank if you are not in a domain
+domain = 'Domain' # Can be blank if you are not in a domain
 workstation = socket.gethostname().upper() # Can be blank if you wish to not 
send this info
-msg_data = "Message to send to the server"
-server_certificate_hash = 
'96B2FC1EC30792619286A0C7FD62863E81A6564E72829CBC0A46F7B1D5D92A18' # Can be 
blank if you don't want CBT sent
 
-ntlm_context = Ntlm()
-negotiate_message = ntlm_context.create_negotiate_message(domain_name, 
workstation).decode()
+# create the CBT struct if you wish to bind it with the auth response
+server_certificate_hash = 
'96B2FC1EC30792619286A0C7FD62863E81A6564E72829CBC0A46F7B1D5D92A18'
+certificate_digest = base64.b16decode(server_certificate_hash)
+cbt_data = GssChannelBindingsStruct()
+cbt_data[cbt_data.APPLICATION_DATA] = b'tls-server-end-point:' + 
certificate_digest
+
+ntlm_context = NtlmContext(username, password, domain, workstation, cbt_data, 
ntlm_compatibility=3)
+negotiate_message = ntlm_context.step()
 
 # Attach the negotiate_message to your NTLM/NEGOTIATE HTTP header and send to 
the server. Get the challenge response back from the server
 challenge_message = http.response.headers['HEADERFIELD']
 
-authenticate_message = ntlm_context.create_authenticate_message(user_name, 
password, domain_name, workstation, server_certificate_hash).decode()
+authenticate_message = ntlm_context.step(challenge_message)
 
-if ntlm_context.session_security is None:
-    raise Exception("Server does not support signing and sealing")
-else:
-    session_security = ntlm_context.session_security
+# Attach the authenticate_message ot your NTLM_NEGOTIATE HTTP header and send 
to the server. You are now authenticated with NTLMv1
 
-# Encrypt the msg with the sealing function and send the message
-msg_data, msg_signature = session_security.wrap(msg_data)
+# Encrypt the message with the wrapping function and send the message
+enc_message = ntlm_context.wrap("Message to send", encrypt=True)
 request.body = msg_data
-request.header = "NTLM %s" % authenticate_message
 request.send
 
-# Receive the response the from the server
-response_msg = response.body[bodyindex]
-response_signature = response.body[signatureindex]
-response_msg = session_security.unwrap(response_msg, response_signature)
+# Receive the response from the server and decrypt
+response_msg = response.content
+response = ntlm_context.unwrap(response_msg)
 ```
 
 Backlog
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ntlm-auth-1.1.0/appveyor.yml 
new/ntlm-auth-1.2.0/appveyor.yml
--- old/ntlm-auth-1.1.0/appveyor.yml    2018-03-06 23:38:38.000000000 +0100
+++ new/ntlm-auth-1.2.0/appveyor.yml    2018-06-07 09:32:00.000000000 +0200
@@ -13,8 +13,6 @@
   # https://www.appveyor.com/docs/installed-software/#python
   - PYTHON: Python27
   - PYTHON: Python27-x64
-  - PYTHON: Python33
-  - PYTHON: Python33-x64
   - PYTHON: Python34
   - PYTHON: Python34-x64
   - PYTHON: Python35
@@ -27,7 +25,6 @@
 
 init:
 - ps: |
-    $ErrorActionPreference = "Stop"
     # Override default Python version/architecture
     $env:Path="C:\$env:PYTHON;C:\$env:PYTHON\Scripts;$env:PATH"
     python -c "import platform; print('Python', platform.python_version(), 
platform.architecture()[0])"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ntlm-auth-1.1.0/ntlm_auth/compute_response.py 
new/ntlm-auth-1.2.0/ntlm_auth/compute_response.py
--- old/ntlm-auth-1.1.0/ntlm_auth/compute_response.py   2018-03-06 
23:38:38.000000000 +0100
+++ new/ntlm-auth-1.2.0/ntlm_auth/compute_response.py   2018-06-07 
09:32:00.000000000 +0200
@@ -8,6 +8,7 @@
 import os
 import struct
 import time
+import warnings
 
 import ntlm_auth.compute_hash as comphash
 import ntlm_auth.compute_keys as compkeys
@@ -101,7 +102,7 @@
         return response
 
     def get_nt_challenge_response(self, lm_challenge_response,
-                                  server_certificate_hash):
+                                  server_certificate_hash=None, cbt_data=None):
         """
         [MS-NLMP] v28.0 2016-07-14
 
@@ -117,10 +118,10 @@
 
         :param lm_challenge_response: The LmChallengeResponse calculated
             beforehand, used to get the key_exchange_key value
-        :param server_certificate_hash: The SHA256 hash of the server
-            certificate (DER encoded) NTLM is authenticated to. Used in Channel
-            Binding Tokens if present, default value is None. See
-            AuthenticateMessage in messages.py for more details
+        :param server_certificate_hash: This is deprecated and will be removed
+            in a future version, use cbt_data instead
+        :param cbt_data: The GssChannelBindingsStruct to bind in the NTLM
+            response
         :return response: (NtChallengeResponse) - The NT response to the server
             challenge. Computed by the client
         :return session_base_key: (SessionBaseKey) - A session key calculated
@@ -178,11 +179,21 @@
                 target_info[AvId.MSV_AV_FLAGS] = \
                     struct.pack("<L", AvFlags.MIC_PROVIDED)
 
-            if server_certificate_hash is not None:
-                channel_bindings_hash = \
-                    self._get_channel_bindings_value(server_certificate_hash)
-                target_info[AvId.MSV_AV_CHANNEL_BINDINGS] = \
-                    channel_bindings_hash
+            if server_certificate_hash is not None and cbt_data is None:
+                warnings.warn("Manually creating the cbt stuct from the cert "
+                              "hash will be removed in a newer version of "
+                              "ntlm-auth. Send the actual CBT struct using "
+                              "cbt_data instead", DeprecationWarning)
+                certificate_digest = base64.b16decode(server_certificate_hash)
+
+                cbt_data = GssChannelBindingsStruct()
+                cbt_data[cbt_data.APPLICATION_DATA] = \
+                    b'tls-server-end-point:' + certificate_digest
+
+            if cbt_data is not None:
+                cbt_bytes = cbt_data.get_data()
+                cbt_hash = hashlib.md5(cbt_bytes).digest()
+                target_info[AvId.MSV_AV_CHANNEL_BINDINGS] = cbt_hash
 
             response, session_base_key = \
                 self._get_NTLMv2_response(self._user_name, self._password,
@@ -445,38 +456,6 @@
         res = res + dobj.encrypt(server_challenge[0:8])
         return res
 
-    @staticmethod
-    def _get_channel_bindings_value(server_certificate_hash):
-        """
-        Get's the MD5 hash of the gss_channel_bindings_struct to add to the
-        AV_PAIR MSV_AV_CHANNEL_BINDINGS. This method takes in the SHA256 hash
-        (Hash of the DER encoded certificate of the server we are connecting
-        to) and add's it to the gss_channel_bindings_struct. It then gets the
-        MD5 hash and converts this to a byte array in preparation of adding it
-        to the AV_PAIR structure.
-
-        :param server_certificate_hash: The SHA256 hash of the server
-            certificate (DER encoded) NTLM is authenticated to
-        :return channel_bindings: An MD5 hash of the
-            gss_channel_bindings_struct to add to the AV_PAIR
-            MsvChannelBindings
-        """
-        # Channel Binding Tokens support, used for NTLMv2
-        # Decode the SHA256 certificate hash
-        certificate_digest = base64.b16decode(server_certificate_hash)
-
-        # Initialise the GssChannelBindingsStruct and add the
-        # certificate_digest to the application_data field
-        gss_channel_bindings = GssChannelBindingsStruct()
-        gss_channel_bindings[gss_channel_bindings.APPLICATION_DATA] = \
-            b'tls-server-end-point:' + certificate_digest
-
-        # Get the gss_channel_bindings_struct and create an MD5 hash
-        channel_bindings_struct_data = gss_channel_bindings.get_data()
-        channel_bindings = hashlib.md5(channel_bindings_struct_data).digest()
-
-        return channel_bindings
-
 
 def get_windows_timestamp():
     # Get Windows Date time, 100 nanoseconds since 1601-01-01 in a 64 bit
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ntlm-auth-1.1.0/ntlm_auth/exceptions.py 
new/ntlm-auth-1.2.0/ntlm_auth/exceptions.py
--- old/ntlm-auth-1.1.0/ntlm_auth/exceptions.py 1970-01-01 01:00:00.000000000 
+0100
+++ new/ntlm-auth-1.2.0/ntlm_auth/exceptions.py 2018-06-07 09:32:00.000000000 
+0200
@@ -0,0 +1,6 @@
+# Copyright: (c) 2018, Jordan Borean (@jborean93) <[email protected]>
+# MIT License (see LICENSE or https://opensource.org/licenses/MIT)
+
+
+class NoAuthContextError(Exception):
+    pass
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ntlm-auth-1.1.0/ntlm_auth/messages.py 
new/ntlm-auth-1.2.0/ntlm_auth/messages.py
--- old/ntlm-auth-1.1.0/ntlm_auth/messages.py   2018-03-06 23:38:38.000000000 
+0100
+++ new/ntlm-auth-1.2.0/ntlm_auth/messages.py   2018-06-07 09:32:00.000000000 
+0200
@@ -254,7 +254,7 @@
 
     def __init__(self, user_name, password, domain_name, workstation,
                  challenge_message, ntlm_compatibility,
-                 server_certificate_hash):
+                 server_certificate_hash=None, cbt_data=None):
         """
         [MS-NLMP] v28.0 2016-07-14
 
@@ -276,13 +276,9 @@
         :param ntlm_compatibility: The Lan Manager Compatibility Level, used to
             determine what NTLM auth version to use, see Ntlm in ntlm.py for
             more details
-        :param server_certificate_hash: The SHA256 hash string of the server
-            certificate (DER encoded) NTLM is authenticating to. This is used
-            to add to the gss_channel_bindings_struct for Channel Binding
-            Tokens support. If none is passed through then ntlm-auth will not
-            use Channel Binding Tokens when authenticating with the server
-            which could cause issues if it is set to only authenticate when
-            these are present. This is only used for NTLMv2 authentication.
+        :param server_certificate_hash: Deprecated, used cbt_data instead
+        :param cbt_data: The GssChannelBindingsStruct that contains the CBT
+            data to bind in the auth response
 
         Message Attributes (Attributes used to compute the message structure):
             signature: An 8-byte character array that MUST contain the ASCII
@@ -351,7 +347,7 @@
             compute_response.get_lm_challenge_response()
         self.nt_challenge_response, key_exchange_key, target_info = \
             compute_response.get_nt_challenge_response(
-                self.lm_challenge_response, server_certificate_hash)
+                self.lm_challenge_response, server_certificate_hash, cbt_data)
         self.target_info = target_info
 
         if self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_KEY_EXCH:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ntlm-auth-1.1.0/ntlm_auth/ntlm.py 
new/ntlm-auth-1.2.0/ntlm_auth/ntlm.py
--- old/ntlm-auth-1.1.0/ntlm_auth/ntlm.py       2018-03-06 23:38:38.000000000 
+0100
+++ new/ntlm-auth-1.2.0/ntlm_auth/ntlm.py       2018-06-07 09:32:00.000000000 
+0200
@@ -3,22 +3,34 @@
 
 import base64
 import struct
+import warnings
+
 from ntlm_auth.constants import NegotiateFlags
+from ntlm_auth.exceptions import NoAuthContextError
 from ntlm_auth.messages import AuthenticateMessage, ChallengeMessage, \
     NegotiateMessage
 from ntlm_auth.session_security import SessionSecurity
 
 
-class Ntlm(object):
+class NtlmContext(object):
 
-    def __init__(self, ntlm_compatibility=3):
+    def __init__(self, username, password, domain=None, workstation=None,
+                 cbt_data=None, ntlm_compatibility=3):
         """
+        Initialises a NTLM context to use when authenticating using the NTLM
+        protocol.
         Initialises the NTLM context to use when sending and receiving messages
         to and from the server. You should be using this object as it supports
         NTLMv2 authenticate and it easier to use than before. It also brings in
         the ability to use signing and sealing with session_security and
         generate a MIC structure.
 
+        :param username: The username to authenticate with
+        :param password: The password for the username
+        :param domain: The domain part of the username (None if n/a)
+        :param workstation: The localworkstation (None if n/a)
+        :param cbt_data: A GssChannelBindingsStruct or None to bind channel
+            data with the auth process
         :param ntlm_compatibility: (Default 3)
             The Lan Manager Compatibility Level to use with the auth message
             This is set by an Administrator in the registry key
@@ -29,23 +41,15 @@
                 2 : NTLMv1 and NTLMv1 with Extended Session Security
                 3-5 : NTLMv2 Only
             Note: Values 3 to 5 are no different from a client perspective
-
-        Attributes:
-            negotiate_flags: A NEGOTIATE structure that contains a set of bit
-                flags. These flags are the options the client supports and are
-                sent in the negotiate_message
-            ntlm_compatibility: The Lan Manager Compatibility Level, same as
-                the input if supplied
-            negotiate_message: A NegotiateMessage object that is sent to the
-                server
-            challenge_message: A ChallengeMessage object that has been created
-                from the server response
-            authenticate_message: An AuthenticateMessage object that is sent to
-                the server based on the ChallengeMessage
-            session_security: A SessionSecurity structure that can be used to
-                sign and seal messages sent after the authentication challenge
         """
+        self.username = username
+        self.password = password
+        self.domain = domain
+        self.workstation = workstation
+        self.cbt_data = cbt_data
+        self._server_certificate_hash = None  # deprecated for backwards compat
         self.ntlm_compatibility = ntlm_compatibility
+        self.complete = False
 
         # Setting up our flags so the challenge message returns the target info
         # block if supported
@@ -62,77 +66,55 @@
         # Setting the message types based on the ntlm_compatibility level
         self._set_ntlm_compatibility_flags(self.ntlm_compatibility)
 
-        self.negotiate_message = None
-        self.challenge_message = None
-        self.authenticate_message = None
-        self.session_security = None
-
-    def create_negotiate_message(self, domain_name=None, workstation=None):
-        """
-        Create an NTLM NEGOTIATE_MESSAGE
-
-        :param domain_name: The domain name of the user account we are
-            authenticating with, default is None
-        :param worksation: The workstation we are using to authenticate with,
-            default is None
-        :return: A base64 encoded string of the NEGOTIATE_MESSAGE
-        """
-        self.negotiate_message = NegotiateMessage(self.negotiate_flags,
-                                                  domain_name, workstation)
-
-        return base64.b64encode(self.negotiate_message.get_data())
-
-    def parse_challenge_message(self, msg2):
-        """
-        Parse the NTLM CHALLENGE_MESSAGE from the server and add it to the Ntlm
-        context fields
-
-        :param msg2: A base64 encoded string of the CHALLENGE_MESSAGE
-        """
-        msg2 = base64.b64decode(msg2)
-        self.challenge_message = ChallengeMessage(msg2)
-
-    def create_authenticate_message(self, user_name, password,
-                                    domain_name=None, workstation=None,
-                                    server_certificate_hash=None):
-        """
-        Create an NTLM AUTHENTICATE_MESSAGE based on the Ntlm context and the
-        previous messages sent and received
-
-        :param user_name: The user name of the user we are trying to
-            authenticate with
-        :param password: The password of the user we are trying to authenticate
-            with
-        :param domain_name: The domain name of the user account we are
-            authenticated with, default is None
-        :param workstation: The workstation we are using to authenticate with,
-            default is None
-        :param server_certificate_hash: The SHA256 hash string of the server
-            certificate (DER encoded) NTLM is authenticating to. Used for
-            Channel Binding Tokens. If nothing is supplied then the CBT hash
-            will not be sent. See messages.py AuthenticateMessage for more
-            details
-        :return: A base64 encoded string of the AUTHENTICATE_MESSAGE
-        """
-        self.authenticate_message = \
-            AuthenticateMessage(user_name, password, domain_name, workstation,
-                                self.challenge_message,
-                                self.ntlm_compatibility,
-                                server_certificate_hash)
-        self.authenticate_message.add_mic(self.negotiate_message,
-                                          self.challenge_message)
-
-        # Setups up the session_security context used to sign and seal messages
-        # if wanted
-        if self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_SEAL or \
-                self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_SIGN:
-            flags = self.authenticate_message.negotiate_flags
-            flag_bytes = struct.unpack("<I", flags)[0]
-            self.session_security = \
-                SessionSecurity(flag_bytes,
-                                self.authenticate_message.exported_session_key)
-
-        return base64.b64encode(self.authenticate_message.get_data())
+        self._negotiate_message = None
+        self._challenge_message = None
+        self._authenticate_message = None
+        self._session_security = None
+
+    def step(self, input_token=None):
+        if self._negotiate_message is None:
+            self._negotiate_message = NegotiateMessage(self.negotiate_flags,
+                                                       self.domain,
+                                                       self.workstation)
+            return self._negotiate_message.get_data()
+        else:
+            self._challenge_message = ChallengeMessage(input_token)
+            self._authenticate_message = AuthenticateMessage(
+                self.username, self.password, self.domain, self.workstation,
+                self._challenge_message, self.ntlm_compatibility,
+                server_certificate_hash=self._server_certificate_hash,
+                cbt_data=self.cbt_data
+            )
+            self._authenticate_message.add_mic(self._negotiate_message,
+                                               self._challenge_message)
+
+            flag_bytes = self._authenticate_message.negotiate_flags
+            flags = struct.unpack("<I", flag_bytes)[0]
+            if flags & NegotiateFlags.NTLMSSP_NEGOTIATE_SEAL or \
+                    flags & NegotiateFlags.NTLMSSP_NEGOTIATE_SIGN:
+                self._session_security = SessionSecurity(
+                    flags, self._authenticate_message.exported_session_key
+                )
+
+            self.complete = True
+            return self._authenticate_message.get_data()
+
+    def wrap(self, data):
+        if self._session_security is None:
+            raise NoAuthContextError("Cannot wrap data as no security context "
+                                     "has been established")
+
+        data, header = self._session_security.wrap(data)
+        return header + data
+
+    def unwrap(self, data):
+        if self._session_security is None:
+            raise NoAuthContextError("Cannot unwrap data as no security "
+                                     "context has been established")
+        header = data[0:16]
+        data = data[16:]
+        message = self._session_security.unwrap(data, header)
+        return message
 
     def _set_ntlm_compatibility_flags(self, ntlm_compatibility):
         if (ntlm_compatibility >= 0) and (ntlm_compatibility <= 5):
@@ -150,3 +132,86 @@
         else:
             raise Exception("Unknown ntlm_compatibility level - "
                             "expecting value between 0 and 5")
+
+
+# Deprecated in favour of NtlmContext - this current class is heavily geared
+# towards a HTTP API which is not always the case with NTLM. This is currently
+# just a thin wrapper over NtlmContext and will be removed in future ntlm-auth
+# versions
+class Ntlm(object):
+
+    def __init__(self, ntlm_compatibility=3):
+        self._context = NtlmContext(None, None,
+                                    ntlm_compatibility=ntlm_compatibility)
+        self._challenge_token = None
+        warnings.warn("Using Ntlm() is deprecated and will be removed in a "
+                      "future version. Use NtlmContext() instead",
+                      DeprecationWarning)
+
+    @property
+    def negotiate_flags(self):
+        return self._context.negotiate_flags
+
+    @negotiate_flags.setter
+    def negotiate_flags(self, value):
+        self._context.negotiate_flags = value
+
+    @property
+    def ntlm_compatibility(self):
+        return self._context.ntlm_compatibility
+
+    @ntlm_compatibility.setter
+    def ntlm_compatibility(self, value):
+        self._context.ntlm_compatibility = value
+
+    @property
+    def negotiate_message(self):
+        return self._context._negotiate_message
+
+    @negotiate_message.setter
+    def negotiate_message(self, value):
+        self._context._negotiate_message = value
+
+    @property
+    def challenge_message(self):
+        return self._context._challenge_message
+
+    @challenge_message.setter
+    def challenge_message(self, value):
+        self._context._challenge_message = value
+
+    @property
+    def authenticate_message(self):
+        return self._context._authenticate_message
+
+    @authenticate_message.setter
+    def authenticate_message(self, value):
+        self._context._authenticate_message = value
+
+    @property
+    def session_security(self):
+        return self._context._session_security
+
+    @session_security.setter
+    def session_security(self, value):
+        self._context._session_security = value
+
+    def create_negotiate_message(self, domain_name=None, workstation=None):
+        self._context.domain = domain_name
+        self._context.workstation = workstation
+        msg = self._context.step()
+        return base64.b64encode(msg)
+
+    def parse_challenge_message(self, msg2):
+        self._challenge_token = base64.b64decode(msg2)
+
+    def create_authenticate_message(self, user_name, password,
+                                    domain_name=None, workstation=None,
+                                    server_certificate_hash=None):
+        self._context.username = user_name
+        self._context.password = password
+        self._context.domain = domain_name
+        self._context.workstation = workstation
+        self._context._server_certificate_hash = server_certificate_hash
+        msg = self._context.step(self._challenge_token)
+        return base64.b64encode(msg)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ntlm-auth-1.1.0/ntlm_auth/session_security.py 
new/ntlm-auth-1.2.0/ntlm_auth/session_security.py
--- old/ntlm-auth-1.1.0/ntlm_auth/session_security.py   2018-03-06 
23:38:38.000000000 +0100
+++ new/ntlm-auth-1.2.0/ntlm_auth/session_security.py   2018-06-07 
09:32:00.000000000 +0200
@@ -99,6 +99,7 @@
             when testing out a server sealing and unsealing
         """
         self.negotiate_flags = negotiate_flags
+        self.exported_session_key = exported_session_key
         self.outgoing_seq_num = 0
         self.incoming_seq_num = 0
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ntlm-auth-1.1.0/setup.py new/ntlm-auth-1.2.0/setup.py
--- old/ntlm-auth-1.1.0/setup.py        2018-03-06 23:38:38.000000000 +0100
+++ new/ntlm-auth-1.2.0/setup.py        2018-06-07 09:32:00.000000000 +0200
@@ -13,7 +13,7 @@
 
 setup(
     name='ntlm-auth',
-    version='1.1.0',
+    version='1.2.0',
     packages=['ntlm_auth'],
     install_requires=[],
     extras_require={
@@ -35,7 +35,6 @@
         'Programming Language :: Python :: 2.6',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.3',
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ntlm-auth-1.1.0/tests/test_compute_response.py 
new/ntlm-auth-1.2.0/tests/test_compute_response.py
--- old/ntlm-auth-1.1.0/tests/test_compute_response.py  2018-03-06 
23:38:38.000000000 +0100
+++ new/ntlm-auth-1.2.0/tests/test_compute_response.py  2018-06-07 
09:32:00.000000000 +0200
@@ -123,16 +123,6 @@
         assert actual_response == expected_response
         assert actual_key == expected_key
 
-    def test_channel_bindings_value(self):
-        # No example is explicitly set in MS-NLMP, using a random certificate
-        # hash and checking with the expected outcome
-        expected = b"\x6E\xA1\x9D\xF0\x66\xDA\x46\x22" \
-                   b"\x05\x1F\x9C\x4F\x92\xC6\xDF\x74"
-        cert_hash = \
-            "E3CA49271E5089CC48CE82109F1324F41DBEDDC29A777410C738F7868C4FF405"
-        actual = ComputeResponse._get_channel_bindings_value(cert_hash)
-        assert actual == expected
-
 
 class TestChallengeResults(object):
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ntlm-auth-1.1.0/tests/test_ntlm.py 
new/ntlm-auth-1.2.0/tests/test_ntlm.py
--- old/ntlm-auth-1.1.0/tests/test_ntlm.py      2018-03-06 23:38:38.000000000 
+0100
+++ new/ntlm-auth-1.2.0/tests/test_ntlm.py      2018-06-07 09:32:00.000000000 
+0200
@@ -9,10 +9,11 @@
 
 from requests.auth import AuthBase
 
-from ntlm_auth.constants import AvId, MessageTypes, NegotiateFlags, \
-    NTLM_SIGNATURE
-from ntlm_auth.messages import TargetInfo
-from ntlm_auth.ntlm import Ntlm
+from ntlm_auth.constants import NegotiateFlags
+from ntlm_auth.exceptions import NoAuthContextError
+from ntlm_auth.gss_channel_bindings import GssChannelBindingsStruct
+from ntlm_auth.ntlm import Ntlm, NtlmContext
+from ntlm_auth.session_security import SessionSecurity
 
 default_negotiate_flags = NegotiateFlags.NTLMSSP_NEGOTIATE_TARGET_INFO | \
                           NegotiateFlags.NTLMSSP_NEGOTIATE_128 | \
@@ -130,56 +131,6 @@
         actual = ntlm_context.create_negotiate_message("Domain", "COMPUTER")
         assert actual == expected
 
-    def test_parse_challenge_message(self):
-        test_target_info = TargetInfo()
-        test_target_info[AvId.MSV_AV_NB_DOMAIN_NAME] = \
-            "Domain".encode('utf-16-le')
-        test_target_info[AvId.MSV_AV_NB_COMPUTER_NAME] = \
-            "Server".encode('utf-16-le')
-        test_challenge_string = base64.b64encode(
-            b"\x4e\x54\x4c\x4d\x53\x53\x50\x00"
-            b"\x02\x00\x00\x00\x03\x00\x0c\x00"
-            b"\x38\x00\x00\x00\x33\x82\x8a\xe2"
-            b"\x01\x23\x45\x67\x89\xab\xcd\xef"
-            b"\x00\x00\x00\x00\x00\x00\x00\x00"
-            b"\x24\x00\x24\x00\x44\x00\x00\x00"
-            b"\x06\x00\x70\x17\x00\x00\x00\x0f"
-            b"\x53\x00\x65\x00\x72\x00\x76\x00"
-            b"\x65\x00\x72\x00\x02\x00\x0c\x00"
-            b"\x44\x00\x6f\x00\x6d\x00\x61\x00"
-            b"\x69\x00\x6e\x00\x01\x00\x0c\x00"
-            b"\x53\x00\x65\x00\x72\x00\x76\x00"
-            b"\x65\x00\x72\x00\x00\x00\x00\x00"
-        )
-        test_ntlm_context = Ntlm()
-        test_ntlm_context.parse_challenge_message(test_challenge_string)
-
-        expected_message_type = MessageTypes.NTLM_CHALLENGE
-        expected_negotiate_flags = 3800728115
-        expected_server_challenge = b"\x01\x23\x45\x67\x89\xab\xcd\xef"
-        expected_signature = NTLM_SIGNATURE
-        expected_target_info = test_target_info.pack()
-        expected_target_name = None
-        expected_version = 1080863910962135046
-
-        actual = test_ntlm_context.challenge_message
-
-        actual_message_type = actual.message_type
-        actual_negotiate_flags = actual.negotiate_flags
-        actual_server_challenge = actual.server_challenge
-        actual_signature = actual.signature
-        actual_target_info = actual.target_info.pack()
-        actual_target_name = actual.target_name
-        actual_version = actual.version
-
-        assert actual_message_type == expected_message_type
-        assert actual_negotiate_flags == expected_negotiate_flags
-        assert actual_server_challenge == expected_server_challenge
-        assert actual_signature == expected_signature
-        assert actual_target_info == expected_target_info
-        assert actual_target_name == expected_target_name
-        assert actual_version == expected_version
-
     def test_create_authenticate_message(self, monkeypatch):
         monkeypatch.setattr('os.urandom', lambda s: b"\xaa" * 8)
         monkeypatch.setattr('ntlm_auth.messages.get_version',
@@ -191,7 +142,7 @@
 
         test_challenge_string = base64.b64encode(
             b"\x4e\x54\x4c\x4d\x53\x53\x50\x00"
-            b"\x02\x00\x00\x00\x03\x00\x0c\x00"
+            b"\x02\x00\x00\x00\x2f\x82\x88\xe2"
             b"\x38\x00\x00\x00\x33\x82\x8a\xe2"
             b"\x01\x23\x45\x67\x89\xab\xcd\xef"
             b"\x00\x00\x00\x00\x00\x00\x00\x00"
@@ -204,16 +155,10 @@
             b"\x53\x00\x65\x00\x72\x00\x76\x00"
             b"\x65\x00\x72\x00\x00\x00\x00\x00"
         )
+
         test_ntlm_context = Ntlm()
         test_ntlm_context.create_negotiate_message("Domain", "COMPUTER")
         test_ntlm_context.parse_challenge_message(test_challenge_string)
-        # Need to override the flags in the challenge message to match the
-        # expectation, these flags are inconsequential and are done manually
-        # for sanity
-        test_ntlm_context.challenge_message.negotiate_flags -= \
-            NegotiateFlags.NTLMSSP_TARGET_TYPE_SERVER
-        test_ntlm_context.challenge_message.negotiate_flags |= \
-            NegotiateFlags.NTLMSSP_REQUEST_TARGET
 
         expected_message = base64.b64encode(
             b"\x4e\x54\x4c\x4d\x53\x53\x50\x00"
@@ -223,7 +168,7 @@
             b"\x48\x00\x00\x00\x08\x00\x08\x00"
             b"\x54\x00\x00\x00\x10\x00\x10\x00"
             b"\x5c\x00\x00\x00\x10\x00\x10\x00"
-            b"\xd8\x00\x00\x00\x35\x82\x88\xe2"
+            b"\xd8\x00\x00\x00\x31\x82\x8a\xe2"
             b"\x05\x01\x28\x0a\x00\x00\x00\x0f"
             b"\x44\x00\x6f\x00\x6d\x00\x61\x00"
             b"\x69\x00\x6e\x00\x55\x00\x73\x00"
@@ -250,6 +195,7 @@
         actual_message = \
             test_ntlm_context.create_authenticate_message("User", "Password",
                                                           "Domain", "COMPUTER")
+
         actual_session_security = test_ntlm_context.session_security
 
         assert actual_message == expected_message
@@ -267,7 +213,7 @@
         test_challenge_string = base64.b64encode(
             b"\x4e\x54\x4c\x4d\x53\x53\x50\x00"
             b"\x02\x00\x00\x00\x03\x00\x0c\x00"
-            b"\x38\x00\x00\x00\x33\x82\x8a\xe2"
+            b"\x38\x00\x00\x00\x03\x92\x8a\xe2"
             b"\x01\x23\x45\x67\x89\xab\xcd\xef"
             b"\x00\x00\x00\x00\x00\x00\x00\x00"
             b"\x24\x00\x24\x00\x44\x00\x00\x00"
@@ -282,20 +228,6 @@
         test_ntlm_context = Ntlm()
         test_ntlm_context.create_negotiate_message("Domain", "COMPUTER")
         test_ntlm_context.parse_challenge_message(test_challenge_string)
-        # Need to override the sign and seal flags so they don't return a
-        # security context
-        test_ntlm_context.negotiate_flags -= \
-            NegotiateFlags.NTLMSSP_NEGOTIATE_SIGN
-        test_ntlm_context.negotiate_flags -=\
-            NegotiateFlags.NTLMSSP_NEGOTIATE_SEAL
-
-        # Need to override the flags in the challenge message to match the
-        # expectation, these flags are inconsequential and are done manualy for
-        # sanity
-        test_ntlm_context.challenge_message.negotiate_flags -= \
-            NegotiateFlags.NTLMSSP_TARGET_TYPE_SERVER
-        test_ntlm_context.challenge_message.negotiate_flags |= \
-            NegotiateFlags.NTLMSSP_REQUEST_TARGET
 
         expected_message = base64.b64encode(
             b"\x4e\x54\x4c\x4d\x53\x53\x50\x00"
@@ -305,7 +237,7 @@
             b"\x48\x00\x00\x00\x08\x00\x08\x00"
             b"\x54\x00\x00\x00\x10\x00\x10\x00"
             b"\x5c\x00\x00\x00\x10\x00\x10\x00"
-            b"\xd8\x00\x00\x00\x35\x82\x88\xe2"
+            b"\xd8\x00\x00\x00\x01\x92\x8a\xe2"
             b"\x05\x01\x28\x0a\x00\x00\x00\x0f"
             b"\x44\x00\x6f\x00\x6d\x00\x61\x00"
             b"\x69\x00\x6e\x00\x55\x00\x73\x00"
@@ -337,6 +269,153 @@
         assert actual_message == expected_message
         assert actual_session_security is None
 
+        # now test the properties map up the the correct NtlmContext ones
+        assert test_ntlm_context.authenticate_message == \
+            test_ntlm_context._context._authenticate_message
+        test_ntlm_context.authenticate_message = b"1"
+        assert test_ntlm_context._context._authenticate_message == b"1"
+
+        assert test_ntlm_context.challenge_message == \
+            test_ntlm_context._context._challenge_message
+        test_ntlm_context.challenge_message = b"2"
+        assert test_ntlm_context._context._challenge_message == b"2"
+
+        assert test_ntlm_context.negotiate_flags == \
+            test_ntlm_context._context.negotiate_flags
+        test_ntlm_context.negotiate_flags = 1
+        assert test_ntlm_context._context.negotiate_flags == 1
+
+        assert test_ntlm_context.negotiate_message == \
+            test_ntlm_context._context._negotiate_message
+        test_ntlm_context.negotiate_message = b"3"
+        assert test_ntlm_context._context._negotiate_message == b"3"
+
+        assert test_ntlm_context.ntlm_compatibility == \
+            test_ntlm_context._context.ntlm_compatibility
+        test_ntlm_context.ntlm_compatibility = 2
+        assert test_ntlm_context._context.ntlm_compatibility == 2
+
+        assert test_ntlm_context.session_security == \
+            test_ntlm_context._context._session_security
+        test_ntlm_context.session_security = b"4"
+        assert test_ntlm_context._context._session_security == b"4"
+
+
+class TestNtlmContext(object):
+
+    def test_ntlm_context(self, monkeypatch):
+        monkeypatch.setattr('os.urandom', lambda s: b"\xaa" * 8)
+        monkeypatch.setattr('ntlm_auth.messages.get_version',
+                            lambda s: b"\x05\x01\x28\x0A\x00\x00\x00\x0F")
+        monkeypatch.setattr('ntlm_auth.messages.get_random_export_session_key',
+                            lambda: b"\x55" * 16)
+        monkeypatch.setattr('ntlm_auth.compute_response.get_windows_timestamp',
+                            lambda: b"\x00" * 8)
+
+        import binascii
+
+        ch = 'E3CA49271E5089CC48CE82109F1324F41DBEDDC29A777410C738F7868C4FF405'
+        cbt_data = GssChannelBindingsStruct()
+        cbt_data[cbt_data.APPLICATION_DATA] = b"tls-server-end-point:" + \
+                                              base64.b16decode(ch)
+        ntlm_context = NtlmContext("User", "Password", "Domain", "COMPUTER",
+                                   cbt_data=cbt_data)
+        actual_nego = ntlm_context.step()
+        expected_nego = b"\x4e\x54\x4c\x4d\x53\x53\x50\x00" \
+                        b"\x01\x00\x00\x00\x32\xb0\x88\xe2" \
+                        b"\x06\x00\x06\x00\x28\x00\x00\x00" \
+                        b"\x08\x00\x08\x00\x2e\x00\x00\x00" \
+                        b"\x05\x01\x28\x0a\x00\x00\x00\x0f" \
+                        b"\x44\x6f\x6d\x61\x69\x6e\x43\x4f" \
+                        b"\x4d\x50\x55\x54\x45\x52"
+        assert actual_nego == expected_nego
+        assert not ntlm_context.complete
+
+        challenge_msg = b"\x4e\x54\x4c\x4d\x53\x53\x50\x00" \
+                        b"\x02\x00\x00\x00\x2f\x82\x88\xe2" \
+                        b"\x38\x00\x00\x00\x33\x82\x8a\xe2" \
+                        b"\x01\x23\x45\x67\x89\xab\xcd\xef" \
+                        b"\x00\x00\x00\x00\x00\x00\x00\x00" \
+                        b"\x24\x00\x24\x00\x44\x00\x00\x00" \
+                        b"\x06\x00\x70\x17\x00\x00\x00\x0f" \
+                        b"\x53\x00\x65\x00\x72\x00\x76\x00" \
+                        b"\x65\x00\x72\x00\x02\x00\x0c\x00" \
+                        b"\x44\x00\x6f\x00\x6d\x00\x61\x00" \
+                        b"\x69\x00\x6e\x00\x01\x00\x0c\x00" \
+                        b"\x53\x00\x65\x00\x72\x00\x76\x00" \
+                        b"\x65\x00\x72\x00\x00\x00\x00\x00"
+        actual_auth = ntlm_context.step(challenge_msg)
+        expected_auth = b'\x4e\x54\x4c\x4d\x53\x53\x50\x00' \
+                        b'\x03\x00\x00\x00\x18\x00\x18\x00' \
+                        b'\x6c\x00\x00\x00\x68\x00\x68\x00' \
+                        b'\x84\x00\x00\x00\x0c\x00\x0c\x00' \
+                        b'\x48\x00\x00\x00\x08\x00\x08\x00' \
+                        b'\x54\x00\x00\x00\x10\x00\x10\x00' \
+                        b'\x5c\x00\x00\x00\x10\x00\x10\x00' \
+                        b'\xec\x00\x00\x00\x31\x82\x8a\xe2' \
+                        b'\x05\x01\x28\x0a\x00\x00\x00\x0f' \
+                        b'\x44\x00\x6f\x00\x6d\x00\x61\x00' \
+                        b'\x69\x00\x6e\x00\x55\x00\x73\x00' \
+                        b'\x65\x00\x72\x00\x43\x00\x4f\x00' \
+                        b'\x4d\x00\x50\x00\x55\x00\x54\x00' \
+                        b'\x45\x00\x52\x00\x86\xc3\x50\x97' \
+                        b'\xac\x9c\xec\x10\x25\x54\x76\x4a' \
+                        b'\x57\xcc\xcc\x19\xaa\xaa\xaa\xaa' \
+                        b'\xaa\xaa\xaa\xaa\x04\x10\xc4\x7a' \
+                        b'\xcf\x19\x97\x89\xde\x7f\x20\x11' \
+                        b'\x95\x7a\xea\x50\x01\x01\x00\x00' \
+                        b'\x00\x00\x00\x00\x00\x00\x00\x00' \
+                        b'\x00\x00\x00\x00\xaa\xaa\xaa\xaa' \
+                        b'\xaa\xaa\xaa\xaa\x00\x00\x00\x00' \
+                        b'\x02\x00\x0c\x00\x44\x00\x6f\x00' \
+                        b'\x6d\x00\x61\x00\x69\x00\x6e\x00' \
+                        b'\x01\x00\x0c\x00\x53\x00\x65\x00' \
+                        b'\x72\x00\x76\x00\x65\x00\x72\x00' \
+                        b'\x0a\x00\x10\x00\x6e\xa1\x9d\xf0' \
+                        b'\x66\xda\x46\x22\x05\x1f\x9c\x4f' \
+                        b'\x92\xc6\xdf\x74\x00\x00\x00\x00' \
+                        b'\x00\x00\x00\x00\xe5\x69\x95\x1d' \
+                        b'\x15\xd4\x73\x5f\x49\xe1\x4c\xf9' \
+                        b'\xa7\xd3\xe6\x72'
+
+        assert actual_auth == expected_auth
+        assert ntlm_context.complete
+
+        request_msg = b"test req"
+        response_msg = b"test res"
+        actual_wrapped = ntlm_context.wrap(request_msg)
+        expected_wrapped = b"\x01\x00\x00\x00\xbc\xe3\x23\xa1" \
+                           b"\x72\x06\x23\x78\x00\x00\x00\x00" \
+                           b"\x70\x80\x1e\x11\xfe\x6b\x3a\xad"
+        assert actual_wrapped == expected_wrapped
+
+        server_sec = SessionSecurity(
+            ntlm_context._session_security.negotiate_flags,
+            ntlm_context._session_security.exported_session_key, "server"
+        )
+        server_unwrap = server_sec.unwrap(actual_wrapped[16:],
+                                          actual_wrapped[0:16])
+        assert server_unwrap == request_msg
+
+        response_wrapped = server_sec.wrap(response_msg)
+
+        actual_unwrap = ntlm_context.unwrap(
+            response_wrapped[1] + response_wrapped[0]
+        )
+        assert actual_unwrap == response_msg
+
+    def test_fail_wrap_no_context(self):
+        ntlm_context = NtlmContext("", "")
+        with pytest.raises(NoAuthContextError) as err:
+            ntlm_context.wrap(b"")
+        assert str(err.value) == \
+            "Cannot wrap data as no security context has been established"
+
+        with pytest.raises(NoAuthContextError) as err:
+            ntlm_context.unwrap(b"")
+        assert str(err.value) == \
+            "Cannot unwrap data as no security context has been established"
+
 
 class TestNtlmFunctional(object):
     """
@@ -483,7 +562,7 @@
         assert actual_code == 200
         assert actual_content == "contents"
 
-    def test_ntlm_3_http_with_cbt(self, runner):
+    def test_ntlm_3_http_with_cbt_dep(self, runner):
         actual = self._send_request(runner[0], runner[1], runner[2], runner[3],
                                     81, 3)
         actual_content = actual.content.decode('utf-8')
@@ -492,7 +571,7 @@
         assert actual_code == 200
         assert actual_content == "contents"
 
-    def test_ntlm_3_http_without_cbt(self, runner):
+    def test_ntlm_3_http_without_cbt_dep(self, runner):
         actual = self._send_request(runner[0], runner[1], runner[2], runner[3],
                                     82, 3)
         actual_content = actual.content.decode('utf-8')
@@ -501,7 +580,7 @@
         assert actual_code == 200
         assert actual_content == "contents"
 
-    def test_ntlm_3_https_with_cbt(self, runner):
+    def test_ntlm_3_https_with_cbt_dep(self, runner):
         actual = self._send_request(runner[0], runner[1], runner[2], runner[3],
                                     441, 3)
         actual_content = actual.content.decode('utf-8')
@@ -512,7 +591,7 @@
         assert actual_code == 200
         assert actual_content == "contents"
 
-    def test_ntlm_3_https_without_cbt(self, runner):
+    def test_ntlm_3_https_without_cbt_dep(self, runner):
         actual = self._send_request(runner[0], runner[1], runner[2], runner[3],
                                     442, 3)
         actual_content = actual.content.decode('utf-8')
@@ -521,8 +600,46 @@
         assert actual_code == 200
         assert actual_content == "contents"
 
+    def test_ntlm_3_http_with_cbt(self, runner):
+        actual = self._send_request(runner[0], runner[1], runner[2], runner[3],
+                                    81, 3, legacy=False)
+        actual_content = actual.content.decode('utf-8')
+        actual_code = actual.status_code
+
+        assert actual_code == 200
+        assert actual_content == "contents"
+
+    def test_ntlm_3_http_without_cbt(self, runner):
+        actual = self._send_request(runner[0], runner[1], runner[2], runner[3],
+                                    82, 3, legacy=False)
+        actual_content = actual.content.decode('utf-8')
+        actual_code = actual.status_code
+
+        assert actual_code == 200
+        assert actual_content == "contents"
+
+    def test_ntlm_3_https_with_cbt(self, runner):
+        actual = self._send_request(runner[0], runner[1], runner[2], runner[3],
+                                    441, 3, legacy=False)
+        actual_content = actual.content.decode('utf-8')
+        actual_code = actual.status_code
+
+        # Only case where CBT should work as we are using NTLMv2 as the auth
+        # type
+        assert actual_code == 200
+        assert actual_content == "contents"
+
+    def test_ntlm_3_https_without_cbt(self, runner):
+        actual = self._send_request(runner[0], runner[1], runner[2], runner[3],
+                                    442, 3, legacy=False)
+        actual_content = actual.content.decode('utf-8')
+        actual_code = actual.status_code
+
+        assert actual_code == 200
+        assert actual_content == "contents"
+
     def _send_request(self, server, domain, username, password, port,
-                      ntlm_compatibility):
+                      ntlm_compatibility, legacy=True):
         """
         Sends a request to the url with the credentials specified. Returns the
         final response
@@ -552,7 +669,8 @@
                  server, port)
         session = requests.Session()
         session.verify = False
-        session.auth = NtlmAuth(domain, username, password, ntlm_compatibility)
+        session.auth = NtlmAuth(domain, username, password, ntlm_compatibility,
+                                legacy)
         request = requests.Request('GET', url)
         prepared_request = session.prepare_request(request)
         response = session.send(prepared_request)
@@ -563,11 +681,12 @@
 # used by the functional tests to auth with an NTLM endpoint
 class NtlmAuth(AuthBase):
 
-    def __init__(self, domain, username, password, ntlm_compatibility):
+    def __init__(self, domain, username, password, ntlm_compatibility, legacy):
         self.username = username
         self.domain = domain.upper()
         self.password = password
-        self.context = Ntlm(ntlm_compatibility=ntlm_compatibility)
+        self.ntlm_compatibility = ntlm_compatibility
+        self.legacy = legacy
 
     def __call__(self, response):
         response.headers['Connection'] = 'Keep-Alive'
@@ -576,9 +695,15 @@
 
     def hook(self, response, **kwargs):
         if response.status_code == 401:
-            return self.retry_with_ntlm_auth('www-authenticate',
-                                             'Authorization', response,
-                                             'NTLM', kwargs)
+            if self.legacy:
+                return self.retry_with_ntlm_auth_legacy('www-authenticate',
+                                                        'Authorization',
+                                                        response, 'NTLM',
+                                                        kwargs)
+            else:
+                return self.retry_with_ntlm_auth('www-authenticate',
+                                                 'Authorization', response,
+                                                 'NTLM', kwargs)
         else:
             return response
 
@@ -586,9 +711,59 @@
                              auth_type, args):
         try:
             cert_hash = self._get_server_cert(response)
+            cbt_data = GssChannelBindingsStruct()
+            cbt_data[cbt_data.APPLICATION_DATA] = b"tls-server-end-point:" + \
+                                                  base64.b16decode(cert_hash)
+        except Exception:
+            cbt_data = None
+
+        context = NtlmContext(self.username, self.password, self.domain,
+                              cbt_data=cbt_data,
+                              ntlm_compatibility=self.ntlm_compatibility)
+
+        # Consume the original response contents and release the connection for
+        # later
+        response.content
+        response.raw.release_conn()
+
+        # Create the negotiate request
+        msg1_req = response.request.copy()
+        msg1 = context.step()
+        msg1_header = "%s %s" % (auth_type, base64.b64encode(msg1).decode())
+        msg1_req.headers[auth_header] = msg1_header
+
+        # Send the negotiate request and receive the challenge message
+        disable_stream_args = dict(args, stream=False)
+        msg2_resp = response.connection.send(msg1_req, **disable_stream_args)
+        msg2_resp.content
+        msg2_resp.raw.release_conn()
+
+        # Parse the challenge response in the ntlm_context
+        msg2_header = msg2_resp.headers[auth_header_field]
+        msg2 = msg2_header.replace(auth_type + ' ', '')
+        msg3 = context.step(base64.b64decode(msg2))
+
+        # Create the authenticate request
+        msg3_req = msg2_resp.request.copy()
+        msg3_header = auth_type + ' ' + base64.b64encode(msg3).decode()
+        msg3_req.headers[auth_header] = msg3_header
+
+        # Send the authenticate request
+        final_response = msg2_resp.connection.send(msg3_req, **args)
+        final_response.history.append(response)
+        final_response.history.append(msg2_resp)
+
+        return final_response
+
+    def retry_with_ntlm_auth_legacy(self, auth_header_field, auth_header,
+                                    response, auth_type, args):
+        try:
+            cert_hash = self._get_server_cert(response)
         except Exception:
             cert_hash = None
 
+        context = Ntlm(ntlm_compatibility=self.ntlm_compatibility)
+
         # Consume the original response contents and release the connection for
         # later
         response.content
@@ -596,7 +771,7 @@
 
         # Create the negotiate request
         msg1_req = response.request.copy()
-        msg1 = self.context.create_negotiate_message(self.domain)
+        msg1 = context.create_negotiate_message(self.domain)
         msg1_header = "%s %s" % (auth_type, msg1.decode('ascii'))
         msg1_req.headers[auth_header] = msg1_header
 
@@ -609,12 +784,12 @@
         # Parse the challenge response in the ntlm_context
         msg2_header = msg2_resp.headers[auth_header_field]
         msg2 = msg2_header.replace(auth_type + ' ', '')
-        self.context.parse_challenge_message(msg2)
+        context.parse_challenge_message(msg2)
 
         # Create the authenticate request
         msg3_req = msg2_resp.request.copy()
 
-        msg3 = self.context.create_authenticate_message(
+        msg3 = context.create_authenticate_message(
             self.username, self.password, self.domain,
             server_certificate_hash=cert_hash
         )


Reply via email to