tests/gkdi: Allow current time to be overridden
[asn/samba-autobuild/.git] / python / samba / tests / gkdi.py
1 #
2 # Helper classes for testing the Group Key Distribution Service.
3 #
4 # Copyright (C) Catalyst.Net Ltd 2023
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 <https://www.gnu.org/licenses/>.
18 #
19
20 import sys
21 import os
22
23 sys.path.insert(0, "bin/python")
24 os.environ["PYTHONUNBUFFERED"] = "1"
25
26 import datetime
27 import secrets
28 from typing import NewType, Optional, Tuple, Union
29
30 import ldb
31
32 from cryptography.hazmat.backends import default_backend
33 from cryptography.hazmat.primitives import hashes
34 from cryptography.hazmat.primitives.kdf.kbkdf import CounterLocation, KBKDFHMAC, Mode
35
36 from samba import (
37     ntstatus,
38     NTSTATUSError,
39     werror,
40 )
41 from samba.credentials import Credentials
42 from samba.dcerpc import gkdi, misc
43 from samba.gkdi import (
44     Algorithm,
45     Gkid,
46     GkidType,
47     GroupKey,
48     KEY_CYCLE_DURATION,
49     KEY_LEN_BYTES,
50     MAX_CLOCK_SKEW,
51     SeedKeyPair,
52 )
53 from samba.hresult import (
54     HRES_E_INVALIDARG,
55     HRES_NTE_BAD_KEY,
56     HRES_NTE_NO_KEY,
57 )
58 from samba.ndr import ndr_pack, ndr_unpack
59 from samba.nt_time import (
60     datetime_from_nt_time,
61     nt_time_from_datetime,
62     NtTime,
63     NtTimeDelta,
64     timedelta_from_nt_time_delta,
65 )
66 from samba.param import LoadParm
67 from samba.samdb import SamDB
68
69 from samba.tests import delete_force, TestCase
70
71
72 HResult = NewType("HResult", int)
73 RootKey = NewType("RootKey", ldb.Message)
74
75
76 ROOT_KEY_START_TIME = NtTime(KEY_CYCLE_DURATION + MAX_CLOCK_SKEW)
77
78 DSDB_GMSA_TIME_OPAQUE = "dsdb_gmsa_time_opaque"
79
80
81 class GetKeyError(Exception):
82     def __init__(self, status: HResult, message: str):
83         super().__init__(status, message)
84
85
86 class GkdiBaseTest(TestCase):
87     # This is the NDR‐encoded security descriptor O:SYD:(A;;FRFW;;;S-1-5-9).
88     gmsa_sd = (
89         b"\x01\x00\x04\x800\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
90         b"\x14\x00\x00\x00\x02\x00\x1c\x00\x01\x00\x00\x00\x00\x00\x14\x00"
91         b"\x9f\x01\x12\x00\x01\x01\x00\x00\x00\x00\x00\x05\t\x00\x00\x00"
92         b"\x01\x01\x00\x00\x00\x00\x00\x05\x12\x00\x00\x00"
93     )
94
95     def set_db_time(self, samdb: SamDB, time: Optional[NtTime]) -> None:
96         samdb.set_opaque(DSDB_GMSA_TIME_OPAQUE, time)
97
98     def get_db_time(self, samdb: SamDB) -> Optional[NtTime]:
99         return samdb.get_opaque(DSDB_GMSA_TIME_OPAQUE)
100
101     def current_time(
102         self, samdb: SamDB, *, offset: Optional[datetime.timedelta] = None
103     ) -> datetime.datetime:
104         now = self.get_db_time(samdb)
105         if now is None:
106             current_time = datetime.datetime.now(tz=datetime.timezone.utc)
107         else:
108             current_time = datetime_from_nt_time(now)
109
110         if offset is not None:
111             current_time += offset
112
113         return current_time
114
115     def current_nt_time(
116         self, samdb: SamDB, *, offset: Optional[datetime.timedelta] = None
117     ) -> NtTime:
118         return nt_time_from_datetime(self.current_time(samdb, offset=offset))
119
120     def current_gkid(
121         self, samdb: SamDB, *, offset: Optional[datetime.timedelta] = None
122     ) -> Gkid:
123         if offset is None:
124             # Allow for clock skew.
125             offset = timedelta_from_nt_time_delta(MAX_CLOCK_SKEW)
126
127         return Gkid.from_nt_time(self.current_nt_time(samdb, offset=offset))
128
129     def gkdi_connect(
130         self, host: str, lp: LoadParm, server_creds: Credentials
131     ) -> gkdi.gkdi:
132         try:
133             return gkdi.gkdi(f"ncacn_ip_tcp:{host}[seal]", lp, server_creds)
134         except NTSTATUSError as err:
135             if err.args[0] == ntstatus.NT_STATUS_PORT_UNREACHABLE:
136                 self.fail(
137                     "Try starting the Microsoft Key Distribution Service (KdsSvc).\n"
138                     "In PowerShell, run:\n\tStart-Service -Name KdsSvc"
139                 )
140
141             raise
142
143     def rpc_get_key(
144         self,
145         conn: gkdi.gkdi,
146         target_sd: bytes,
147         root_key_id: Optional[misc.GUID],
148         gkid: Gkid,
149     ) -> SeedKeyPair:
150         out_len, out, result = conn.GetKey(
151             list(target_sd), root_key_id, gkid.l0_idx, gkid.l1_idx, gkid.l2_idx
152         )
153         result_code, result_string = result
154         if (
155             root_key_id is None
156             and result_code & 0xFFFF == werror.WERR_TOO_MANY_OPEN_FILES
157         ):
158             self.fail(
159                 "The server has given up selecting a root key because there are too"
160                 " many keys (more than 1000) in the Master Root Keys container. Delete"
161                 " some root keys and try again."
162             )
163         if result != (0, None):
164             raise GetKeyError(result_code, result_string)
165         self.assertEqual(len(out), out_len, "output len mismatch")
166
167         envelope = ndr_unpack(gkdi.GroupKeyEnvelope, bytes(out))
168
169         gkid = Gkid(envelope.l0_index, envelope.l1_index, envelope.l2_index)
170         l1_key = bytes(envelope.l1_key) if envelope.l1_key else None
171         l2_key = bytes(envelope.l2_key) if envelope.l2_key else None
172
173         hash_algorithm = Algorithm.from_kdf_parameters(bytes(envelope.kdf_parameters))
174
175         root_key_id = envelope.root_key_id
176
177         return SeedKeyPair(l1_key, l2_key, gkid, hash_algorithm, root_key_id)
178
179     def get_root_key_object(
180         self, samdb: SamDB, root_key_id: Optional[misc.GUID], gkid: Gkid
181     ) -> Tuple[RootKey, misc.GUID]:
182         """Return a root key object and its corresponding GUID.
183
184         *root_key_id* specifies the GUID of the root key object to return. It
185         can be ``None`` to indicate that the selected key should be the most
186         recently created key starting not after the time indicated by *gkid*.
187
188         Bear in mind as that the Microsoft Key Distribution Service caches root
189         keys, the most recently created key might not be the one that Windows
190         chooses."""
191
192         root_key_attrs = [
193             "cn",
194             "msKds-CreateTime",
195             "msKds-KDFAlgorithmID",
196             "msKds-KDFParam",
197             "msKds-RootKeyData",
198             "msKds-UseStartTime",
199             "msKds-Version",
200         ]
201
202         gkid_start_nt_time = gkid.start_nt_time()
203
204         exact_key_specified = root_key_id is not None
205         if exact_key_specified:
206             root_key_dn = self.get_root_key_container_dn(samdb)
207             root_key_dn.add_child(f"CN={root_key_id}")
208
209             try:
210                 root_key_res = samdb.search(
211                     root_key_dn, scope=ldb.SCOPE_BASE, attrs=root_key_attrs
212                 )
213             except ldb.LdbError as err:
214                 if err.args[0] == ldb.ERR_NO_SUCH_OBJECT:
215                     raise GetKeyError(HRES_NTE_NO_KEY, "no such root key exists")
216
217                 raise
218
219             root_key_object = root_key_res[0]
220         else:
221             root_keys = samdb.search(
222                 self.get_root_key_container_dn(samdb),
223                 scope=ldb.SCOPE_SUBTREE,
224                 expression=f"(msKds-UseStartTime<={gkid_start_nt_time})",
225                 attrs=root_key_attrs,
226             )
227             if not root_keys:
228                 raise GetKeyError(
229                     HRES_NTE_NO_KEY, "no root keys exist at specified time"
230                 )
231
232             def root_key_create_time(key: RootKey) -> NtTime:
233                 create_time = key.get("msKds-CreateTime", idx=0)
234                 if create_time is None:
235                     return NtTime(0)
236
237                 return NtTime(int(create_time))
238
239             root_key_object = max(root_keys, key=root_key_create_time)
240
241             root_key_cn = root_key_object.get("cn", idx=0)
242             self.assertIsNotNone(root_key_cn)
243             root_key_id = misc.GUID(root_key_cn)
244
245         data = root_key_object.get("msKds-RootKeyData", idx=0)
246         self.assertIsNotNone(data)
247         if len(data) != KEY_LEN_BYTES:
248             raise GetKeyError(
249                 HRES_NTE_BAD_KEY, f"root key data must be {KEY_LEN_BYTES} bytes"
250             )
251
252         use_start_nt_time = NtTime(
253             int(root_key_object.get("msKds-UseStartTime", idx=0))
254         )
255         if use_start_nt_time == 0:
256             raise GetKeyError(HRES_NTE_BAD_KEY, "root key effective time is 0")
257         use_start_nt_time = NtTime(
258             use_start_nt_time - NtTimeDelta(KEY_CYCLE_DURATION + MAX_CLOCK_SKEW)
259         )
260
261         if exact_key_specified and not (0 <= use_start_nt_time <= gkid_start_nt_time):
262             raise GetKeyError(HRES_E_INVALIDARG, "root key is not yet valid")
263
264         return root_key_object, root_key_id
265
266     def validate_get_key_request(
267         self, gkid: Gkid, current_gkid: Gkid, root_key_specified: bool
268     ) -> None:
269         if gkid > current_gkid:
270             raise GetKeyError(
271                 HRES_E_INVALIDARG,
272                 f"invalid request for a key from the future: {gkid} > {current_gkid}",
273             )
274
275         gkid_type = gkid.gkid_type()
276         if gkid_type is GkidType.DEFAULT:
277             derived_from = (
278                 " derived from the specified root key" if root_key_specified else ""
279             )
280             raise NotImplementedError(
281                 f"The latest group key{derived_from} is being requested."
282             )
283
284         if gkid_type is not GkidType.L2_SEED_KEY:
285             raise GetKeyError(
286                 HRES_E_INVALIDARG, f"invalid request for {gkid_type.description()}"
287             )
288
289     def get_key(
290         self,
291         samdb: SamDB,
292         target_sd: bytes,  # An NDR‐encoded valid security descriptor in self‐relative format.
293         root_key_id: Optional[misc.GUID],
294         gkid: Gkid,
295         *,
296         root_key_id_hint: Optional[misc.GUID] = None,
297         current_gkid: Optional[Gkid] = None,
298     ) -> SeedKeyPair:
299         """Emulate the ISDKey.GetKey() RPC method.
300
301         When passed a NULL root key ID, GetKey() may use a cached root key
302         rather than picking the most recently created applicable key as the
303         documentation implies. If it’s important to arrive at the same result as
304         Windows, pass a GUID in the *root_key_id_hint* parameter to specify a
305         particular root key to use."""
306
307         if current_gkid is None:
308             current_gkid = self.current_gkid(samdb)
309
310         root_key_specified = root_key_id is not None
311         if root_key_specified:
312             self.assertIsNone(
313                 root_key_id_hint, "don’t provide both root key ID parameters"
314             )
315
316         self.validate_get_key_request(gkid, current_gkid, root_key_specified)
317
318         root_key_object, root_key_id = self.get_root_key_object(
319             samdb, root_key_id if root_key_specified else root_key_id_hint, gkid
320         )
321
322         if root_key_specified:
323             if gkid.l0_idx < current_gkid.l0_idx:
324                 # All of the seed keys with an L0 index less than the current L0
325                 # index are from the past and thus are safe to return. If the
326                 # caller has requested a specific seed key with a past L0 index,
327                 # return the L1 seed key (L0, 31, −1), from which any L1 or L2
328                 # seed key having that L0 index can be derived.
329                 l1_gkid = Gkid(gkid.l0_idx, 31, -1)
330                 seed_key = self.compute_seed_key(
331                     target_sd, root_key_id, root_key_object, l1_gkid
332                 )
333                 return SeedKeyPair(
334                     seed_key.key,
335                     None,
336                     Gkid(gkid.l0_idx, 31, 31),
337                     seed_key.hash_algorithm,
338                     root_key_id,
339                 )
340
341             # All of the previous seed keys with an L0 index equal to the
342             # current L0 index can be derived from the current seed key or from
343             # the next older L1 seed key.
344             gkid = current_gkid
345
346         if gkid.l2_idx == 31:
347             # The current seed key, and all previous seed keys with that same L0
348             # index, can be derived from the L1 seed key (L0, L1, 31).
349             l1_gkid = Gkid(gkid.l0_idx, gkid.l1_idx, -1)
350             seed_key = self.compute_seed_key(
351                 target_sd, root_key_id, root_key_object, l1_gkid
352             )
353             return SeedKeyPair(
354                 seed_key.key, None, gkid, seed_key.hash_algorithm, root_key_id
355             )
356
357         # Compute the L2 seed key to return.
358         seed_key = self.compute_seed_key(target_sd, root_key_id, root_key_object, gkid)
359
360         next_older_seed_key = None
361         if gkid.l1_idx != 0:
362             # From the current seed key can be derived only those seed keys that
363             # share its L1 and L2 indices. To be able to derive previous seed
364             # keys with older L1 indices, the caller must be given the next
365             # older L1 seed key as well.
366             next_older_l1_gkid = Gkid(gkid.l0_idx, gkid.l1_idx - 1, -1)
367             next_older_seed_key = self.compute_seed_key(
368                 target_sd, root_key_id, root_key_object, next_older_l1_gkid
369             ).key
370
371         return SeedKeyPair(
372             next_older_seed_key,
373             seed_key.key,
374             gkid,
375             seed_key.hash_algorithm,
376             root_key_id,
377         )
378
379     def get_key_exact(
380         self,
381         samdb: SamDB,
382         target_sd: bytes,  # An NDR‐encoded valid security descriptor in self‐relative format.
383         root_key_id: Optional[misc.GUID],
384         gkid: Gkid,
385         current_gkid: Optional[Gkid] = None,
386     ) -> GroupKey:
387         if current_gkid is None:
388             current_gkid = self.current_gkid(samdb)
389
390         root_key_specified = root_key_id is not None
391         self.validate_get_key_request(gkid, current_gkid, root_key_specified)
392
393         root_key_object, root_key_id = self.get_root_key_object(
394             samdb, root_key_id, gkid
395         )
396
397         return self.compute_seed_key(target_sd, root_key_id, root_key_object, gkid)
398
399     def get_root_key_data(self, root_key: RootKey) -> Tuple[bytes, Algorithm]:
400         version = root_key.get("msKds-Version", idx=0)
401         self.assertEqual(b"1", version)
402
403         algorithm_id = root_key.get("msKds-KDFAlgorithmID", idx=0)
404         self.assertEqual(b"SP800_108_CTR_HMAC", algorithm_id)
405
406         hash_algorithm = Algorithm.from_kdf_parameters(
407             root_key.get("msKds-KDFParam", idx=0)
408         )
409
410         root_key_data = root_key.get("msKds-RootKeyData", idx=0)
411         self.assertIsInstance(root_key_data, bytes)
412
413         return root_key_data, hash_algorithm
414
415     def compute_seed_key(
416         self,
417         target_sd: bytes,
418         root_key_id: misc.GUID,
419         root_key: RootKey,
420         target_gkid: Gkid,
421     ) -> GroupKey:
422         target_gkid_type = target_gkid.gkid_type()
423         self.assertIn(
424             target_gkid_type,
425             (GkidType.L1_SEED_KEY, GkidType.L2_SEED_KEY),
426             f"unexpected attempt to compute {target_gkid_type.description()}",
427         )
428
429         root_key_data, algorithm = self.get_root_key_data(root_key)
430         root_key_id_bytes = ndr_pack(root_key_id)
431
432         hash_algorithm = algorithm.algorithm()
433
434         # Derive the L0 seed key.
435         gkid = Gkid.l0_seed_key(target_gkid.l0_idx)
436         key = self.derive_key(root_key_data, root_key_id_bytes, hash_algorithm, gkid)
437
438         # Derive the L1 seed key.
439
440         gkid = gkid.derive_l1_seed_key()
441         key = self.derive_key(
442             key, root_key_id_bytes, hash_algorithm, gkid, target_sd=target_sd
443         )
444
445         while gkid.l1_idx != target_gkid.l1_idx:
446             gkid = gkid.derive_l1_seed_key()
447             key = self.derive_key(key, root_key_id_bytes, hash_algorithm, gkid)
448
449         # Derive the L2 seed key.
450         while gkid != target_gkid:
451             gkid = gkid.derive_l2_seed_key()
452             key = self.derive_key(key, root_key_id_bytes, hash_algorithm, gkid)
453
454         return GroupKey(key, gkid, algorithm, root_key_id)
455
456     def derive_key(
457         self,
458         key: bytes,
459         root_key_id_bytes: bytes,
460         hash_algorithm: hashes.HashAlgorithm,
461         gkid: Gkid,
462         *,
463         target_sd: Optional[bytes] = None,
464     ) -> bytes:
465         def u32_bytes(n: int) -> bytes:
466             return (n & 0xFFFF_FFFF).to_bytes(length=4, byteorder="little")
467
468         context = (
469             root_key_id_bytes
470             + u32_bytes(gkid.l0_idx)
471             + u32_bytes(gkid.l1_idx)
472             + u32_bytes(gkid.l2_idx)
473         )
474         if target_sd is not None:
475             context += target_sd
476         return self.kdf(hash_algorithm, key, context)
477
478     def kdf(
479         self,
480         hash_algorithm: hashes.HashAlgorithm,
481         key: bytes,
482         context: bytes,
483         *,
484         label="KDS service",
485         len_in_bytes=KEY_LEN_BYTES,
486     ) -> bytes:
487         label = label.encode("utf-16-le") + b"\x00\x00"
488         kdf = KBKDFHMAC(
489             algorithm=hash_algorithm,
490             mode=Mode.CounterMode,
491             length=len_in_bytes,
492             rlen=4,
493             llen=4,
494             location=CounterLocation.BeforeFixed,
495             label=label,
496             context=context,
497             fixed=None,
498             backend=default_backend(),
499         )
500         return kdf.derive(key)
501
502     def get_config_dn(self, samdb: SamDB, dn: str) -> ldb.Dn:
503         config_dn = samdb.get_config_basedn()
504         config_dn.add_child(dn)
505         return config_dn
506
507     def get_server_config_dn(self, samdb: SamDB) -> ldb.Dn:
508         # [MS-GKDI] has “CN=Sid Key Service” for “CN=Group Key Distribution
509         # Service”, and “CN=SID Key Server Configuration” for “CN=Group Key
510         # Distribution Service Server Configuration”.
511         return self.get_config_dn(
512             samdb,
513             "CN=Group Key Distribution Service Server Configuration,"
514             "CN=Server Configuration,"
515             "CN=Group Key Distribution Service,"
516             "CN=Services",
517         )
518
519     def get_root_key_container_dn(self, samdb: SamDB) -> ldb.Dn:
520         # [MS-GKDI] has “CN=Sid Key Service” for “CN=Group Key Distribution Service”.
521         return self.get_config_dn(
522             samdb,
523             "CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services",
524         )
525
526     def create_root_key(
527         self,
528         samdb: SamDB,
529         domain_dn: ldb.Dn,
530         *,
531         use_start_time: Optional[Union[datetime.datetime, NtTime]] = None,
532         hash_algorithm: Optional[Algorithm] = Algorithm.SHA512,
533         guid: Optional[misc.GUID] = None,
534         data: Optional[bytes] = None,
535     ) -> misc.GUID:
536         # we defer the actual work to the create_root_key() function,
537         # which exists so that the samba-tool tests can borrow that
538         # function.
539
540         root_key_guid, root_key_dn = create_root_key(
541             samdb,
542             domain_dn,
543             current_nt_time=self.current_nt_time(
544                 samdb,
545                 # Allow for clock skew.
546                 offset=timedelta_from_nt_time_delta(MAX_CLOCK_SKEW),
547             ),
548             use_start_time=use_start_time,
549             hash_algorithm=hash_algorithm,
550             guid=guid,
551             data=data,
552         )
553
554         if guid is not None:
555             # A test may request that a root key have a specific GUID so that
556             # results may be reproducible. Ensure these keys are cleaned up
557             # afterwards.
558             self.addCleanup(delete_force, samdb, root_key_dn)
559             self.assertEqual(guid, root_key_guid)
560
561         return root_key_guid
562
563
564 def create_root_key(
565     samdb: SamDB,
566     domain_dn: ldb.Dn,
567     *,
568     current_nt_time: NtTime,
569     use_start_time: Optional[Union[datetime.datetime, NtTime]] = None,
570     hash_algorithm: Optional[Algorithm] = Algorithm.SHA512,
571     guid: Optional[misc.GUID] = None,
572     data: Optional[bytes] = None,
573 ) -> Tuple[misc.GUID, ldb.Dn]:
574     # [MS-GKDI] 3.1.4.1.1, “Creating a New Root Key”, states that if the
575     # server receives a GetKey request and the root keys container in Active
576     # Directory is empty, the the server must create a new root key object
577     # based on the default Server Configuration object. Additional root keys
578     # are to be created based on either the default Server Configuration
579     # object or an updated one specifying optional configuration values.
580
581     if guid is None:
582         guid = misc.GUID(secrets.token_bytes(16))
583
584     if data is None:
585         data = secrets.token_bytes(KEY_LEN_BYTES)
586
587     create_time = current_nt_time
588
589     if use_start_time is None:
590         # Root keys created by Windows without the ‘-EffectiveImmediately’
591         # parameter have an effective time of exactly ten days in the
592         # future, presumably to allow time for replication.
593         #
594         # Microsoft’s documentation on creating a KDS root key, located at
595         # https://learn.microsoft.com/en-us/windows-server/security/group-managed-service-accounts/create-the-key-distribution-services-kds-root-key,
596         # claims to the contrary that domain controllers will only wait up
597         # to ten hours before allowing Group Managed Service Accounts to be
598         # created.
599         #
600         # The same page includes instructions for creating a root key with
601         # an effective time of ten hours in the past (for testing purposes),
602         # but I’m not sure why — the KDS will consider a key valid for use
603         # immediately after its start time has passed, without bothering to
604         # wait ten hours first. In fact, it will consider a key to be valid
605         # a full ten hours (plus clock skew) *before* its declared start
606         # time — intentional, or (conceivably) the result of an accidental
607         # negation?
608         current_interval_start_nt_time = Gkid.from_nt_time(
609             current_nt_time
610         ).start_nt_time()
611         use_start_time = NtTime(
612             current_interval_start_nt_time + KEY_CYCLE_DURATION + MAX_CLOCK_SKEW
613         )
614
615     if isinstance(use_start_time, datetime.datetime):
616         use_start_nt_time = nt_time_from_datetime(use_start_time)
617     elif isinstance(use_start_time, int):
618         use_start_nt_time = use_start_time
619     else:
620         raise ValueError("use_start_time should be a datetime or int")
621
622     kdf_parameters = None
623     if hash_algorithm is not None:
624         kdf_parameters = gkdi.KdfParameters()
625         kdf_parameters.hash_algorithm = hash_algorithm.value
626         kdf_parameters = ndr_pack(kdf_parameters)
627
628     # These are the encoded p and g values, respectively, of the “2048‐bit
629     # MODP Group with 256‐bit Prime Order Subgroup” from RFC 5114 section
630     # 2.3.
631     field_order = (
632         b"\x87\xa8\xe6\x1d\xb4\xb6f<\xff\xbb\xd1\x9ce\x19Y\x99\x8c\xee\xf6\x08"
633         b"f\r\xd0\xf2],\xee\xd4C^;\x00\xe0\r\xf8\xf1\xd6\x19W\xd4\xfa\xf7\xdfE"
634         b"a\xb2\xaa0\x16\xc3\xd9\x114\to\xaa;\xf4)m\x83\x0e\x9a|"
635         b" \x9e\x0cd\x97Qz\xbd"
636         b'Z\x8a\x9d0k\xcfg\xed\x91\xf9\xe6r[GX\xc0"\xe0\xb1\xefBu\xbf{l[\xfc\x11'
637         b"\xd4_\x90\x88\xb9A\xf5N\xb1\xe5\x9b\xb8\xbc9\xa0\xbf\x120\x7f\\O\xdbp\xc5"
638         b"\x81\xb2?v\xb6:\xca\xe1\xca\xa6\xb7\x90-RRg5H\x8a\x0e\xf1<m\x9aQ\xbf\xa4\xab"
639         b":\xd84w\x96RM\x8e\xf6\xa1g\xb5\xa4\x18%\xd9g\xe1D\xe5\x14\x05d%"
640         b"\x1c\xca\xcb\x83\xe6\xb4\x86\xf6\xb3\xca?yqP`&\xc0\xb8W\xf6\x89\x96(V"
641         b"\xde\xd4\x01\n\xbd\x0b\xe6!\xc3\xa3\x96\nT\xe7\x10\xc3u\xf2cu\xd7\x01A\x03"
642         b"\xa4\xb5C0\xc1\x98\xaf\x12a\x16\xd2'n\x11q_i8w\xfa\xd7\xef\t\xca\xdb\tJ\xe9"
643         b"\x1e\x1a\x15\x97"
644     )
645     generator = (
646         b"?\xb3,\x9bs\x13M\x0b.wPf`\xed\xbdHL\xa7\xb1\x8f!\xef T\x07\xf4y:"
647         b"\x1a\x0b\xa1%\x10\xdb\xc1Pw\xbeF?\xffO\xedJ\xac\x0b\xb5U\xbe:l\x1b\x0ckG\xb1"
648         b"\xbc7s\xbf~\x8cob\x90\x12(\xf8\xc2\x8c\xbb\x18\xa5Z\xe3\x13A\x00\ne"
649         b"\x01\x96\xf91\xc7zW\xf2\xdd\xf4c\xe5\xe9\xec\x14Kw}\xe6*\xaa\xb8\xa8b"
650         b"\x8a\xc3v\xd2\x82\xd6\xed8d\xe6y\x82B\x8e\xbc\x83\x1d\x144\x8fo/\x91\x93"
651         b"\xb5\x04Z\xf2vqd\xe1\xdf\xc9g\xc1\xfb?.U\xa4\xbd\x1b\xff\xe8;\x9c\x80"
652         b"\xd0R\xb9\x85\xd1\x82\xea\n\xdb*;s\x13\xd3\xfe\x14\xc8HK\x1e\x05%\x88\xb9"
653         b"\xb7\xd2\xbb\xd2\xdf\x01a\x99\xec\xd0n\x15W\xcd\t\x15\xb35;\xbbd\xe0\xec7"
654         b"\x7f\xd0(7\r\xf9+R\xc7\x89\x14(\xcd\xc6~\xb6\x18KR=\x1d\xb2F\xc3/c\x07\x84"
655         b"\x90\xf0\x0e\xf8\xd6G\xd1H\xd4yTQ^#'\xcf\xef\x98\xc5\x82fKL\x0fl\xc4\x16Y"
656     )
657     assert len(field_order) == len(generator)
658
659     key_length = len(field_order)
660
661     ffc_dh_parameters = gkdi.FfcDhParameters()
662     ffc_dh_parameters.field_order = list(field_order)
663     ffc_dh_parameters.generator = list(generator)
664     ffc_dh_parameters.key_length = key_length
665     ffc_dh_parameters = ndr_pack(ffc_dh_parameters)
666
667     root_key_dn = samdb.get_config_basedn()
668     root_key_dn.add_child(
669         "CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services"
670     )
671
672     root_key_dn.add_child(f"CN={guid}")
673
674     # Avoid deleting root key objects without subsequently restarting the
675     # Microsoft Key Distribution Service. This service will keep its root
676     # key cached even after the corresponding AD object has been deleted,
677     # breaking later tests that try to look up the root key object.
678
679     details = {
680         "dn": root_key_dn,
681         "objectClass": "msKds-ProvRootKey",
682         "msKds-RootKeyData": data,
683         "msKds-CreateTime": str(create_time),
684         "msKds-UseStartTime": str(use_start_nt_time),
685         "msKds-DomainID": str(domain_dn),
686         "msKds-Version": "1",  # comes from Server Configuration object.
687         "msKds-KDFAlgorithmID": (
688             "SP800_108_CTR_HMAC"
689         ),  # comes from Server Configuration.
690         "msKds-SecretAgreementAlgorithmID": "DH",  # comes from Server Configuration.
691         "msKds-SecretAgreementParam": (
692             ffc_dh_parameters
693         ),  # comes from Server Configuration.
694         "msKds-PublicKeyLength": "2048",  # comes from Server Configuration.
695         "msKds-PrivateKeyLength": (
696             "512"
697         ),  # comes from Server Configuration. [MS-GKDI] claims this defaults to ‘256’.
698     }
699     if kdf_parameters is not None:
700         details["msKds-KDFParam"] = kdf_parameters  # comes from Server Configuration.
701
702     samdb.add(details)
703
704     return (guid, root_key_dn)