1# Copyright: (c) 2018, Jordan Borean (@jborean93) <jborean93@gmail.com>
2# MIT License (see LICENSE or https://opensource.org/licenses/MIT)
3
4import binascii
5import hashlib
6import hmac
7import struct
8
9import ntlm_auth.compute_keys as compkeys
10
11from ntlm_auth.constants import NegotiateFlags, SignSealConstants
12from ntlm_auth.rc4 import ARC4
13
14
15class _NtlmMessageSignature1(object):
16    EXPECTED_BODY_LENGTH = 16
17
18    def __init__(self, random_pad, checksum, seq_num):
19        """
20        [MS-NLMP] v28.0 2016-07-14
21
22        2.2.2.9.1 NTLMSSP_MESSAGE_SIGNATURE
23        This version of the NTLMSSP_MESSAGE_SIGNATURE structure MUST be used
24        when the NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY flag is not
25        negotiated.
26
27        :param random_pad: A 4-byte array that contains the random pad for the
28            message
29        :param checksum: A 4-byte array that contains the checksum for the
30            message
31        :param seq_num: A 32-bit unsigned integer that contains the NTLM
32            sequence number for this application message
33        """
34        self.version = b"\x01\x00\x00\x00"
35        self.random_pad = random_pad
36        self.checksum = checksum
37        self.seq_num = seq_num
38
39    def get_data(self):
40        signature = self.version
41        signature += self.random_pad
42        signature += self.checksum
43        signature += self.seq_num
44
45        assert self.EXPECTED_BODY_LENGTH == len(signature), \
46            "BODY_LENGTH: %d != signature: %d" \
47            % (self.EXPECTED_BODY_LENGTH, len(signature))
48
49        return signature
50
51
52class _NtlmMessageSignature2(object):
53    EXPECTED_BODY_LENGTH = 16
54
55    def __init__(self, checksum, seq_num):
56        """
57        [MS-NLMP] v28.0 2016-07-14
58
59        2.2.2.9.2 NTLMSSP_MESSAGE_SIGNATURE for Extended Session Security
60        This version of the NTLMSSP_MESSAGE_SIGNATURE structure MUST be used
61        when the NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY flag is negotiated
62
63        :param checksum: An 8-byte array that contains the checksum for the
64            message
65        :param seq_num: A 32-bit unsigned integer that contains the NTLM
66            sequence number for this application message
67        """
68        self.version = b"\x01\x00\x00\x00"
69        self.checksum = checksum
70        self.seq_num = seq_num
71
72    def get_data(self):
73        signature = self.version
74        signature += self.checksum
75        signature += self.seq_num
76
77        assert self.EXPECTED_BODY_LENGTH == len(signature),\
78            "BODY_LENGTH: %d != signature: %d"\
79            % (self.EXPECTED_BODY_LENGTH, len(signature))
80
81        return signature
82
83
84class SessionSecurity(object):
85
86    def __init__(self, negotiate_flags, exported_session_key, source="client"):
87        """
88        Initialises a security session context that can be used by libraries
89        that call ntlm-auth to sign and seal messages send to the server as
90        well as verify and unseal messages that have been received from the
91        server. This is similar to the GSS_Wrap functions specified in the
92        MS-NLMP document which does the same task.
93
94        :param negotiate_flags: The negotiate flag structure that has been
95            negotiated with the server
96        :param exported_session_key: A 128-bit session key used to derive
97            signing and sealing keys
98        :param source: The source of the message, only used in test scenarios
99            when testing out a server sealing and unsealing
100        """
101        self.negotiate_flags = negotiate_flags
102        self.exported_session_key = exported_session_key
103        self.outgoing_seq_num = 0
104        self.incoming_seq_num = 0
105        self._source = source
106        self._client_sealing_key = compkeys.get_seal_key(self.negotiate_flags, exported_session_key,
107                                                         SignSealConstants.CLIENT_SEALING)
108        self._server_sealing_key = compkeys.get_seal_key(self.negotiate_flags, exported_session_key,
109                                                         SignSealConstants.SERVER_SEALING)
110
111        self.outgoing_handle = None
112        self.incoming_handle = None
113        self.reset_rc4_state(True)
114        self.reset_rc4_state(False)
115
116        if source == "client":
117            self.outgoing_signing_key = compkeys.get_sign_key(exported_session_key, SignSealConstants.CLIENT_SIGNING)
118            self.incoming_signing_key = compkeys.get_sign_key(exported_session_key, SignSealConstants.SERVER_SIGNING)
119        elif source == "server":
120            self.outgoing_signing_key = compkeys.get_sign_key(exported_session_key, SignSealConstants.SERVER_SIGNING)
121            self.incoming_signing_key = compkeys.get_sign_key(exported_session_key, SignSealConstants.CLIENT_SIGNING)
122        else:
123            raise ValueError("Invalid source parameter %s, must be client "
124                             "or server" % source)
125
126    def reset_rc4_state(self, outgoing=True):
127        csk = self._client_sealing_key
128        ssk = self._server_sealing_key
129        if outgoing:
130            self.outgoing_handle = ARC4(csk if self._source == 'client' else ssk)
131        else:
132            self.incoming_handle = ARC4(ssk if self._source == 'client' else csk)
133
134    def wrap(self, message):
135        """
136        [MS-NLMP] v28.0 2016-07-14
137
138        3.4.6 GSS_WrapEx()
139        Emulates the GSS_Wrap() implementation to sign and seal messages if the
140        correct flags are set.
141
142        :param message: The message data that will be wrapped
143        :return message: The message that has been sealed if flags are set
144        :return signature: The signature of the message, None if flags are not
145            set
146        """
147        if self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_SEAL:
148            encrypted_message = self._seal_message(message)
149            signature = self.get_signature(message)
150            message = encrypted_message
151
152        elif self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_SIGN:
153            signature = self.get_signature(message)
154        else:
155            signature = None
156
157        return message, signature
158
159    def unwrap(self, message, signature):
160        """
161        [MS-NLMP] v28.0 2016-07-14
162
163        3.4.7 GSS_UnwrapEx()
164        Emulates the GSS_Unwrap() implementation to unseal messages and verify
165        the signature sent matches what has been computed locally. Will throw
166        an Exception if the signature doesn't match
167
168        :param message: The message data received from the server
169        :param signature: The signature of the message
170        :return message: The message that has been unsealed if flags are set
171        """
172        if self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_SEAL:
173            message = self._unseal_message(message)
174            self.verify_signature(message, signature)
175
176        elif self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_SIGN:
177            self.verify_signature(message, signature)
178
179        return message
180
181    def _seal_message(self, message):
182        """
183        [MS-NLMP] v28.0 2016-07-14
184
185        3.4.3 Message Confidentiality
186        Will generate an encrypted message using RC4 based on the
187        ClientSealingKey
188
189        :param message: The message to be sealed (encrypted)
190        :return encrypted_message: The encrypted message
191        """
192        encrypted_message = self.outgoing_handle.update(message)
193        return encrypted_message
194
195    def _unseal_message(self, message):
196        """
197        [MS-NLMP] v28.0 2016-07-14
198
199        3.4.3 Message Confidentiality
200        Will generate a dencrypted message using RC4 based on the
201        ServerSealingKey
202
203        :param message: The message to be unsealed (dencrypted)
204        :return decrypted_message: The decrypted message
205        """
206        decrypted_message = self.incoming_handle.update(message)
207        return decrypted_message
208
209    def get_signature(self, message):
210        """
211        [MS-NLMP] v28.0 2016-07-14
212
213        3.4.4 Message Signature Functions
214        Will create the signature based on the message to send to the server.
215        Depending on the negotiate_flags set this could either be an NTLMv1
216        signature or NTLMv2 with Extended Session Security signature.
217
218        :param message: The message data that will be signed
219        :return signature: Either _NtlmMessageSignature1 or
220            _NtlmMessageSignature2 depending on the flags set
221        """
222        signature = calc_signature(message, self.negotiate_flags,
223                                   self.outgoing_signing_key,
224                                   self.outgoing_seq_num, self.outgoing_handle)
225        self.outgoing_seq_num += 1
226
227        return signature.get_data()
228
229    def verify_signature(self, message, signature):
230        """
231        Will verify that the signature received from the server matches up with
232        the expected signature computed locally. Will throw an exception if
233        they do not match
234
235        :param message: The message data that is received from the server
236        :param signature: The signature of the message received from the server
237        """
238        if self.negotiate_flags & \
239                NegotiateFlags.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY:
240            actual_checksum = signature[4:12]
241            actual_seq_num = struct.unpack("<I", signature[12:16])[0]
242        else:
243            actual_checksum = signature[8:12]
244            actual_seq_num = struct.unpack("<I", signature[12:16])[0]
245
246        expected_signature = calc_signature(message, self.negotiate_flags,
247                                            self.incoming_signing_key,
248                                            self.incoming_seq_num,
249                                            self.incoming_handle)
250        expected_checksum = expected_signature.checksum
251        expected_seq_num = struct.unpack("<I", expected_signature.seq_num)[0]
252
253        if actual_checksum != expected_checksum:
254            raise Exception("The signature checksum does not match, message "
255                            "has been altered")
256
257        if actual_seq_num != expected_seq_num:
258            raise Exception("The signature sequence number does not match up, "
259                            "message not received in the correct sequence")
260
261        self.incoming_seq_num += 1
262
263
264def calc_signature(message, negotiate_flags, signing_key, seq_num, handle):
265    seq_num = struct.pack("<I", seq_num)
266    if negotiate_flags & \
267            NegotiateFlags.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY:
268        checksum_hmac = hmac.new(signing_key, seq_num + message,
269                                 digestmod=hashlib.md5)
270        if negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_KEY_EXCH:
271            checksum = handle.update(checksum_hmac.digest()[:8])
272        else:
273            checksum = checksum_hmac.digest()[:8]
274
275        signature = _NtlmMessageSignature2(checksum, seq_num)
276
277    else:
278        message_crc = binascii.crc32(message) % (1 << 32)
279        checksum = struct.pack("<I", message_crc)
280        random_pad = handle.update(struct.pack("<I", 0))
281        checksum = handle.update(checksum)
282        seq_num = handle.update(seq_num)
283        random_pad = struct.pack("<I", 0)
284
285        signature = _NtlmMessageSignature1(random_pad, checksum, seq_num)
286
287    return signature
288