1 # Tests for Tests for source4/dsdb/samdb/ldb_modules/password_hash.c
3 # Copyright (C) Catalyst IT Ltd. 2017
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 Base class for tests for source4/dsdb/samdb/ldb_modules/password_hash.c
23 from samba
.credentials
import Credentials
24 from samba
.samdb
import SamDB
25 from samba
.auth
import system_session
26 from samba
.tests
import TestCase
27 from samba
.ndr
import ndr_unpack
28 from samba
.dcerpc
import drsblobs
29 from samba
.dcerpc
.samr
import DOMAIN_PASSWORD_STORE_CLEARTEXT
30 from samba
.dsdb
import UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED
31 from samba
.tests
import delete_force
32 from samba
.tests
.password_test
import PasswordCommon
36 from hashlib
import md5
40 USER_NAME
= "PasswordHashTestUser"
41 USER_PASS
= samba
.generate_random_password(32, 32)
42 UPN
= "PWHash@User.Principle"
44 # Get named package from the passed supplemental credentials
46 # returns the package and it's position within the supplemental credentials
49 def get_package(sc
, name
):
54 for p
in sc
.sub
.packages
:
61 # Calculate the MD5 password digest from the supplied user, realm and password
65 def calc_digest(user
, realm
, password
):
67 data
= "%s:%s:%s" % (user
, realm
, password
)
68 if isinstance(data
, str):
69 data
= data
.encode('utf8')
71 return md5(data
).hexdigest()
74 class PassWordHashTests(TestCase
):
77 self
.lp
= samba
.tests
.env_loadparm()
78 super(PassWordHashTests
, self
).setUp()
80 def set_store_cleartext(self
, cleartext
):
81 # get the current pwdProperties
82 pwdProperties
= self
.ldb
.get_pwdProperties()
83 # update the clear-text properties flag
84 props
= int(pwdProperties
)
86 props |
= DOMAIN_PASSWORD_STORE_CLEARTEXT
88 props
&= ~DOMAIN_PASSWORD_STORE_CLEARTEXT
89 self
.ldb
.set_pwdProperties(str(props
))
91 # Add a user to ldb, this will exercise the password_hash code
92 # and calculate the appropriate supplemental credentials
93 def add_user(self
, options
=None, clear_text
=False, ldb
=None):
94 # set any needed options
95 if options
is not None:
96 for (option
, value
) in options
:
97 self
.lp
.set(option
, value
)
100 self
.creds
= Credentials()
101 self
.session
= system_session()
102 self
.creds
.guess(self
.lp
)
103 self
.session
= system_session()
104 self
.ldb
= SamDB(session_info
=self
.session
,
105 credentials
=self
.creds
,
110 res
= self
.ldb
.search(base
=self
.ldb
.get_config_basedn(),
111 expression
="ncName=%s" % self
.ldb
.get_default_basedn(),
112 attrs
=["nETBIOSName"])
113 self
.netbios_domain
= str(res
[0]["nETBIOSName"][0])
114 self
.dns_domain
= self
.ldb
.domain_dns_name()
116 # Gets back the basedn
117 base_dn
= self
.ldb
.domain_dn()
119 # Gets back the configuration basedn
120 configuration_dn
= self
.ldb
.get_config_basedn().get_linearized()
122 # permit password changes during this test
123 PasswordCommon
.allow_password_changes(self
, self
.ldb
)
125 self
.base_dn
= self
.ldb
.domain_dn()
129 # Restore the current domain setting on exit.
130 pwdProperties
= self
.ldb
.get_pwdProperties()
131 self
.addCleanup(self
.ldb
.set_pwdProperties
, pwdProperties
)
132 # Update the domain setting
133 self
.set_store_cleartext(clear_text
)
134 account_control |
= UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED
136 # (Re)adds the test user USER_NAME with password USER_PASS
137 # and userPrincipalName UPN
138 delete_force(self
.ldb
, "cn=" + USER_NAME
+ ",cn=users," + self
.base_dn
)
140 "dn": "cn=" + USER_NAME
+ ",cn=users," + self
.base_dn
,
141 "objectclass": "user",
142 "sAMAccountName": USER_NAME
,
143 "userPassword": USER_PASS
,
144 "userPrincipalName": UPN
,
145 "userAccountControl": str(account_control
)
148 # Get the supplemental credentials for the user under test
149 def get_supplemental_creds(self
):
150 base
= "cn=" + USER_NAME
+ ",cn=users," + self
.base_dn
151 res
= self
.ldb
.search(scope
=ldb
.SCOPE_BASE
,
153 attrs
=["supplementalCredentials"])
154 self
.assertIs(True, len(res
) > 0)
156 sc_blob
= obj
["supplementalCredentials"][0]
157 sc
= ndr_unpack(drsblobs
.supplementalCredentialsBlob
, sc_blob
)
160 # Calculate and validate a Wdigest value
161 def check_digest(self
, user
, realm
, password
, digest
):
162 expected
= calc_digest(user
, realm
, password
)
163 actual
= binascii
.hexlify(bytearray(digest
)).decode('utf8')
164 error
= "Digest expected[%s], actual[%s], " \
165 "user[%s], realm[%s], pass[%s]" % \
166 (expected
, actual
, user
, realm
, password
)
167 self
.assertEqual(expected
, actual
, error
)
169 # Check all of the 29 expected WDigest values
171 def check_wdigests(self
, digests
):
173 self
.assertEqual(29, digests
.num_hashes
)
175 # Using the n-1 pattern in the array indexes to make it easier
176 # to check the tests against the spec and the samba-tool user tests.
177 self
.check_digest(USER_NAME
,
180 digests
.hashes
[1 - 1].hash)
181 self
.check_digest(USER_NAME
.lower(),
182 self
.netbios_domain
.lower(),
184 digests
.hashes
[2 - 1].hash)
185 self
.check_digest(USER_NAME
.upper(),
186 self
.netbios_domain
.upper(),
188 digests
.hashes
[3 - 1].hash)
189 self
.check_digest(USER_NAME
,
190 self
.netbios_domain
.upper(),
192 digests
.hashes
[4 - 1].hash)
193 self
.check_digest(USER_NAME
,
194 self
.netbios_domain
.lower(),
196 digests
.hashes
[5 - 1].hash)
197 self
.check_digest(USER_NAME
.upper(),
198 self
.netbios_domain
.lower(),
200 digests
.hashes
[6 - 1].hash)
201 self
.check_digest(USER_NAME
.lower(),
202 self
.netbios_domain
.upper(),
204 digests
.hashes
[7 - 1].hash)
205 self
.check_digest(USER_NAME
,
208 digests
.hashes
[8 - 1].hash)
209 self
.check_digest(USER_NAME
.lower(),
210 self
.dns_domain
.lower(),
212 digests
.hashes
[9 - 1].hash)
213 self
.check_digest(USER_NAME
.upper(),
214 self
.dns_domain
.upper(),
216 digests
.hashes
[10 - 1].hash)
217 self
.check_digest(USER_NAME
,
218 self
.dns_domain
.upper(),
220 digests
.hashes
[11 - 1].hash)
221 self
.check_digest(USER_NAME
,
222 self
.dns_domain
.lower(),
224 digests
.hashes
[12 - 1].hash)
225 self
.check_digest(USER_NAME
.upper(),
226 self
.dns_domain
.lower(),
228 digests
.hashes
[13 - 1].hash)
229 self
.check_digest(USER_NAME
.lower(),
230 self
.dns_domain
.upper(),
232 digests
.hashes
[14 - 1].hash)
233 self
.check_digest(UPN
,
236 digests
.hashes
[15 - 1].hash)
237 self
.check_digest(UPN
.lower(),
240 digests
.hashes
[16 - 1].hash)
241 self
.check_digest(UPN
.upper(),
244 digests
.hashes
[17 - 1].hash)
246 name
= "%s\\%s" % (self
.netbios_domain
, USER_NAME
)
247 self
.check_digest(name
,
250 digests
.hashes
[18 - 1].hash)
252 name
= "%s\\%s" % (self
.netbios_domain
.lower(), USER_NAME
.lower())
253 self
.check_digest(name
,
256 digests
.hashes
[19 - 1].hash)
258 name
= "%s\\%s" % (self
.netbios_domain
.upper(), USER_NAME
.upper())
259 self
.check_digest(name
,
262 digests
.hashes
[20 - 1].hash)
263 self
.check_digest(USER_NAME
,
266 digests
.hashes
[21 - 1].hash)
267 self
.check_digest(USER_NAME
.lower(),
270 digests
.hashes
[22 - 1].hash)
271 self
.check_digest(USER_NAME
.upper(),
274 digests
.hashes
[23 - 1].hash)
275 self
.check_digest(UPN
,
278 digests
.hashes
[24 - 1].hash)
279 self
.check_digest(UPN
.lower(),
282 digests
.hashes
[25 - 1].hash)
283 self
.check_digest(UPN
.upper(),
286 digests
.hashes
[26 - 1].hash)
287 name
= "%s\\%s" % (self
.netbios_domain
, USER_NAME
)
288 self
.check_digest(name
,
291 digests
.hashes
[27 - 1].hash)
293 name
= "%s\\%s" % (self
.netbios_domain
.lower(), USER_NAME
.lower())
294 self
.check_digest(name
,
297 digests
.hashes
[28 - 1].hash)
299 name
= "%s\\%s" % (self
.netbios_domain
.upper(), USER_NAME
.upper())
300 self
.check_digest(name
,
303 digests
.hashes
[29 - 1].hash)
305 def checkUserPassword(self
, up
, expected
):
307 # Check we've received the correct number of hashes
308 self
.assertEqual(len(expected
), up
.num_hashes
)
311 for (tag
, alg
, rounds
) in expected
:
312 self
.assertEqual(tag
, up
.hashes
[i
].scheme
)
314 data
= up
.hashes
[i
].value
.decode('utf8').split("$")
315 # Check we got the expected crypt algorithm
316 self
.assertEqual(alg
, data
[1])
319 cmd
= "$%s$%s" % (alg
, data
[2])
321 cmd
= "$%s$rounds=%d$%s" % (alg
, rounds
, data
[3])
323 # Calculate the expected hash value
324 expected
= crypt
.crypt(USER_PASS
, cmd
)
325 self
.assertEqual(expected
, up
.hashes
[i
].value
.decode('utf8'))
328 # Check that the correct nt_hash was stored for userPassword
329 def checkNtHash(self
, password
, nt_hash
):
330 creds
= Credentials()
331 creds
.set_anonymous()
332 creds
.set_password(password
)
333 expected
= creds
.get_nt_hash()
334 actual
= bytearray(nt_hash
)
335 self
.assertEqual(expected
, actual
)