python:tests: Use Managed Service Accounts well‐known GUID
[Samba.git] / python / samba / tests / dckeytab.py
blob339190ec3ad8f29fd6cf51dfcf4e929e41076ec3
1 # Tests for source4/libnet/py_net_dckeytab.c
3 # Copyright (C) David Mulder <dmulder@suse.com> 2018
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/>.
19 import os
20 import sys
21 import string
22 from samba.net import Net
23 from samba import enable_net_export_keytab
25 from samba import credentials, dsdb, ntstatus, NTSTATUSError, tests
26 from samba.dcerpc import krb5ccache, security
27 from samba.dsdb import UF_WORKSTATION_TRUST_ACCOUNT
28 from samba.ndr import ndr_unpack, ndr_pack
29 from samba.param import LoadParm
30 from samba.samdb import SamDB
31 from samba.tests import TestCaseInTempDir, delete_force
33 from ldb import SCOPE_BASE
35 enable_net_export_keytab()
38 class DCKeytabTests(TestCaseInTempDir):
39 def setUp(self):
40 super().setUp()
41 self.lp = LoadParm()
42 self.lp.load_default()
43 self.creds = self.insta_creds(template=self.get_credentials())
44 self.samdb = SamDB(url=f"ldap://{os.environ.get('SERVER')}",
45 credentials=self.creds,
46 lp=self.lp)
48 self.ktfile = os.path.join(self.tempdir, 'test.keytab')
49 self.principal = self.creds.get_principal()
51 def tearDown(self):
52 super().tearDown()
54 def keytab_as_set(self, keytab_bytes):
55 def entry_to_tuple(entry):
56 principal = '/'.join(entry.principal.components) + f"@{entry.principal.realm}"
57 enctype = entry.enctype
58 kvno = entry.key_version
59 key = tuple(entry.key.data)
60 return (principal, enctype, kvno, key)
62 keytab = ndr_unpack(krb5ccache.KEYTAB, keytab_bytes)
63 entry = keytab.entry
65 keytab_as_set = set()
67 entry_as_tuple = entry_to_tuple(entry)
68 keytab_as_set.add(entry_as_tuple)
70 keytab_bytes = keytab.further_entry
71 while True:
72 multiple_entry = ndr_unpack(krb5ccache.MULTIPLE_KEYTAB_ENTRIES, keytab_bytes)
73 entry = multiple_entry.entry
74 entry_as_tuple = entry_to_tuple(entry)
75 self.assertNotIn(entry_as_tuple, keytab_as_set)
76 keytab_as_set.add(entry_as_tuple)
78 keytab_bytes = multiple_entry.further_entry
79 if keytab_bytes is None or len(keytab_bytes) == 0:
80 break
82 return keytab_as_set
84 def test_export_keytab(self):
85 net = Net(None, self.lp)
86 self.addCleanup(self.rm_files, self.ktfile)
87 net.export_keytab(keytab=self.ktfile, principal=self.principal)
88 self.assertTrue(os.path.exists(self.ktfile), 'keytab was not created')
90 # Parse the first entry in the keytab
91 with open(self.ktfile, 'rb') as bytes_kt:
92 keytab_bytes = bytes_kt.read()
94 # confirm only this principal was exported
95 for entry in self.keytab_as_set(keytab_bytes):
96 (principal, enctype, kvno, key) = entry
97 self.assertEqual(principal, self.principal)
99 def test_export_keytab_all(self):
100 net = Net(None, self.lp)
101 self.addCleanup(self.rm_files, self.ktfile)
102 net.export_keytab(keytab=self.ktfile)
103 self.assertTrue(os.path.exists(self.ktfile), 'keytab was not created')
105 with open(self.ktfile, 'rb') as bytes_kt:
106 keytab_bytes = bytes_kt.read()
108 # Parse the keytab
109 keytab_as_set = self.keytab_as_set(keytab_bytes)
111 # confirm many principals were exported
112 self.assertGreater(len(keytab_as_set), 10)
114 def test_export_keytab_all_keep_stale(self):
115 net = Net(None, self.lp)
116 self.addCleanup(self.rm_files, self.ktfile)
117 net.export_keytab(keytab=self.ktfile)
119 new_principal=f"keytab_testuser@{self.creds.get_realm()}"
120 self.samdb.newuser("keytab_testuser", "4rfvBGT%")
121 self.addCleanup(self.samdb.deleteuser, "keytab_testuser")
123 net.export_keytab(keytab=self.ktfile, keep_stale_entries=True)
125 self.assertTrue(os.path.exists(self.ktfile), 'keytab was not created')
127 with open(self.ktfile, 'rb') as bytes_kt:
128 keytab_bytes = bytes_kt.read()
130 # confirm many principals were exported
131 # self.keytab_as_set() will also check we only got it
132 # each entry once
133 keytab_as_set = self.keytab_as_set(keytab_bytes)
135 self.assertGreater(len(keytab_as_set), 10)
137 # Look for the new principal, showing this was updated
138 found = False
139 for entry in keytab_as_set:
140 (principal, enctype, kvno, key) = entry
141 if principal == new_principal:
142 found = True
144 self.assertTrue(found)
146 def test_export_keytab_nochange_update(self):
147 new_principal=f"keytab_testuser@{self.creds.get_realm()}"
148 self.samdb.newuser("keytab_testuser", "4rfvBGT%")
149 self.addCleanup(self.samdb.deleteuser, "keytab_testuser")
151 net = Net(None, self.lp)
152 self.addCleanup(self.rm_files, self.ktfile)
153 net.export_keytab(keytab=self.ktfile, principal=new_principal)
154 self.assertTrue(os.path.exists(self.ktfile), 'keytab was not created')
156 with open(self.ktfile, 'rb') as bytes_kt:
157 keytab_orig_bytes = bytes_kt.read()
159 net.export_keytab(keytab=self.ktfile, principal=new_principal)
161 # Parse the first entry in the keytab
162 with open(self.ktfile, 'rb') as bytes_kt:
163 keytab_bytes = bytes_kt.read()
165 self.assertEqual(keytab_orig_bytes, keytab_bytes)
167 # confirm only this principal was exported.
168 # self.keytab_as_set() will also check we only got it
169 # once
170 for entry in self.keytab_as_set(keytab_bytes):
171 (principal, enctype, kvno, key) = entry
172 self.assertEqual(principal, new_principal)
174 def test_export_keytab_change_update(self):
175 new_principal=f"keytab_testuser@{self.creds.get_realm()}"
176 self.samdb.newuser("keytab_testuser", "4rfvBGT%")
177 self.addCleanup(self.samdb.deleteuser, "keytab_testuser")
179 net = Net(None, self.lp)
180 self.addCleanup(self.rm_files, self.ktfile)
181 net.export_keytab(keytab=self.ktfile, principal=new_principal)
182 self.assertTrue(os.path.exists(self.ktfile), 'keytab was not created')
184 # Parse the first entry in the keytab
185 with open(self.ktfile, 'rb') as bytes_kt:
186 keytab_orig_bytes = bytes_kt.read()
188 self.samdb.setpassword(f"(userPrincipalName={new_principal})", "5rfvBGT%")
190 net.export_keytab(keytab=self.ktfile, principal=new_principal)
192 with open(self.ktfile, 'rb') as bytes_kt:
193 keytab_change_bytes = bytes_kt.read()
195 self.assertNotEqual(keytab_orig_bytes, keytab_change_bytes)
197 # We can't parse it as the parser is simple and doesn't
198 # understand holes in the file.
200 def test_export_keytab_change2_update(self):
201 new_principal=f"keytab_testuser@{self.creds.get_realm()}"
202 self.samdb.newuser("keytab_testuser", "4rfvBGT%")
203 self.addCleanup(self.samdb.deleteuser, "keytab_testuser")
205 net = Net(None, self.lp)
206 self.addCleanup(self.rm_files, self.ktfile)
207 net.export_keytab(keytab=self.ktfile, principal=new_principal)
208 self.assertTrue(os.path.exists(self.ktfile), 'keytab was not created')
210 # Parse the first entry in the keytab
211 with open(self.ktfile, 'rb') as bytes_kt:
212 keytab_orig_bytes = bytes_kt.read()
214 # intended to trigger the pruning code for old keys
215 self.samdb.setpassword(f"(userPrincipalName={new_principal})", "5rfvBGT%")
216 self.samdb.setpassword(f"(userPrincipalName={new_principal})", "6rfvBGT%")
218 net.export_keytab(keytab=self.ktfile, principal=new_principal)
220 with open(self.ktfile, 'rb') as bytes_kt:
221 keytab_change_bytes = bytes_kt.read()
223 self.assertNotEqual(keytab_orig_bytes, keytab_change_bytes)
225 # We can't parse it as the parser is simple and doesn't
226 # understand holes in the file.
228 def test_export_keytab_change3_update_keep(self):
229 new_principal=f"keytab_testuser@{self.creds.get_realm()}"
230 self.samdb.newuser("keytab_testuser", "4rfvBGT%")
231 self.addCleanup(self.samdb.deleteuser, "keytab_testuser")
232 net = Net(None, self.lp)
233 self.addCleanup(self.rm_files, self.ktfile)
234 net.export_keytab(keytab=self.ktfile, principal=new_principal)
235 self.assertTrue(os.path.exists(self.ktfile), 'keytab was not created')
237 # Parse the first entry in the keytab
238 with open(self.ktfile, 'rb') as bytes_kt:
239 keytab_orig_bytes = bytes_kt.read()
241 # By changing the password three times, we allow Samba to fill
242 # out current, old, older from supplementalCredentials and
243 # still have one password that must still be from the original
244 # keytab
245 self.samdb.setpassword(f"(userPrincipalName={new_principal})", "5rfvBGT%")
246 self.samdb.setpassword(f"(userPrincipalName={new_principal})", "6rfvBGT%")
247 self.samdb.setpassword(f"(userPrincipalName={new_principal})", "6rfvBGT%")
249 net.export_keytab(keytab=self.ktfile, principal=new_principal, keep_stale_entries=True)
251 with open(self.ktfile, 'rb') as bytes_kt:
252 keytab_change_bytes = bytes_kt.read()
254 self.assertNotEqual(keytab_orig_bytes, keytab_change_bytes)
256 # self.keytab_as_set() will also check we got each entry
257 # exactly once
258 keytab_as_set = self.keytab_as_set(keytab_change_bytes)
260 # Look for the new principal, showing this was updated but the old kept
261 found = 0
262 for entry in keytab_as_set:
263 (principal, enctype, kvno, key) = entry
264 if principal == new_principal and enctype == credentials.ENCTYPE_AES128_CTS_HMAC_SHA1_96:
265 found += 1
267 # Samba currently does not export the previous keys into the keytab, but could.
268 self.assertEqual(found, 4)
270 # confirm at least 12 keys (4 changes, 1 in orig export and 3
271 # history in 2nd export, 3 enctypes) were exported
272 self.assertGreaterEqual(len(keytab_as_set), 12)
274 def test_export_keytab_change2_export2_update_keep(self):
275 new_principal=f"keytab_testuser@{self.creds.get_realm()}"
276 self.samdb.newuser("keytab_testuser", "4rfvBGT%")
277 self.addCleanup(self.samdb.deleteuser, "keytab_testuser")
278 net = Net(None, self.lp)
279 self.addCleanup(self.rm_files, self.ktfile)
280 net.export_keytab(keytab=self.ktfile, principal=new_principal)
281 self.assertTrue(os.path.exists(self.ktfile), 'keytab was not created')
283 # Parse the first entry in the keytab
284 with open(self.ktfile, 'rb') as bytes_kt:
285 keytab_orig_bytes = bytes_kt.read()
287 self.samdb.setpassword(f"(userPrincipalName={new_principal})", "5rfvBGT%")
289 net.export_keytab(keytab=self.ktfile, principal=new_principal, keep_stale_entries=True)
291 self.samdb.setpassword(f"(userPrincipalName={new_principal})", "6rfvBGT%")
293 net.export_keytab(keytab=self.ktfile, principal=new_principal, keep_stale_entries=True)
295 with open(self.ktfile, 'rb') as bytes_kt:
296 keytab_change_bytes = bytes_kt.read()
298 self.assertNotEqual(keytab_orig_bytes, keytab_change_bytes)
300 # self.keytab_as_set() will also check we got each entry
301 # exactly once
302 keytab_as_set = self.keytab_as_set(keytab_change_bytes)
304 # Look for the new principal, showing this was updated but the old kept
305 found = 0
306 for entry in keytab_as_set:
307 (principal, enctype, kvno, key) = entry
308 if principal == new_principal and enctype == credentials.ENCTYPE_AES128_CTS_HMAC_SHA1_96:
309 found += 1
311 # This covers the simple case, one export per password change
312 self.assertEqual(found, 3)
314 # confirm at least 9 keys (3 exports, 3 enctypes) were exported
315 self.assertGreaterEqual(len(keytab_as_set), 9)
317 def test_export_keytab_not_a_dir(self):
318 net = Net(None, self.lp)
319 with open(self.ktfile, mode='w') as f:
320 f.write("NOT A KEYTAB")
321 self.addCleanup(self.rm_files, self.ktfile)
323 try:
324 net.export_keytab(keytab=self.ktfile + "/f")
325 self.fail("Expected failure to write to an existing file")
326 except NTSTATUSError as err:
327 num, _ = err.args
328 self.assertEqual(num, ntstatus.NT_STATUS_NOT_A_DIRECTORY)
330 def test_export_keytab_existing(self):
331 net = Net(None, self.lp)
332 with open(self.ktfile, mode='w') as f:
333 f.write("NOT A KEYTAB")
334 self.addCleanup(self.rm_files, self.ktfile)
336 try:
337 net.export_keytab(keytab=self.ktfile)
338 self.fail(f"Expected failure to write to an existing file {self.ktfile}")
339 except NTSTATUSError as err:
340 num, _ = err.args
341 self.assertEqual(num, ntstatus.NT_STATUS_OBJECT_NAME_EXISTS)
343 def test_export_keytab_gmsa(self):
345 # Create gMSA account
346 gmsa_username = "GMSA_K5KeytabTest$"
347 gmsa_principal = f"{gmsa_username}@{self.samdb.domain_dns_name().upper()}"
348 gmsa_base_dn = self.samdb.get_wellknown_dn(
349 self.samdb.get_default_basedn(),
350 dsdb.DS_GUID_MANAGED_SERVICE_ACCOUNTS_CONTAINER,
352 gmsa_user_dn = f"CN={gmsa_username},{gmsa_base_dn}"
354 msg = self.samdb.search(base="", scope=SCOPE_BASE, attrs=["tokenGroups"])[0]
355 connecting_user_sid = str(ndr_unpack(security.dom_sid, msg["tokenGroups"][0]))
357 domain_sid = security.dom_sid(self.samdb.get_domain_sid())
358 allow_sddl = f"O:SYD:(A;;RP;;;{connecting_user_sid})"
359 allow_sd = ndr_pack(security.descriptor.from_sddl(allow_sddl, domain_sid))
361 details = {
362 "dn": str(gmsa_user_dn),
363 "objectClass": "msDS-GroupManagedServiceAccount",
364 "msDS-ManagedPasswordInterval": "1",
365 "msDS-GroupMSAMembership": allow_sd,
366 "sAMAccountName": gmsa_username,
367 "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT),
370 delete_force(self.samdb, gmsa_user_dn)
371 self.samdb.add(details)
372 self.addCleanup(delete_force, self.samdb, gmsa_user_dn)
374 # Export keytab of gMSA account remotely
375 net = Net(None, self.lp)
376 try:
377 net.export_keytab(samdb=self.samdb, keytab=self.ktfile, principal=gmsa_principal)
378 except RuntimeError as e:
379 self.fail(e)
381 self.assertTrue(os.path.exists(self.ktfile), 'keytab was not created')
383 # Parse the first entry in the keytab
384 with open(self.ktfile, 'rb') as bytes_kt:
385 keytab_bytes = bytes_kt.read()
387 remote_keytab = ndr_unpack(krb5ccache.KEYTAB, keytab_bytes)
389 self.rm_files('test.keytab')
391 # Export keytab of gMSA account locally
392 try:
393 net.export_keytab(keytab=self.ktfile, principal=gmsa_principal)
394 except RuntimeError as e:
395 self.fail(e)
397 self.assertTrue(os.path.exists(self.ktfile), 'keytab was not created')
399 # Parse the first entry in the keytab
400 with open(self.ktfile, 'rb') as bytes_kt:
401 keytab_bytes = bytes_kt.read()
403 self.rm_files('test.keytab')
405 local_keytab = ndr_unpack(krb5ccache.KEYTAB, keytab_bytes)
407 # Confirm that the principal is as expected
409 principal_parts = gmsa_principal.split('@')
411 self.assertEqual(local_keytab.entry.principal.component_count, 1)
412 self.assertEqual(local_keytab.entry.principal.realm, principal_parts[1])
413 self.assertEqual(local_keytab.entry.principal.components[0], principal_parts[0])
415 self.assertEqual(remote_keytab.entry.principal.component_count, 1)
416 self.assertEqual(remote_keytab.entry.principal.realm, principal_parts[1])
417 self.assertEqual(remote_keytab.entry.principal.components[0], principal_parts[0])
419 # Put all keys from each into a dictionary, and confirm all remote keys are in local keytab
421 remote_keys = {}
423 while True:
424 remote_keys[remote_keytab.entry.enctype] = remote_keytab.entry.key.data
425 keytab_bytes = remote_keytab.further_entry
426 if not keytab_bytes:
427 break
429 remote_keytab = ndr_unpack(krb5ccache.MULTIPLE_KEYTAB_ENTRIES, keytab_bytes)
431 local_keys = {}
433 while True:
434 local_keys[local_keytab.entry.enctype] = local_keytab.entry.key.data
435 keytab_bytes = local_keytab.further_entry
436 if keytab_bytes is None or len(keytab_bytes) == 0:
437 break
438 local_keytab = ndr_unpack(krb5ccache.MULTIPLE_KEYTAB_ENTRIES, keytab_bytes)
440 # Check that the gMSA keys are in the local keys
441 remote_enctypes = set(remote_keys.keys())
443 # Check that at least the AES keys were generated
444 self.assertLessEqual(set(credentials.ENCTYPE_AES256_CTS_HMAC_SHA1_96,
445 credentials.ENCTYPE_AES128_CTS_HMAC_SHA1_96),
446 remote_enctypes)
448 local_enctypes = set(local_keys.keys())
450 self.assertLessEqual(remote_enctypes, local_enctypes)
452 common_enctypes = remote_enctypes & local_enctypes
454 for enctype in common_enctypes:
455 self.assertEqual(remote_keys[enctype], local_keys[enctype])