327fe5a4fdee533d5136f4ef378d9dd6b75f2e16
[samba.git] / python / samba / tests / samba_tool / user_wdigest.py
1 # Tests for the samba-tool user sub command reading Primary:WDigest
2 #
3 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017
4 #
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19
20 import os
21 import time
22 import base64
23 import ldb
24 import samba
25 from samba.tests.samba_tool.base import SambaToolCmdTest
26 from samba import (
27         credentials,
28         nttime2unix,
29         dsdb
30         )
31 from samba.ndr import ndr_unpack
32 from samba.dcerpc import drsblobs
33 from hashlib import md5
34 import random
35 import string
36 from samba.compat import text_type
37
38 USER_NAME = "WdigestTestUser"
39 # Create a random 32 character password, containing only letters and
40 # digits to avoid issues when used on the command line.
41 USER_PASS = ''.join(random.choice(string.ascii_uppercase +
42                                   string.ascii_lowercase +
43                                   string.digits) for _ in range(32))
44
45 # Calculate the MD5 password digest from the supplied user, realm and password
46 #
47 def calc_digest(user, realm, password):
48     data = "%s:%s:%s" % (user, realm, password)
49     if isinstance(data, text_type):
50         data = data.encode('utf8')
51
52     return "%s:%s:%s" % (user, realm, md5(data).hexdigest())
53
54
55 class UserCmdWdigestTestCase(SambaToolCmdTest):
56     """Tests for samba-tool user subcommands extraction of the wdigest values
57        Test results validated against Windows Server 2012 R2.
58        NOTE: That as at 22-05-2017 the values Documented at
59              3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
60              are incorrect.
61     """
62     users = []
63     samdb = None
64
65     def setUp(self):
66         super(UserCmdWdigestTestCase, self).setUp()
67         self.lp = samba.tests.env_loadparm()
68         self.samdb = self.getSamDB(
69             "-H", "ldap://%s" % os.environ["DC_SERVER"],
70             "-U%s%%%s" % (os.environ["DC_USERNAME"],
71                           os.environ["DC_PASSWORD"]))
72         self.dns_domain = self.samdb.domain_dns_name()
73         res = self.samdb.search(
74             base=self.samdb.get_config_basedn(),
75             expression="ncName=%s" % self.samdb.get_default_basedn(),
76             attrs=["nETBIOSName"])
77         self.netbios_domain = res[0]["nETBIOSName"][0]
78         self.runsubcmd("user",
79                        "create",
80                        USER_NAME,
81                        USER_PASS,
82                        "-H",
83                        "ldap://%s" % os.environ["DC_SERVER"],
84                        "-U%s%%%s" % (
85                             os.environ["DC_USERNAME"],
86                             os.environ["DC_PASSWORD"]))
87
88     def tearDown(self):
89         super(UserCmdWdigestTestCase, self).tearDown()
90         self.runsubcmd("user", "delete", USER_NAME)
91
92     def _testWDigest(self,  attribute, expected, missing=False):
93
94         (result, out, err) = self.runsubcmd("user",
95                                             "getpassword",
96                                             USER_NAME,
97                                             "--attributes",
98                                             attribute)
99         self.assertCmdSuccess(result,
100                               out,
101                               err,
102                               "Ensure getpassword runs")
103         self.assertEqual(err, "", "getpassword")
104         self.assertMatch(out,
105                          "Got password OK",
106                          "getpassword out[%s]" % out)
107
108         if missing:
109             self.assertTrue(attribute not in out)
110         else:
111             self.assertMatch(out.replace('\n ', ''),
112                              "%s: %s" % (attribute, expected))
113
114     def test_Wdigest_no_suffix(self):
115         attribute = "virtualWDigest"
116         self._testWDigest(attribute, None, True)
117
118     def test_Wdigest_non_numeric_suffix(self):
119         attribute = "virtualWDigestss"
120         self._testWDigest(attribute, None, True)
121
122     def test_Wdigest00(self):
123         attribute = "virtualWDigest00"
124         self._testWDigest(attribute, None, True)
125
126     # Hash01  MD5(sAMAccountName,
127     #            NETBIOSDomainName,
128     #            password)
129     #
130     def test_Wdigest01(self):
131         attribute = "virtualWDigest01"
132         expected = calc_digest(USER_NAME,
133                                self.netbios_domain,
134                                USER_PASS)
135         self._testWDigest(attribute, expected)
136
137     # Hash02 MD5(LOWER(sAMAccountName),
138     #            LOWER(NETBIOSDomainName),
139     #            password)
140     #
141     def test_Wdigest02(self):
142         attribute = "virtualWDigest02"
143         expected = calc_digest(USER_NAME.lower(),
144                                self.netbios_domain.lower(),
145                                USER_PASS)
146         self._testWDigest(attribute, expected)
147
148     # Hash03 MD5(UPPER(sAMAccountName),
149     #            UPPER(NETBIOSDomainName),
150     #            password)
151     #
152     def test_Wdigest03(self):
153         attribute = "virtualWDigest03"
154         expected = calc_digest(USER_NAME.upper(),
155                                self.netbios_domain.upper(),
156                                USER_PASS)
157         self._testWDigest(attribute, expected)
158
159     # Hash04 MD5(sAMAccountName,
160     #            UPPER(NETBIOSDomainName),
161     #            password)
162     #
163     def test_Wdigest04(self):
164         attribute = "virtualWDigest04"
165         expected = calc_digest(USER_NAME,
166                                self.netbios_domain.upper(),
167                                USER_PASS)
168         self._testWDigest(attribute, expected)
169
170     # Hash05 MD5(sAMAccountName,
171     #            LOWER(NETBIOSDomainName),
172     #            password)
173     #
174     def test_Wdigest05(self):
175         attribute = "virtualWDigest05"
176         expected = calc_digest(USER_NAME,
177                                self.netbios_domain.lower(),
178                                USER_PASS)
179         self._testWDigest(attribute, expected)
180
181     # Hash06 MD5(UPPER(sAMAccountName),
182     #            LOWER(NETBIOSDomainName),
183     #            password)
184     #
185     def test_Wdigest06(self):
186         attribute = "virtualWDigest06"
187         expected = calc_digest(USER_NAME.upper(),
188                                self.netbios_domain.lower(),
189                                USER_PASS)
190         self._testWDigest(attribute, expected)
191
192     # Hash07 MD5(LOWER(sAMAccountName),
193     #            UPPER(NETBIOSDomainName),
194     #            password)
195     #
196     def test_Wdigest07(self):
197         attribute = "virtualWDigest07"
198         expected = calc_digest(USER_NAME.lower(),
199                                self.netbios_domain.upper(),
200                                USER_PASS)
201         self._testWDigest(attribute, expected)
202
203     # Hash08 MD5(sAMAccountName,
204     #            DNSDomainName,
205     #            password)
206     #
207     # Note: Samba lowercases the DNSDomainName at provision time,
208     #       Windows preserves the case. This means that the WDigest08 values
209     #       calculated byt Samba and Windows differ.
210     #
211     def test_Wdigest08(self):
212         attribute = "virtualWDigest08"
213         expected = calc_digest(USER_NAME,
214                                self.dns_domain,
215                                USER_PASS)
216         self._testWDigest(attribute, expected)
217
218     # Hash09 MD5(LOWER(sAMAccountName),
219     #            LOWER(DNSDomainName),
220     #            password)
221     #
222     def test_Wdigest09(self):
223         attribute = "virtualWDigest09"
224         expected = calc_digest(USER_NAME.lower(),
225                                self.dns_domain.lower(),
226                                USER_PASS)
227         self._testWDigest(attribute, expected)
228
229     # Hash10 MD5(UPPER(sAMAccountName),
230     #            UPPER(DNSDomainName),
231     #            password)
232     #
233     def test_Wdigest10(self):
234         attribute = "virtualWDigest10"
235         expected = calc_digest(USER_NAME.upper(),
236                                self.dns_domain.upper(),
237                                USER_PASS)
238         self._testWDigest(attribute, expected)
239
240     # Hash11 MD5(sAMAccountName,
241     #            UPPER(DNSDomainName),
242     #            password)
243     #
244     def test_Wdigest11(self):
245         attribute = "virtualWDigest11"
246         expected = calc_digest(USER_NAME,
247                                self.dns_domain.upper(),
248                                USER_PASS)
249         self._testWDigest(attribute, expected)
250
251     # Hash12 MD5(sAMAccountName,
252     #            LOWER(DNSDomainName),
253     #            password)
254     #
255     def test_Wdigest12(self):
256         attribute = "virtualWDigest12"
257         expected = calc_digest(USER_NAME,
258                                self.dns_domain.lower(),
259                                USER_PASS)
260         self._testWDigest(attribute, expected)
261
262     # Hash13 MD5(UPPER(sAMAccountName),
263     #            LOWER(DNSDomainName),
264     #            password)
265     #
266     def test_Wdigest13(self):
267         attribute = "virtualWDigest13"
268         expected = calc_digest(USER_NAME.upper(),
269                                self.dns_domain.lower(),
270                                USER_PASS)
271         self._testWDigest(attribute, expected)
272
273
274     # Hash14 MD5(LOWER(sAMAccountName),
275     #            UPPER(DNSDomainName),
276     #            password)
277     #
278     def test_Wdigest14(self):
279         attribute = "virtualWDigest14"
280         expected = calc_digest(USER_NAME.lower(),
281                                self.dns_domain.upper(),
282                                USER_PASS)
283         self._testWDigest(attribute, expected)
284
285     # Hash15 MD5(userPrincipalName,
286     #            password)
287     #
288     def test_Wdigest15(self):
289         attribute = "virtualWDigest15"
290         name = "%s@%s" % (USER_NAME, self.dns_domain)
291         expected = calc_digest(name,
292                                "",
293                                USER_PASS)
294         self._testWDigest(attribute, expected)
295
296     # Hash16 MD5(LOWER(userPrincipalName),
297     #            password)
298     #
299     def test_Wdigest16(self):
300         attribute = "virtualWDigest16"
301         name = "%s@%s" % (USER_NAME.lower(), self.dns_domain.lower())
302         expected = calc_digest(name,
303                                "",
304                                USER_PASS)
305         self._testWDigest(attribute, expected)
306
307     # Hash17 MD5(UPPER(userPrincipalName),
308     #            password)
309     #
310     def test_Wdigest17(self):
311         attribute = "virtualWDigest17"
312         name = "%s@%s" % (USER_NAME.upper(), self.dns_domain.upper())
313         expected = calc_digest(name,
314                                "",
315                                USER_PASS)
316         self._testWDigest(attribute, expected)
317
318     # Hash18 MD5(NETBIOSDomainName\sAMAccountName,
319     #            password)
320     #
321     def test_Wdigest18(self):
322         attribute = "virtualWDigest18"
323         name = "%s\\%s" % (self.netbios_domain, USER_NAME)
324         expected = calc_digest(name,
325                                "",
326                                USER_PASS)
327         self._testWDigest(attribute, expected)
328
329     # Hash19 MD5(LOWER(NETBIOSDomainName\sAMAccountName),
330     #            password)
331     #
332     def test_Wdigest19(self):
333         attribute = "virtualWDigest19"
334         name = "%s\\%s" % (self.netbios_domain, USER_NAME)
335         expected = calc_digest(name.lower(),
336                                "",
337                                USER_PASS)
338         self._testWDigest(attribute, expected)
339
340     # Hash20 MD5(UPPER(NETBIOSDomainName\sAMAccountName),
341     #            password)
342     #
343     def test_Wdigest20(self):
344         attribute = "virtualWDigest20"
345         name = "%s\\%s" % (self.netbios_domain, USER_NAME)
346         expected = calc_digest(name.upper(),
347                                "",
348                                USER_PASS)
349         self._testWDigest(attribute, expected)
350
351     # Hash21 MD5(sAMAccountName,
352     #            "Digest",
353     #            password)
354     #
355     def test_Wdigest21(self):
356         attribute = "virtualWDigest21"
357         expected = calc_digest(USER_NAME,
358                                "Digest",
359                                USER_PASS)
360         self._testWDigest(attribute, expected)
361
362     # Hash22 MD5(LOWER(sAMAccountName),
363     #            "Digest",
364     #            password)
365     #
366     def test_Wdigest22(self):
367         attribute = "virtualWDigest22"
368         expected = calc_digest(USER_NAME.lower(),
369                                "Digest",
370                                USER_PASS)
371         self._testWDigest(attribute, expected)
372
373     # Hash23 MD5(UPPER(sAMAccountName),
374     #            "Digest",
375     #            password)
376     #
377     def test_Wdigest23(self):
378         attribute = "virtualWDigest23"
379         expected = calc_digest(USER_NAME.upper(),
380                                "Digest",
381                                USER_PASS)
382         self._testWDigest(attribute, expected)
383
384     # Hash24  MD5(userPrincipalName),
385     #             "Digest",
386     #              password)
387     #
388     def test_Wdigest24(self):
389         attribute = "virtualWDigest24"
390         name = "%s@%s" % (USER_NAME, self.dns_domain)
391         expected = calc_digest(name,
392                                "Digest",
393                                USER_PASS)
394         self._testWDigest(attribute, expected)
395
396     # Hash25 MD5(LOWER(userPrincipalName),
397     #            "Digest",
398     #            password)
399     #
400     def test_Wdigest25(self):
401         attribute = "virtualWDigest25"
402         name = "%s@%s" % (USER_NAME, self.dns_domain.lower())
403         expected = calc_digest(name.lower(),
404                                "Digest",
405                                USER_PASS)
406         self._testWDigest(attribute, expected)
407
408     # Hash26 MD5(UPPER(userPrincipalName),
409     #            "Digest",
410     #             password)
411     #
412     def test_Wdigest26(self):
413         attribute = "virtualWDigest26"
414         name = "%s@%s" % (USER_NAME, self.dns_domain.lower())
415         expected = calc_digest(name.upper(),
416                                "Digest",
417                                USER_PASS)
418         self._testWDigest(attribute, expected)
419     # Hash27 MD5(NETBIOSDomainName\sAMAccountName,
420     #            "Digest",
421     #            password)
422     #
423     def test_Wdigest27(self):
424         attribute = "virtualWDigest27"
425         name = "%s\\%s" % (self.netbios_domain, USER_NAME)
426         expected = calc_digest(name,
427                                "Digest",
428                                USER_PASS)
429         self._testWDigest(attribute, expected)
430
431     # Hash28 MD5(LOWER(NETBIOSDomainName\sAMAccountName),
432     #            "Digest",
433     #            password)
434     #
435     def test_Wdigest28(self):
436         attribute = "virtualWDigest28"
437         name = "%s\\%s" % (self.netbios_domain.lower(), USER_NAME.lower())
438         expected = calc_digest(name,
439                                "Digest",
440                                USER_PASS)
441         self._testWDigest(attribute, expected)
442
443     # Hash29 MD5(UPPER(NETBIOSDomainName\sAMAccountName),
444     #            "Digest",
445     #             password)
446     #
447     def test_Wdigest29(self):
448         attribute = "virtualWDigest29"
449         name = "%s\\%s" % (self.netbios_domain.upper(), USER_NAME.upper())
450         expected = calc_digest(name,
451                                "Digest",
452                                USER_PASS)
453         self._testWDigest(attribute, expected)
454
455     def test_Wdigest30(self):
456         attribute = "virtualWDigest30"
457         self._testWDigest(attribute, None, True)
458
459     # Check digest calculation against an known htdigest value
460     def test_calc_digest(self):
461         htdigest = "gary:fred:2204fcc247cb47ded249ef2fe0013255"
462         digest = calc_digest("gary", "fred", "password")
463         self.assertEqual(htdigest, digest)