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/>.
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
):
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
,
48 self
.ktfile
= os
.path
.join(self
.tempdir
, 'test.keytab')
49 self
.principal
= self
.creds
.get_principal()
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
)
67 entry_as_tuple
= entry_to_tuple(entry
)
68 keytab_as_set
.add(entry_as_tuple
)
70 keytab_bytes
= keytab
.further_entry
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:
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()
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
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
139 for entry
in keytab_as_set
:
140 (principal
, enctype
, kvno
, key
) = entry
141 if principal
== new_principal
:
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
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
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
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
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
:
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
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
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
:
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
)
324 net
.export_keytab(keytab
=self
.ktfile
+ "/f")
325 self
.fail("Expected failure to write to an existing file")
326 except NTSTATUSError
as err
:
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
)
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
:
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
))
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
)
377 net
.export_keytab(samdb
=self
.samdb
, keytab
=self
.ktfile
, principal
=gmsa_principal
)
378 except RuntimeError as 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
393 net
.export_keytab(keytab
=self
.ktfile
, principal
=gmsa_principal
)
394 except RuntimeError as 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
424 remote_keys
[remote_keytab
.entry
.enctype
] = remote_keytab
.entry
.key
.data
425 keytab_bytes
= remote_keytab
.further_entry
429 remote_keytab
= ndr_unpack(krb5ccache
.MULTIPLE_KEYTAB_ENTRIES
, keytab_bytes
)
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:
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
),
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
])