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
39 USER_NAME
= "PasswordHashTestUser"
40 USER_PASS
= samba
.generate_random_password(32, 32)
41 UPN
= "PWHash@User.Principle"
43 # Get named package from the passed supplemental credentials
45 # returns the package and it's position within the supplemental credentials
46 def get_package(sc
, name
):
51 for p
in sc
.sub
.packages
:
58 # Calculate the MD5 password digest from the supplied user, realm and password
60 def calc_digest(user
, realm
, password
):
62 data
= "%s:%s:%s" % (user
, realm
, password
)
63 return binascii
.hexlify(md5
.new(data
).digest())
66 class PassWordHashTests(TestCase
):
69 self
.lp
= samba
.tests
.env_loadparm()
70 super(PassWordHashTests
, self
).setUp()
72 # Add a user to ldb, this will exercise the password_hash code
73 # and calculate the appropriate supplemental credentials
74 def add_user(self
, options
=None, clear_text
=False, ldb
=None):
75 # set any needed options
76 if options
is not None:
77 for (option
, value
) in options
:
78 self
.lp
.set(option
, value
)
81 self
.creds
= Credentials()
82 self
.session
= system_session()
83 self
.creds
.guess(self
.lp
)
84 self
.session
= system_session()
85 self
.ldb
= SamDB(session_info
=self
.session
,
86 credentials
=self
.creds
,
91 res
= self
.ldb
.search(base
=self
.ldb
.get_config_basedn(),
92 expression
="ncName=%s" % self
.ldb
.get_default_basedn(),
93 attrs
=["nETBIOSName"])
94 self
.netbios_domain
= res
[0]["nETBIOSName"][0]
95 self
.dns_domain
= self
.ldb
.domain_dns_name()
98 # Gets back the basedn
99 base_dn
= self
.ldb
.domain_dn()
101 # Gets back the configuration basedn
102 configuration_dn
= self
.ldb
.get_config_basedn().get_linearized()
104 # Get the old "dSHeuristics" if it was set
105 dsheuristics
= self
.ldb
.get_dsheuristics()
107 # Set the "dSHeuristics" to activate the correct "userPassword"
109 self
.ldb
.set_dsheuristics("000000001")
111 # Reset the "dSHeuristics" as they were before
112 self
.addCleanup(self
.ldb
.set_dsheuristics
, dsheuristics
)
114 # Get the old "minPwdAge"
115 minPwdAge
= self
.ldb
.get_minPwdAge()
117 # Set it temporarily to "0"
118 self
.ldb
.set_minPwdAge("0")
119 self
.base_dn
= self
.ldb
.domain_dn()
121 # Reset the "minPwdAge" as it was before
122 self
.addCleanup(self
.ldb
.set_minPwdAge
, minPwdAge
)
126 # get the current pwdProperties
127 pwdProperties
= self
.ldb
.get_pwdProperties()
128 # enable clear text properties
129 props
= int(pwdProperties
)
130 props |
= DOMAIN_PASSWORD_STORE_CLEARTEXT
131 self
.ldb
.set_pwdProperties(str(props
))
132 # Restore the value on exit.
133 self
.addCleanup(self
.ldb
.set_pwdProperties
, pwdProperties
)
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
))
164 error
= "Digest expected[%s], actual[%s], " \
165 "user[%s], realm[%s], pass[%s]" % \
166 (expected
, actual
, user
, realm
, password
)
167 self
.assertEquals(expected
, actual
, error
)
169 # Check all of the 29 expected WDigest values
171 def check_wdigests(self
, digests
):
173 self
.assertEquals(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
.assertEquals(len(expected
), up
.num_hashes
)
311 for (tag
, alg
, rounds
) in expected
:
312 self
.assertEquals(tag
, up
.hashes
[i
].scheme
)
314 data
= up
.hashes
[i
].value
.split("$")
315 # Check we got the expected crypt algorithm
316 self
.assertEquals(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
.assertEquals(expected
, up
.hashes
[i
].value
)
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
.assertEquals(expected
, actual
)