2 # Helper classes for testing the Group Key Distribution Service.
4 # Copyright (C) Catalyst.Net Ltd 2023
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.
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.
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/>.
23 sys.path.insert(0, "bin/python")
24 os.environ["PYTHONUNBUFFERED"] = "1"
28 from typing import NewType, Optional, Tuple, Union
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
41 from samba.credentials import Credentials
42 from samba.dcerpc import gkdi, misc
43 from samba.gkdi import (
53 from samba.hresult import (
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,
64 timedelta_from_nt_time_delta,
66 from samba.param import LoadParm
67 from samba.samdb import SamDB
69 from samba.tests import delete_force, TestCase
72 HResult = NewType("HResult", int)
73 RootKey = NewType("RootKey", ldb.Message)
76 ROOT_KEY_START_TIME = NtTime(KEY_CYCLE_DURATION + MAX_CLOCK_SKEW)
78 DSDB_GMSA_TIME_OPAQUE = "dsdb_gmsa_time_opaque"
81 class GetKeyError(Exception):
82 def __init__(self, status: HResult, message: str):
83 super().__init__(status, message)
86 class GkdiBaseTest(TestCase):
87 # This is the NDR‐encoded security descriptor O:SYD:(A;;FRFW;;;S-1-5-9).
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"
95 def set_db_time(self, samdb: SamDB, time: Optional[NtTime]) -> None:
96 samdb.set_opaque(DSDB_GMSA_TIME_OPAQUE, time)
98 def get_db_time(self, samdb: SamDB) -> Optional[NtTime]:
99 return samdb.get_opaque(DSDB_GMSA_TIME_OPAQUE)
102 self, samdb: SamDB, *, offset: Optional[datetime.timedelta] = None
103 ) -> datetime.datetime:
104 now = self.get_db_time(samdb)
106 current_time = datetime.datetime.now(tz=datetime.timezone.utc)
108 current_time = datetime_from_nt_time(now)
110 if offset is not None:
111 current_time += offset
116 self, samdb: SamDB, *, offset: Optional[datetime.timedelta] = None
118 return nt_time_from_datetime(self.current_time(samdb, offset=offset))
121 self, samdb: SamDB, *, offset: Optional[datetime.timedelta] = None
124 # Allow for clock skew.
125 offset = timedelta_from_nt_time_delta(MAX_CLOCK_SKEW)
127 return Gkid.from_nt_time(self.current_nt_time(samdb, offset=offset))
130 self, host: str, lp: LoadParm, server_creds: Credentials
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:
137 "Try starting the Microsoft Key Distribution Service (KdsSvc).\n"
138 "In PowerShell, run:\n\tStart-Service -Name KdsSvc"
147 root_key_id: Optional[misc.GUID],
150 out_len, out, result = conn.GetKey(
151 list(target_sd), root_key_id, gkid.l0_idx, gkid.l1_idx, gkid.l2_idx
153 result_code, result_string = result
156 and result_code & 0xFFFF == werror.WERR_TOO_MANY_OPEN_FILES
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."
163 if result != (0, None):
164 raise GetKeyError(result_code, result_string)
165 self.assertEqual(len(out), out_len, "output len mismatch")
167 envelope = ndr_unpack(gkdi.GroupKeyEnvelope, bytes(out))
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
173 hash_algorithm = Algorithm.from_kdf_parameters(bytes(envelope.kdf_parameters))
175 root_key_id = envelope.root_key_id
177 return SeedKeyPair(l1_key, l2_key, gkid, hash_algorithm, root_key_id)
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.
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*.
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
195 "msKds-KDFAlgorithmID",
198 "msKds-UseStartTime",
202 gkid_start_nt_time = gkid.start_nt_time()
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}")
210 root_key_res = samdb.search(
211 root_key_dn, scope=ldb.SCOPE_BASE, attrs=root_key_attrs
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")
219 root_key_object = root_key_res[0]
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,
229 HRES_NTE_NO_KEY, "no root keys exist at specified time"
232 def root_key_create_time(key: RootKey) -> NtTime:
233 create_time = key.get("msKds-CreateTime", idx=0)
234 if create_time is None:
237 return NtTime(int(create_time))
239 root_key_object = max(root_keys, key=root_key_create_time)
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)
245 data = root_key_object.get("msKds-RootKeyData", idx=0)
246 self.assertIsNotNone(data)
247 if len(data) != KEY_LEN_BYTES:
249 HRES_NTE_BAD_KEY, f"root key data must be {KEY_LEN_BYTES} bytes"
252 use_start_nt_time = NtTime(
253 int(root_key_object.get("msKds-UseStartTime", idx=0))
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)
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")
264 return root_key_object, root_key_id
266 def validate_get_key_request(
267 self, gkid: Gkid, current_gkid: Gkid, root_key_specified: bool
269 if gkid > current_gkid:
272 f"invalid request for a key from the future: {gkid} > {current_gkid}",
275 gkid_type = gkid.gkid_type()
276 if gkid_type is GkidType.DEFAULT:
278 " derived from the specified root key" if root_key_specified else ""
280 raise NotImplementedError(
281 f"The latest group key{derived_from} is being requested."
284 if gkid_type is not GkidType.L2_SEED_KEY:
286 HRES_E_INVALIDARG, f"invalid request for {gkid_type.description()}"
292 target_sd: bytes, # An NDR‐encoded valid security descriptor in self‐relative format.
293 root_key_id: Optional[misc.GUID],
296 root_key_id_hint: Optional[misc.GUID] = None,
297 current_gkid: Optional[Gkid] = None,
299 """Emulate the ISDKey.GetKey() RPC method.
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."""
307 if current_gkid is None:
308 current_gkid = self.current_gkid(samdb)
310 root_key_specified = root_key_id is not None
311 if root_key_specified:
313 root_key_id_hint, "don’t provide both root key ID parameters"
316 self.validate_get_key_request(gkid, current_gkid, root_key_specified)
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
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
336 Gkid(gkid.l0_idx, 31, 31),
337 seed_key.hash_algorithm,
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.
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
354 seed_key.key, None, gkid, seed_key.hash_algorithm, root_key_id
357 # Compute the L2 seed key to return.
358 seed_key = self.compute_seed_key(target_sd, root_key_id, root_key_object, gkid)
360 next_older_seed_key = None
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
375 seed_key.hash_algorithm,
382 target_sd: bytes, # An NDR‐encoded valid security descriptor in self‐relative format.
383 root_key_id: Optional[misc.GUID],
385 current_gkid: Optional[Gkid] = None,
387 if current_gkid is None:
388 current_gkid = self.current_gkid(samdb)
390 root_key_specified = root_key_id is not None
391 self.validate_get_key_request(gkid, current_gkid, root_key_specified)
393 root_key_object, root_key_id = self.get_root_key_object(
394 samdb, root_key_id, gkid
397 return self.compute_seed_key(target_sd, root_key_id, root_key_object, gkid)
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)
403 algorithm_id = root_key.get("msKds-KDFAlgorithmID", idx=0)
404 self.assertEqual(b"SP800_108_CTR_HMAC", algorithm_id)
406 hash_algorithm = Algorithm.from_kdf_parameters(
407 root_key.get("msKds-KDFParam", idx=0)
410 root_key_data = root_key.get("msKds-RootKeyData", idx=0)
411 self.assertIsInstance(root_key_data, bytes)
413 return root_key_data, hash_algorithm
415 def compute_seed_key(
418 root_key_id: misc.GUID,
422 target_gkid_type = target_gkid.gkid_type()
425 (GkidType.L1_SEED_KEY, GkidType.L2_SEED_KEY),
426 f"unexpected attempt to compute {target_gkid_type.description()}",
429 root_key_data, algorithm = self.get_root_key_data(root_key)
430 root_key_id_bytes = ndr_pack(root_key_id)
432 hash_algorithm = algorithm.algorithm()
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)
438 # Derive the L1 seed key.
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
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)
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)
454 return GroupKey(key, gkid, algorithm, root_key_id)
459 root_key_id_bytes: bytes,
460 hash_algorithm: hashes.HashAlgorithm,
463 target_sd: Optional[bytes] = None,
465 def u32_bytes(n: int) -> bytes:
466 return (n & 0xFFFF_FFFF).to_bytes(length=4, byteorder="little")
470 + u32_bytes(gkid.l0_idx)
471 + u32_bytes(gkid.l1_idx)
472 + u32_bytes(gkid.l2_idx)
474 if target_sd is not None:
476 return self.kdf(hash_algorithm, key, context)
480 hash_algorithm: hashes.HashAlgorithm,
485 len_in_bytes=KEY_LEN_BYTES,
487 label = label.encode("utf-16-le") + b"\x00\x00"
489 algorithm=hash_algorithm,
490 mode=Mode.CounterMode,
494 location=CounterLocation.BeforeFixed,
498 backend=default_backend(),
500 return kdf.derive(key)
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)
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(
513 "CN=Group Key Distribution Service Server Configuration,"
514 "CN=Server Configuration,"
515 "CN=Group Key Distribution Service,"
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(
523 "CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services",
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,
536 # we defer the actual work to the create_root_key() function,
537 # which exists so that the samba-tool tests can borrow that
540 root_key_guid, root_key_dn = create_root_key(
543 current_nt_time=self.current_nt_time(
545 # Allow for clock skew.
546 offset=timedelta_from_nt_time_delta(MAX_CLOCK_SKEW),
548 use_start_time=use_start_time,
549 hash_algorithm=hash_algorithm,
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
558 self.addCleanup(delete_force, samdb, root_key_dn)
559 self.assertEqual(guid, root_key_guid)
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.
582 guid = misc.GUID(secrets.token_bytes(16))
585 data = secrets.token_bytes(KEY_LEN_BYTES)
587 create_time = current_nt_time
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.
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
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
608 current_interval_start_nt_time = Gkid.from_nt_time(
611 use_start_time = NtTime(
612 current_interval_start_nt_time + KEY_CYCLE_DURATION + MAX_CLOCK_SKEW
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
620 raise ValueError("use_start_time should be a datetime or int")
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)
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
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"
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"
657 assert len(field_order) == len(generator)
659 key_length = len(field_order)
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)
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"
672 root_key_dn.add_child(f"CN={guid}")
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.
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": (
689 ), # comes from Server Configuration.
690 "msKds-SecretAgreementAlgorithmID": "DH", # comes from Server Configuration.
691 "msKds-SecretAgreementParam": (
693 ), # comes from Server Configuration.
694 "msKds-PublicKeyLength": "2048", # comes from Server Configuration.
695 "msKds-PrivateKeyLength": (
697 ), # comes from Server Configuration. [MS-GKDI] claims this defaults to ‘256’.
699 if kdf_parameters is not None:
700 details["msKds-KDFParam"] = kdf_parameters # comes from Server Configuration.
704 return (guid, root_key_dn)