]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
librpc/idl: Add idl for msDS-KeyCredentialLink
authorGary Lockyer <gary@catalyst.net.nz>
Tue, 10 Jun 2025 21:04:01 +0000 (09:04 +1200)
committerDouglas Bagnall <dbagnall@samba.org>
Thu, 19 Jun 2025 00:08:31 +0000 (00:08 +0000)
Idl and supporting helpers for msDS-KeyCredentialLinks.
See [MS-ADTS] 2.2.20 Key Credential Link Structures

Currently the KeyMaterial is treated as a binary blob

The naming and casing of the variable names is close as is possible to
those in the specification.

Signed-off-by: Gary Lockyer <gary@catalyst.net.nz>
Reviewed-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Autobuild-User(master): Douglas Bagnall <dbagnall@samba.org>
Autobuild-Date(master): Thu Jun 19 00:08:31 UTC 2025 on atb-devel-224

librpc/idl/keycredlink.idl [new file with mode: 0644]
librpc/idl/wscript_build
librpc/ndr/ndr_keycredlink.c [new file with mode: 0644]
librpc/ndr/ndr_keycredlink.h [new file with mode: 0644]
librpc/wscript_build
python/samba/tests/key_credential_link.py [new file with mode: 0755]
selftest/tests.py
source4/librpc/wscript_build

diff --git a/librpc/idl/keycredlink.idl b/librpc/idl/keycredlink.idl
new file mode 100644 (file)
index 0000000..6492df5
--- /dev/null
@@ -0,0 +1,167 @@
+/*
+   Definitions for packing and unpacking of msDS-KeyCredentialLink
+   structures, derived from [MS-ADTS] 2.2.20 Key Credential Link Structures
+
+   Note: - KeyMaterial is treated as a binary blob
+         - KEYCREDENTIALLINK_ENTRY ordering by identifier not enforced
+        - Presence of the mandatory KEYCREDENTIALLINK_ENTRYs Key_ID,
+          KeyMaterial and KeyUsage is not enforced
+*/
+
+#include "idl_types.h"
+
+[
+  pointer_default(unique),
+  helper("../librpc/ndr/ndr_keycredlink.h")
+]
+interface keycredlink
+{
+       /* Public structures. */
+
+       typedef [enum8bit, public] enum {
+               KeyID = 0x01,
+               KeyHash = 0x02,
+               KeyMaterial = 0x03,
+               KeyUsage = 0x04,
+               KeySource = 0x05,
+               DeviceId = 0x06,
+               CustomKeyInformation = 0x07,
+               KeyApproximateLastLogonTimeStamp = 0x08,
+               KeyCreationTime = 0x09
+       } KEYCREDENTIALLINK_ENTRY_Identifier;
+
+       typedef [enum8bit, public] enum {
+               KEY_USAGE_NGC  = 0x01,
+               KEY_USAGE_FIDO = 0x02,
+               KEY_USAGE_FEK  = 0x03
+       } KEYCREDENTIALLINK_ENTRY_KeyUsage;
+
+       typedef [enum8bit, public] enum {
+               KEY_SOURCE_AD = 0x00
+       } KEYCREDENTIALLINK_ENTRY_KeySource;
+
+       typedef [bitmap8bit, public] bitmap {
+               CUSTOM_KEY_INFO_FLAGS_ATTESTATION  = 0x01,
+               /* Reserved for future use */
+               CUSTOM_KEY_INFO_FLAGS_MFA_NOT_USED = 0x02
+               /*
+                * During creation of this key, the requesting client
+                * authenticated using only a single credential.
+                */
+       } CUSTOM_KEY_INFO_Flags;
+
+       typedef [enum8bit, public] enum {
+               Unspecified = 0x00,    // No volume specified
+                                      // defined as None in the docs but this
+                                      // causes issues in the python bindings
+               OSV  = 0x01,    // Operating system volume
+               FDV  = 0x02,    // Fixed data volume
+               RDV  = 0x03     // Removable data volume
+       } CUSTOM_KEY_INFO_VolType;
+
+       typedef [enum8bit, public] enum {
+               Unsupported = 0x00,    // Notification is not supported
+                                      // defined as None in the docs but this
+                                      // causes issues in the python bindings
+               Supported   = 0x01     // Notification is supported
+       } CUSTOM_KEY_INFO_SupportsNotification;
+
+       typedef [enum8bit, public] enum {
+               Unknown = 0x00,
+               Weak    = 0x01,
+               Normal  = 0x02
+       } CUSTOM_KEY_INFO_KeyStrength;
+
+       /*
+        * Extended custom key information
+        */
+       typedef [public, flag(NDR_NOALIGN)] struct {
+               [value(0)] uint8 version;
+               uint8 size;
+               uint8 data[size];
+               /*
+                * A Concise Binary Object Representation (CBOR)-encoded blob
+                * whose length is specified by the Size field.
+                * CBOR is a binary data serialization format defined in
+                * [RFC7049]. The contents of this field are opaque and
+                * have no behavioural impact on the protocol.
+                */
+       } EncodedExtendedCKI;
+
+       /*
+        * This structure has two possible representations which are
+        * differentiated by the sized of the encoded data.
+        *
+        * a) only the Version and Flags fields are present;
+        *    and the structure has a size of 2 bytes.
+        * b) all additional are also present
+        *    - the structure's total size is variable but not 2
+        *
+        * The boolean isExtended attribute is used to indicate which version
+        * was unpacked or should be packed.
+        *
+        * Note: isExtended and count are not present in the packed binary
+        *       representation
+        */
+
+       typedef [nopush, nopull] struct {
+               [value(1)] uint8 version;
+               CUSTOM_KEY_INFO_Flags flags;
+               boolean8 isExtended;
+               /*
+                * Not present in packed representation indicates
+                * that the following fields are present
+                */
+               CUSTOM_KEY_INFO_VolType volType;
+               CUSTOM_KEY_INFO_SupportsNotification supportsNotification;
+               [value(1)] uint8 fekKeyVersion;
+               CUSTOM_KEY_INFO_KeyStrength keyStrength;
+               uint8 reserved[10];     /* Reserved bytes not currently used */
+               uint32 count;
+               /* Not present in packed representation size cki array */
+               EncodedExtendedCKI cki[count];
+       } CUSTOM_KEY_INFORMATION;
+
+       typedef [switch_type(KEYCREDENTIALLINK_ENTRY_Identifier),
+                public,
+                nopull,
+                nodiscriminant,
+                gensize,
+                flag(NDR_NOALIGN)]
+       union {
+       [case(KeyID)]
+               uint8 keyId[32];
+       [case(KeyHash)]
+               uint8 keyHash[32];
+       [case(KeyUsage)]
+               KEYCREDENTIALLINK_ENTRY_KeyUsage keyUsage;
+       [case(KeySource), value(KEY_SOURCE_AD)]
+               KEYCREDENTIALLINK_ENTRY_KeySource keySource;
+       [case(KeyMaterial)] [flag(NDR_REMAINING)]
+               DATA_BLOB keyMaterial;
+               /* Currently treating Key Material as an opaque binary blob */
+       [case(DeviceId)]
+               uint8 deviceId[16];
+       [case(CustomKeyInformation)]
+               CUSTOM_KEY_INFORMATION customKeyInformation;
+       [case(KeyApproximateLastLogonTimeStamp)]
+               NTTIME lastLogon;
+       [case(KeyCreationTime)]
+               NTTIME created;
+       } KEYCREDENTIALLINK_ENTRY_Value;
+
+       typedef [public, nopull, flag(NDR_NOALIGN)] struct {
+               [value(
+                       ndr_size_KEYCREDENTIALLINK_ENTRY_Value(
+                               &value,identifier,ndr->flags))]
+                       uint16 length;
+               KEYCREDENTIALLINK_ENTRY_Identifier identifier;
+               [switch_is(identifier)] KEYCREDENTIALLINK_ENTRY_Value value;
+       } KEYCREDENTIALLINK_ENTRY;
+
+       typedef [public, nopull, nopush, flag(NDR_NOALIGN)] struct {
+               [value(0x0200)] uint32 version;
+               uint32 count;
+               KEYCREDENTIALLINK_ENTRY entries[count];
+       } KEYCREDENTIALLINK_BLOB;
+}
index c7d6413b47fd687e60e2234566c71e4ef1fc598f..8a8f97d8592fa30a2a4596ae051cdf5cff757dfd 100644 (file)
@@ -132,6 +132,7 @@ bld.SAMBA_PIDL_LIST('PIDL',
                     drsblobs.idl
                     gmsa.idl
                     idmap.idl
+                    keycredlink.idl
                     krb5pac.idl
                     krb5ccache.idl
                     schannel.idl
diff --git a/librpc/ndr/ndr_keycredlink.c b/librpc/ndr/ndr_keycredlink.c
new file mode 100644 (file)
index 0000000..9672029
--- /dev/null
@@ -0,0 +1,441 @@
+/*
+   Unix SMB/CIFS implementation.
+
+   Support routines for packing and unpacking of msDS-KeyCredentialLink
+   structures.
+
+   See [MS-ADTS] 2.2.20 Key Credential Link Structures
+
+   Copyright (C) Gary Lockyer 2025
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "lib/replace/replace.h"
+#include "librpc/gen_ndr/ndr_keycredlink.h"
+#include "gen_ndr/keycredlink.h"
+#include "libndr.h"
+#include <assert.h>
+
+/*
+ * The KEYCREDENTIALLINK_BLOB consists of the version and a series of variable
+ * length KEYCREDENTIALLINK_ENTRIES.
+ */
+enum ndr_err_code ndr_pull_KEYCREDENTIALLINK_BLOB(
+       struct ndr_pull *ndr,
+       ndr_flags_type ndr_flags,
+       struct KEYCREDENTIALLINK_BLOB *blob)
+{
+       libndr_flags _flags_save_STRUCT = ndr->flags;
+       ndr_set_flags(&ndr->flags, LIBNDR_FLAG_NOALIGN);
+
+       NDR_CHECK(ndr_pull_uint32(ndr, ndr_flags, &blob->version));
+       if (blob->version != 0x0200) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_RANGE,
+                                     "Invalid version of (0x%04x) "
+                                     "should be 0x0200, at byte %zu\n",
+                                     blob->version,
+                                     (ndr->offset - sizeof(uint32_t)));
+       }
+       blob->count = 0;
+       blob->entries = talloc_array(ndr->current_mem_ctx,
+                                    struct KEYCREDENTIALLINK_ENTRY,
+                                    blob->count);
+       if (blob->entries == NULL) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_ALLOC,
+                                     "Failed to pull KEYCREDENTIALLINK_ENTRY");
+       }
+       while (ndr->offset < ndr->data_size) {
+               blob->entries = talloc_realloc(ndr->current_mem_ctx,
+                                              blob->entries,
+                                              struct KEYCREDENTIALLINK_ENTRY,
+                                              blob->count + 1);
+               if (blob->entries == NULL) {
+                       return ndr_pull_error(
+                               ndr,
+                               NDR_ERR_ALLOC,
+                               "Failed to pull KEYCREDENTIALLINK_ENTRY");
+               }
+               NDR_CHECK(ndr_pull_KEYCREDENTIALLINK_ENTRY(
+                       ndr, ndr_flags, &blob->entries[blob->count]));
+               blob->count++;
+       }
+       ndr->flags = _flags_save_STRUCT;
+       return NDR_ERR_SUCCESS;
+}
+
+enum ndr_err_code ndr_push_KEYCREDENTIALLINK_BLOB(
+       struct ndr_push *ndr,
+       ndr_flags_type ndr_flags,
+       const struct KEYCREDENTIALLINK_BLOB *blob)
+{
+       int i = 0;
+
+       if (blob->version != 0x0200) {
+               return ndr_push_error(ndr,
+                                     NDR_ERR_RANGE,
+                                     "Invalid version of (0x%04x) "
+                                     "should be 0x0200, at byte %zu\n",
+                                     blob->version,
+                                     (ndr->offset - sizeof(uint32_t)));
+       }
+       NDR_CHECK(ndr_push_uint32(ndr, ndr_flags, blob->version));
+
+       for (i = 0; i < blob->count; i++) {
+               NDR_CHECK(ndr_push_KEYCREDENTIALLINK_ENTRY(ndr,
+                                                          ndr_flags,
+                                                          &blob->entries[i]));
+       }
+       return NDR_ERR_SUCCESS;
+}
+
+/*
+ * To pull the CUSTOM_KEY_INFORMATION the length from the enclosing
+ * KEYCREDENTIALLINK_ENTRY needs to be passed in.
+ *
+ * CUSTOM_KEY_INFORMATION has two representations based on the size parameter
+ *
+ * If size is 2 only the version and flags are expected.
+ * If the size is greater than 2 then
+ *    version, flags, volType, supportsNotification, fekKeyVersion,
+ *    keyStrength and the reserved bytes are expected
+ *    Optionally followed by a series of EncodedExtendedCKI entries
+ *
+ */
+static enum ndr_err_code pull_cki(struct ndr_pull *ndr,
+                                 ndr_flags_type ndr_flags,
+                                 struct CUSTOM_KEY_INFORMATION *info,
+                                 uint32_t size)
+{
+       /* Calculate the end of the CUSTOM_KEY_INFORMATION in the raw bytes */
+       uint32_t end_offset = ndr->offset + size;
+
+       /*
+        * Initialise the CUSTOM_KEY_INFORMATION, in case this is the
+        * short form.
+        */
+       *info = (struct CUSTOM_KEY_INFORMATION){0};
+
+       NDR_CHECK(ndr_pull_uint8(ndr, ndr_flags, &info->version));
+       if (info->version != 0x01) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_RANGE,
+                                     "Invalid version of (0x%02x) "
+                                     "should be 0x01, at byte %zu\n",
+                                     info->version,
+                                     (ndr->offset - sizeof(uint8_t)));
+       }
+       NDR_CHECK(ndr_pull_CUSTOM_KEY_INFO_Flags(ndr, ndr_flags, &info->flags));
+
+       if (size == 2) {
+               info->isExtended = false;
+               return NDR_ERR_SUCCESS;
+       }
+       info->isExtended = true;
+       NDR_CHECK(ndr_pull_CUSTOM_KEY_INFO_VolType(ndr,
+                                                  ndr_flags,
+                                                  &info->volType));
+       NDR_CHECK(ndr_pull_CUSTOM_KEY_INFO_SupportsNotification(
+               ndr, ndr_flags, &info->supportsNotification));
+       NDR_CHECK(ndr_pull_uint8(ndr, ndr_flags, &info->fekKeyVersion));
+       if (info->fekKeyVersion != 0x01) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_RANGE,
+                                     "Invalid fekKeyVersion of (0x%02x) "
+                                     "should be 0x01, at byte %zu\n",
+                                     info->fekKeyVersion,
+                                     (ndr->offset - sizeof(uint8_t)));
+       }
+       NDR_CHECK(ndr_pull_CUSTOM_KEY_INFO_KeyStrength(ndr,
+                                                      ndr_flags,
+                                                      &info->keyStrength));
+       NDR_CHECK(ndr_pull_array_uint8(ndr, ndr_flags, info->reserved, 10));
+
+       /* Pull the EncodedExtendedCKI values */
+       info->count = 0;
+       info->cki = talloc_array(ndr->current_mem_ctx,
+                                struct EncodedExtendedCKI,
+                                info->count);
+       if (info->cki == NULL) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_ALLOC,
+                                     "Failed to pull EncodedExtendCKI");
+       }
+       while (ndr->offset < end_offset) {
+               info->cki = talloc_realloc(ndr->current_mem_ctx,
+                                          info->cki,
+                                          struct EncodedExtendedCKI,
+                                          info->count + 1);
+               if (info->cki == NULL) {
+                       return ndr_pull_error(
+                               ndr,
+                               NDR_ERR_ALLOC,
+                               "Failed to pull EncodedExtendedCKI");
+               }
+               NDR_CHECK(ndr_pull_EncodedExtendedCKI(ndr,
+                                                     ndr_flags,
+                                                     &info->cki[info->count]));
+               info->count++;
+       }
+       return NDR_ERR_SUCCESS;
+}
+
+/*
+ * CUSTOM_KEY-INFORMATION has two representations with differing sizes
+ * the flag isExtended controls which version is written.
+ */
+enum ndr_err_code ndr_push_CUSTOM_KEY_INFORMATION(
+       struct ndr_push *ndr,
+       ndr_flags_type ndr_flags,
+       const struct CUSTOM_KEY_INFORMATION *info)
+{
+       int i = 0;
+
+       if (info->version != 0x01) {
+               return ndr_push_error(ndr,
+                                     NDR_ERR_RANGE,
+                                     "Invalid version of (0x%02x) "
+                                     "should be 0x01, at byte %zu\n",
+                                     info->version,
+                                     (ndr->offset - sizeof(uint8_t)));
+       }
+       NDR_CHECK(ndr_push_uint8(ndr, ndr_flags, info->version));
+       NDR_CHECK(ndr_push_CUSTOM_KEY_INFO_Flags(ndr, ndr_flags, info->flags));
+       if (!info->isExtended) {
+               return NDR_ERR_SUCCESS;
+       }
+
+       NDR_CHECK(ndr_push_CUSTOM_KEY_INFO_VolType(ndr,
+                                                  ndr_flags,
+                                                  info->volType));
+       NDR_CHECK(ndr_push_CUSTOM_KEY_INFO_SupportsNotification(
+               ndr, ndr_flags, info->supportsNotification));
+       if (info->fekKeyVersion != 0x01) {
+               return ndr_push_error(ndr,
+                                     NDR_ERR_RANGE,
+                                     "Invalid fekKeyVersion of (0x%02x) "
+                                     "should be 0x01, at byte %zu\n",
+                                     info->fekKeyVersion,
+                                     (ndr->offset - sizeof(uint8_t)));
+       }
+       NDR_CHECK(ndr_push_uint8(ndr, ndr_flags, info->fekKeyVersion));
+       NDR_CHECK(ndr_push_CUSTOM_KEY_INFO_KeyStrength(ndr,
+                                                      ndr_flags,
+                                                      info->keyStrength));
+       NDR_CHECK(ndr_push_array_uint8(ndr, ndr_flags, info->reserved, 10));
+
+       for (i = 0; i < info->count; i++) {
+               NDR_CHECK(ndr_push_EncodedExtendedCKI(ndr,
+                                                     ndr_flags,
+                                                     &info->cki[i]));
+       }
+       return NDR_ERR_SUCCESS;
+}
+
+/*
+ * To pull a KEYCREDENTIALLINK_Value the length from the enclosing
+ * KEYCREDENTIALLINK_ENTRY needs to be passed in.
+ *
+ */
+static enum ndr_err_code ndr_pull_value(struct ndr_pull *ndr,
+                                       ndr_flags_type ndr_flags,
+                                       union KEYCREDENTIALLINK_ENTRY_Value *r,
+                                       uint32_t size)
+{
+       uint32_t level;
+       const size_t header_len = sizeof(uint16_t) + sizeof(uint8_t);
+       const size_t identifier_len = sizeof(uint8_t);
+       libndr_flags flags_save = ndr->flags;
+
+       /* this function should only be called if NDR_SCALARS is set */
+       assert(ndr_flags & NDR_SCALARS);
+
+       ndr_set_flags(&ndr->flags, LIBNDR_FLAG_NOALIGN);
+
+       /* This token is not used again */
+       NDR_CHECK(ndr_pull_steal_switch_value(ndr, r, &level));
+
+       switch (level) {
+       case KeyID: {
+               if (size != 32) {
+                       return ndr_pull_error(ndr,
+                                             NDR_ERR_ARRAY_SIZE,
+                                             "Invalid size of (%" PRIu32
+                                             ") for KeyID "
+                                             "should be (32), at byte %zu\n",
+                                             size,
+                                             (ndr->offset - header_len));
+               }
+               NDR_CHECK(
+                       ndr_pull_array_uint8(ndr, NDR_SCALARS, r->keyId, size));
+               break;
+       }
+
+       case KeyHash: {
+               if (size != 32) {
+                       return ndr_pull_error(ndr,
+                                             NDR_ERR_ARRAY_SIZE,
+                                             "Invalid size of (%" PRIu32
+                                             ") for KeyHash "
+                                             "should be (32), at byte %zu\n",
+                                             size,
+                                             (ndr->offset - header_len));
+               }
+               NDR_CHECK(ndr_pull_array_uint8(
+                       ndr, NDR_SCALARS, r->keyHash, size));
+               break;
+       }
+
+       case KeyUsage: {
+               if (size != 1) {
+                       return ndr_pull_error(ndr,
+                                             NDR_ERR_LENGTH,
+                                             "Invalid length of (%" PRIu32
+                                             ") for KeyUsage "
+                                             "should be (1), at byte %zu\n",
+                                             size,
+                                             (ndr->offset - header_len));
+               }
+               NDR_CHECK(ndr_pull_KEYCREDENTIALLINK_ENTRY_KeyUsage(
+                       ndr, NDR_SCALARS, &r->keyUsage));
+               break;
+       }
+
+       case KeySource: {
+               if (size != 1) {
+                       return ndr_pull_error(ndr,
+                                             NDR_ERR_LENGTH,
+                                             "Invalid length of (%" PRIu32
+                                             ") for KeySource "
+                                             "should be (1), at byte %zu\n",
+                                             size,
+                                             (ndr->offset - header_len));
+               }
+               NDR_CHECK(ndr_pull_KEYCREDENTIALLINK_ENTRY_KeySource(
+                       ndr, NDR_SCALARS, &r->keySource));
+               break;
+       }
+
+       case KeyMaterial: {
+               if (size == 0) {
+                       return ndr_pull_error(
+                               ndr,
+                               NDR_ERR_LENGTH,
+                               "Invalid length of (%" PRIu32
+                               ") for keyMaterial "
+                               "should be non zero, at byte %zu\n",
+                               size,
+                               (ndr->offset - header_len));
+               }
+               NDR_PULL_NEED_BYTES(ndr, size);
+               r->keyMaterial = data_blob_talloc(ndr->current_mem_ctx,
+                                                 ndr->data + ndr->offset,
+                                                 size);
+               if (r->keyMaterial.data == NULL) {
+                       return ndr_pull_error(ndr,
+                                             NDR_ERR_ALLOC,
+                                             "Failed to pull keyMaterial");
+               }
+               ndr->offset += size;
+               break;
+       }
+
+       case DeviceId: {
+               if (size != 16) {
+                       return ndr_pull_error(ndr,
+                                             NDR_ERR_ARRAY_SIZE,
+                                             "Invalid size of (%" PRIu32
+                                             ") for KeySource "
+                                             "should be (1), at byte %zu\n",
+                                             size,
+                                             (ndr->offset - header_len));
+               }
+               NDR_CHECK(ndr_pull_array_uint8(
+                       ndr, NDR_SCALARS, r->deviceId, size));
+               break;
+       }
+
+       case CustomKeyInformation: {
+               NDR_CHECK(pull_cki(
+                       ndr, NDR_SCALARS, &r->customKeyInformation, size));
+               break;
+       }
+
+       case KeyApproximateLastLogonTimeStamp: {
+               if (size != 8) {
+                       return ndr_pull_error(
+                               ndr,
+                               NDR_ERR_LENGTH,
+                               "Invalid length of (%" PRIu32 ") for "
+                               "KeyApproximateLastLogonTimeStamp "
+                               "should be (8), at byte %zu\n",
+                               size,
+                               (ndr->offset - header_len));
+               }
+               NDR_CHECK(ndr_pull_NTTIME(ndr, NDR_SCALARS, &r->lastLogon));
+               break;
+       }
+
+       case KeyCreationTime: {
+               if (size != 8) {
+                       return ndr_pull_error(ndr,
+                                             NDR_ERR_RANGE,
+                                             "Invalid size of (%" PRIu32
+                                             ") for "
+                                             "KeyCreationTime "
+                                             "should be (8), at byte %zu\n",
+                                             size,
+                                             (ndr->offset - header_len));
+               }
+               NDR_CHECK(ndr_pull_NTTIME(ndr, NDR_SCALARS, &r->created));
+               break;
+       }
+
+       default:
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_BAD_SWITCH,
+                                     "Bad switch value %02x at byte %zu",
+                                     level,
+                                     ndr->offset - identifier_len);
+       }
+       ndr->flags = flags_save;
+       return NDR_ERR_SUCCESS;
+}
+
+/*
+ * Need to pass the length element of the KEYCREDENTIALLINK_ENTRY down to
+ * ndr_pull_value, the code that pulls the KEYCREDENTIALLINK_ENTRY_Value.
+ */
+enum ndr_err_code ndr_pull_KEYCREDENTIALLINK_ENTRY(
+       struct ndr_pull *ndr,
+       ndr_flags_type ndr_flags,
+       struct KEYCREDENTIALLINK_ENTRY *r)
+{
+       libndr_flags _flags_save_STRUCT = ndr->flags;
+       ndr_set_flags(&ndr->flags, LIBNDR_FLAG_NOALIGN);
+       if (ndr_flags & NDR_SCALARS) {
+               NDR_CHECK(ndr_pull_uint16(ndr, NDR_SCALARS, &r->length));
+               NDR_CHECK(ndr_pull_KEYCREDENTIALLINK_ENTRY_Identifier(
+                       ndr, NDR_SCALARS, &r->identifier));
+               NDR_CHECK(ndr_pull_set_switch_value(ndr,
+                                                   &r->value,
+                                                   r->identifier));
+               NDR_CHECK(
+                       ndr_pull_value(ndr, NDR_SCALARS, &r->value, r->length));
+       }
+       ndr->flags = _flags_save_STRUCT;
+       return NDR_ERR_SUCCESS;
+}
diff --git a/librpc/ndr/ndr_keycredlink.h b/librpc/ndr/ndr_keycredlink.h
new file mode 100644 (file)
index 0000000..48baafa
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+   Unix SMB/CIFS implementation.
+
+   Support routines for packing and unpacking of msDS-KeyCredentialLink
+   structures.
+
+   See [MS-ADTS] 2.2.20 Key Credential Link Structures
+
+   Copyright (C) Gary Lockyer 2025
+
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
index dedba157d9f6095a84c66e0169c0242587eee075..f8a2a28cff1573a4efd5767567bdd33a7cb0a0a6 100644 (file)
@@ -647,7 +647,8 @@ bld.SAMBA_LIBRARY('ndr-samba',
     NDR_DNSSERVER NDR_EPMAPPER NDR_XATTR NDR_UNIXINFO NDR_NAMED_PIPE_AUTH
     NDR_NTPRINTING NDR_FSRVP NDR_WITNESS NDR_MDSSVC NDR_OPEN_FILES NDR_SMBXSRV
     NDR_SMB3POSIX NDR_RPCD_WITNESS
-    NDR_KRB5CCACHE NDR_WSP NDR_GKDI NDR_GMSA''',
+    NDR_KRB5CCACHE NDR_WSP NDR_GKDI NDR_GMSA
+    NDR_KEYCREDLINK''',
     private_library=True,
     grouping_library=True
     )
@@ -755,6 +756,11 @@ bld.SAMBA_SUBSYSTEM('NDR_FSRVP_STATE',
     source='gen_ndr/ndr_fsrvp_state.c',
     public_deps='ndr'
     )
+
+bld.SAMBA_SUBSYSTEM('NDR_KEYCREDLINK',
+    source='ndr/ndr_keycredlink.c gen_ndr/ndr_keycredlink.c',
+    public_deps='ndr'
+    )
 #
 # Cmocka tests
 #
diff --git a/python/samba/tests/key_credential_link.py b/python/samba/tests/key_credential_link.py
new file mode 100755 (executable)
index 0000000..b73c6a1
--- /dev/null
@@ -0,0 +1,555 @@
+#!/usr/bin/env python3
+# Tests for NDR packing and unpacking of msDS-KeyCredentialLink structures
+#
+# See [MS-ADTS] 2.2.20 Key Credential Link Structures
+#
+# Copyright (C) Gary Lockyer 2025
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import sys
+import os
+
+sys.path.insert(0, "bin/python")
+os.environ["PYTHONUNBUFFERED"] = "1"
+
+from samba.dcerpc import keycredlink
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.tests import TestCase
+
+
+class KeyCredentialLinkTests(TestCase):
+    def test_unpack_empty_key_blob(self):
+        """ensure that a minimal KEYCREDENTIALLINK_BLOB (only the version)
+        can be unpacked, then packed into an identical bytes
+        """
+        empty_key_blob = bytes.fromhex(
+            "00 02 00 00"  # Version 2 value 0x00000200
+        )
+        blob = ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, empty_key_blob)
+
+        self.assertEqual(blob.version, 0x0200)
+        self.assertEqual(blob.count, 0)
+        self.assertEqual(len(blob.entries), 0)
+
+        packed = ndr_pack(blob)
+        self.assertEqual(empty_key_blob, packed)
+
+    def test_unpack_empty_key_blob_invalid_version(self):
+        """ensure that a KEYCREDENTIALLINK_BLOB with an invalid version
+        is rejected.
+        """
+        invalid_version_key_blob = bytes.fromhex(
+            "00 03 00 00"  # Version 3 value 0x00000300
+        )
+        with self.assertRaises(RuntimeError) as e:
+            ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, invalid_version_key_blob)
+
+        self.assertEqual(e.exception.args[0], 13)
+        self.assertEqual(e.exception.args[1], "Range Error")
+
+    def test_unpack_short_key_blob(self):
+        """ensure that a KEYCREDENTIALLINK_BLOB with only 3 bytes
+        is rejected.
+        """
+        short_key_blob = bytes.fromhex(
+            "00 02 00"  # Version 2 value 0x00000200
+        )
+        with self.assertRaises(RuntimeError) as e:
+            ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, short_key_blob)
+
+        self.assertEqual(e.exception.args[0], 11)
+        self.assertEqual(e.exception.args[1], "Buffer Size Error")
+
+    def test_unpack_KeyId(self):
+        """ensure that a KEYCREDENTIALLINK_BLOB with a keyId
+        is correctly packed and unpacked.
+        """
+        source = bytes.fromhex(
+            "00 02 00 00"  # Version 2 value 0x00000200
+            "20 00"  # 32 bytes of data
+            "01"  # a Key Id
+            "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
+            "10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
+        )
+        key_id = bytes.fromhex(
+            "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
+            "10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
+        )
+        blob = ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, source)
+
+        self.assertEqual(blob.version, 0x0200)
+        self.assertEqual(blob.count, 1)
+        self.assertEqual(len(blob.entries), 1)
+        self.assertEqual(blob.entries[0].length, 32)
+        self.assertEqual(blob.entries[0].identifier, keycredlink.KeyID)
+        self.assertEqual(bytes(blob.entries[0].value), key_id)
+
+        packed = ndr_pack(blob)
+        self.assertEqual(source, packed)
+
+    def test_unpack_KeyHash(self):
+        """ensure that a KEYCREDENTIALLINK_BLOB with a keyHash
+        is correctly packed and unpacked.
+        """
+        key_blob_key_source = bytes.fromhex(
+            "00 02 00 00"  # Version 2 value 0x00000200
+            "20 00"  # 32 bytes of data
+            "02"  # a Key Hash
+            "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
+            "10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
+        )
+        key_hash = bytes.fromhex(
+            "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
+            "10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
+        )
+        blob = ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, key_blob_key_source)
+
+        self.assertEqual(blob.version, 0x0200)
+        self.assertEqual(blob.count, 1)
+        self.assertEqual(len(blob.entries), 1)
+        self.assertEqual(blob.entries[0].length, 32)
+        self.assertEqual(blob.entries[0].identifier, keycredlink.KeyHash)
+        self.assertEqual(bytes(blob.entries[0].value), key_hash)
+
+        packed = ndr_pack(blob)
+        self.assertEqual(key_blob_key_source, packed)
+
+    def test_unpack_KeyUsage(self):
+        """ensure that a KEYCREDENTIALLINK_BLOB with a keyUsage
+        is correctly packed and unpacked.
+        """
+        key_blob_key_source = bytes.fromhex(
+            "00 02 00 00"  # Version 2 value 0x00000200
+            "01 00"  # 1 byte of data
+            "04"  # a Key Usage
+            "01"  # KEY_USAGE_NGC
+        )
+        blob = ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, key_blob_key_source)
+
+        self.assertEqual(blob.version, 0x0200)
+        self.assertEqual(blob.count, 1)
+        self.assertEqual(len(blob.entries), 1)
+        self.assertEqual(blob.entries[0].length, 1)
+        self.assertEqual(blob.entries[0].identifier, keycredlink.KeyUsage)
+        self.assertEqual(blob.entries[0].value, keycredlink.KEY_USAGE_NGC)
+
+        packed = ndr_pack(blob)
+        self.assertEqual(key_blob_key_source, packed)
+
+    def test_unpack_KeySource(self):
+        """ensure that a KEYCREDENTIALLINK_BLOB with a keySource
+        is correctly packed and unpacked.
+        """
+        blob_source = bytes.fromhex(
+            "00 02 00 00"  # Version 2 value 0x00000200
+            "01 00"  # 1 byte of data
+            "05"  # a Key Source
+            "00"  # KEY_SOURCE_AD
+        )
+        blob = ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, blob_source)
+
+        self.assertEqual(blob.version, 0x0200)
+        self.assertEqual(blob.count, 1)
+        self.assertEqual(len(blob.entries), 1)
+        self.assertEqual(blob.entries[0].length, 1)
+        self.assertEqual(blob.entries[0].identifier, keycredlink.KeySource)
+        self.assertEqual(blob.entries[0].value, keycredlink.KEY_SOURCE_AD)
+
+        packed = ndr_pack(blob)
+        self.assertEqual(blob_source, packed)
+
+    def test_unpack_DeviceId(self):
+        """ensure that a KEYCREDENTIALLINK_BLOB with a deviceId
+        is correctly packed and unpacked.
+        """
+        blob_source = bytes.fromhex(
+            "00 02 00 00"  # Version 2 value 0x00000200
+            "10 00"  # 16 bytes of data
+            "06"  # a Device Id
+            "00 01 02 03 04 05 06 07"
+            "08 09 0A 0B 0C 0D 0E 0F"
+        )
+        device_id = bytes.fromhex("00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F")
+
+        blob = ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, blob_source)
+
+        self.assertEqual(blob.version, 0x0200)
+        self.assertEqual(blob.count, 1)
+        self.assertEqual(len(blob.entries), 1)
+        self.assertEqual(blob.entries[0].length, 16)
+        self.assertEqual(blob.entries[0].identifier, keycredlink.DeviceId)
+        self.assertEqual(bytes(blob.entries[0].value), device_id)
+
+        packed = ndr_pack(blob)
+        self.assertEqual(blob_source, packed)
+
+    def test_unpack_CustomKeyInformation(self):
+        """ensure that a KEYCREDENTIALLINK_BLOB with a short custom key
+        (2 bytes) is correctly packed and unpacked.
+        """
+        blob_source = bytes.fromhex(
+            "00 02 00 00"  # Version 2 value 0x00000200
+            "02 00"  # 2 bytes of data
+            "07"  # Custom Key Information
+            "01"  # Version 1
+            "02"  # Flags MFA not used
+        )
+
+        blob = ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, blob_source)
+
+        self.assertEqual(blob.version, 0x0200)
+        self.assertEqual(blob.count, 1)
+        self.assertEqual(len(blob.entries), 1)
+        self.assertEqual(blob.entries[0].length, 2)
+        self.assertEqual(blob.entries[0].identifier, keycredlink.CustomKeyInformation)
+        self.assertEqual(blob.entries[0].value.version, 1)
+        self.assertEqual(blob.entries[0].value.flags, 0x02)
+        self.assertFalse(blob.entries[0].value.isExtended)
+
+        # The remaining fields should have been set to zeros
+        zeros = bytes.fromhex("00 00 00 00 00 00 00 00 00 00")
+        self.assertEqual(blob.entries[0].value.volType, 0x00)
+        self.assertEqual(blob.entries[0].value.supportsNotification, 0x00)
+        self.assertEqual(blob.entries[0].value.fekKeyVersion, 0x00)
+        self.assertEqual(blob.entries[0].value.keyStrength, 0x00)
+        self.assertEqual(bytes(blob.entries[0].value.reserved), zeros)
+        self.assertEqual(blob.entries[0].value.count, 0)
+        self.assertEqual(len(blob.entries[0].value.cki), 0)
+
+        packed = ndr_pack(blob)
+        self.assertEqual(blob_source, packed)
+
+    def test_unpack_CustomKeyInformationExtendedNoCki(self):
+        """ensure that a KEYCREDENTIALLINK_BLOB with long custom key
+        information (16 bytes), and no EncodedExtendedCki is
+        correctly packed and unpacked.
+        """
+        blob_source = bytes.fromhex(
+            "00 02 00 00"  # Version 2 value 0x00000200
+            "10 00"  # 16 bytes of data
+            "07"  # Custom Key Information
+            "01"  # Version 1
+            "03"  # Flags MFA not used and attestation
+            "02"  # fixed volume
+            "01"  # Notification supported
+            "01"  # Fek Key Version
+            "01"  # weak key strength
+            "00 00 00 00 00 00 00 00 00 00"  # reserved space
+        )
+        reserved = bytes.fromhex("00 00 00 00 00 00 00 00 00 00")
+
+        blob = ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, blob_source)
+
+        self.assertEqual(blob.version, 0x0200)
+        self.assertEqual(blob.count, 1)
+        self.assertEqual(len(blob.entries), 1)
+        self.assertEqual(blob.entries[0].length, 16)
+        self.assertEqual(blob.entries[0].identifier, keycredlink.CustomKeyInformation)
+        self.assertEqual(blob.entries[0].value.version, 1)
+        self.assertEqual(blob.entries[0].value.flags, 0x03)
+        self.assertTrue(blob.entries[0].value.isExtended)
+        self.assertEqual(blob.entries[0].value.volType, keycredlink.FDV)
+        self.assertEqual(
+            blob.entries[0].value.supportsNotification, keycredlink.Supported
+        )
+        self.assertEqual(blob.entries[0].value.fekKeyVersion, 0x01)
+        self.assertEqual(blob.entries[0].value.keyStrength, keycredlink.Weak)
+        self.assertEqual(bytes(blob.entries[0].value.reserved), reserved)
+        self.assertEqual(blob.entries[0].value.count, 0)
+        self.assertEqual(len(blob.entries[0].value.cki), 0)
+
+        packed = ndr_pack(blob)
+        self.assertEqual(blob_source, packed)
+
+    def test_unpack_CustomKeyInformationExtendedOneCki(self):
+        """ensure that a KEYCREDENTIALLINK_BLOB with long custom key
+        information (16 bytes), and one EncodedExtendedCki is
+        correctly packed and unpacked.
+        """
+        blob_source = bytes.fromhex(
+            "00 02 00 00"  # Version 2 value 0x00000200
+            "14 00"  # 16 byte header + 4 bytes
+            "   07"  # Custom Key Information
+            "   01"  # Version 1
+            "   00"  # Flags MFA not used
+            "   00"  # No volume type
+            "   00"  # Unsupported
+            "   01"  # Fek Key Version
+            "   00"  # Unknown key strength
+            "   00 00 00 00 00 00 00 00 00 00"  # reserved space
+            "   00 02 0D 0A"
+        )
+        reserved = bytes.fromhex("00 00 00 00 00 00 00 00 00 00")
+
+        blob = ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, blob_source)
+
+        self.assertEqual(blob.version, 0x0200)
+        self.assertEqual(blob.count, 1)
+        self.assertEqual(len(blob.entries), 1)
+        self.assertEqual(blob.entries[0].length, 20)
+        self.assertEqual(blob.entries[0].identifier, keycredlink.CustomKeyInformation)
+        self.assertEqual(blob.entries[0].value.version, 1)
+        self.assertEqual(blob.entries[0].value.flags, 0x00)
+        self.assertTrue(blob.entries[0].value.isExtended)
+        self.assertEqual(blob.entries[0].value.volType, keycredlink.Unspecified)
+        self.assertEqual(
+            blob.entries[0].value.supportsNotification, keycredlink.Unsupported
+        )
+        self.assertEqual(blob.entries[0].value.fekKeyVersion, 1)
+        self.assertEqual(blob.entries[0].value.keyStrength, keycredlink.Unknown)
+        self.assertEqual(bytes(blob.entries[0].value.reserved), reserved)
+        self.assertEqual(blob.entries[0].value.count, 1)
+        self.assertEqual(len(blob.entries[0].value.cki), 1)
+        self.assertEqual(blob.entries[0].value.cki[0].size, 2)
+        self.assertEqual(blob.entries[0].value.cki[0].data, [13, 10])
+        self.assertEqual(len(blob.entries[0].value.cki[0].data), 2)
+
+        packed = ndr_pack(blob)
+        self.assertEqual(blob_source, packed)
+
+    def test_unpack_CustomKeyInformationExtendedTwoCki(self):
+        """ensure that a KEYCREDENTIALLINK_BLOB with long custom key
+        information (16 bytes), and two EncodedExtendedCkis is
+        correctly packed and unpacked.
+        """
+        blob_source = bytes.fromhex(
+            "00 02 00 00"  # Version 2 value 0x00000200
+            "19 00"  # 16 bytes header + 9 bytes CKI info
+            "07"  # Custom Key Information
+            "01"  # Version 1
+            "01"  # Flags Attestation
+            "03"  # Removablevolume
+            "01"  # Notification supported
+            "01"  # Fek Key Version
+            "02"  # Normal key strength
+            "00 00 00 00 00 00 00 00 00 00"  # reserved space
+            "00 02 0D 0A"
+            "00 03 01 02 03"
+        )
+        reserved = bytes.fromhex("00 00 00 00 00 00 00 00 00 00")
+
+        blob = ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, blob_source)
+
+        self.assertEqual(blob.version, 0x0200)
+        self.assertEqual(blob.count, 1)
+        self.assertEqual(len(blob.entries), 1)
+        self.assertEqual(blob.entries[0].length, 25)
+        self.assertEqual(blob.entries[0].identifier, keycredlink.CustomKeyInformation)
+        self.assertEqual(blob.entries[0].value.version, 1)
+        self.assertEqual(
+            blob.entries[0].value.flags, keycredlink.CUSTOM_KEY_INFO_FLAGS_ATTESTATION
+        )
+        self.assertTrue(blob.entries[0].value.isExtended)
+        self.assertEqual(blob.entries[0].value.volType, keycredlink.RDV)
+        self.assertEqual(
+            blob.entries[0].value.supportsNotification, keycredlink.Supported
+        )
+        self.assertEqual(blob.entries[0].value.fekKeyVersion, 0x01)
+        self.assertEqual(blob.entries[0].value.keyStrength, keycredlink.Normal)
+        self.assertEqual(bytes(blob.entries[0].value.reserved), reserved)
+        self.assertEqual(blob.entries[0].value.count, 2)
+        self.assertEqual(len(blob.entries[0].value.cki), 2)
+        self.assertEqual(blob.entries[0].value.cki[0].size, 2)
+        self.assertEqual(blob.entries[0].value.cki[0].data, [13, 10])
+        self.assertEqual(blob.entries[0].value.cki[1].size, 3)
+        self.assertEqual(blob.entries[0].value.cki[1].data, [1, 2, 3])
+
+        packed = ndr_pack(blob)
+        self.assertEqual(blob_source, packed)
+
+    def test_unpack_LastLogon(self):
+        """ensure that a KEYCREDENTIALLINK_BLOB with a last logon is
+        correctly packed and unpacked.
+        """
+        blob_source = bytes.fromhex(
+            "00 02 00 00"  # Version 2 value 0x00000200
+            "08 00"  # 8 bytes of data
+            "08"  # Approximate Last Logon Timestamp
+            "80 30 68 87 D0 D4 DB 01"  # Wed Jun 04 2025 09:43:22 GMT+1200
+        )
+        time = 0x1DBD4D087683080  # 133934606027600000 decimal
+        blob = ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, blob_source)
+
+        self.assertEqual(blob.version, 0x0200)
+        self.assertEqual(blob.count, 1)
+        self.assertEqual(len(blob.entries), 1)
+        self.assertEqual(blob.entries[0].length, 8)
+        self.assertEqual(
+            blob.entries[0].identifier, keycredlink.KeyApproximateLastLogonTimeStamp
+        )
+        self.assertEqual(blob.entries[0].value, time)
+
+        packed = ndr_pack(blob)
+        self.assertEqual(blob_source, packed)
+
+    def test_unpack_KeyCreationTime(self):
+        """ensure that a KEYCREDENTIALLINK_BLOB with a key creation time is
+        correctly packed and unpacked.
+        """
+        blob_source = bytes.fromhex(
+            "00 02 00 00"  # Version 2 value 0x00000200
+            "08 00"  # 8 bytes of data
+            "09"  # Key Creation Time
+            "80 96 26 FA DE 4D B8 01"  # Sun Sep 26 1993 08:03:02 GMT+1200
+        )
+        time = 0x1B84DDEFA269680  # 123934609827600000 decimal
+        blob = ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, blob_source)
+
+        self.assertEqual(blob.version, 0x0200)
+        self.assertEqual(blob.count, 1)
+        self.assertEqual(len(blob.entries), 1)
+        self.assertEqual(blob.entries[0].length, 8)
+        self.assertEqual(blob.entries[0].identifier, keycredlink.KeyCreationTime)
+        self.assertEqual(blob.entries[0].value, time)
+
+        packed = ndr_pack(blob)
+        self.assertEqual(blob_source, packed)
+
+    def test_unpack_full(self):
+        """ensure that fully populated KEYCREDENTIALLINK_BLOB is
+        correctly packed and unpacked.
+        """
+        source = bytes.fromhex(
+            "00 02 00 00"  # Version 2 value 0x00000200
+            "20 00 01"  # 32 bytes of data, identifier = key id
+            "          00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
+            "          10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
+            "20 00 02"  # 32 bytes of data, identifier = key hash
+            "          10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
+            "          00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
+            "40 00 03"  # 64 bytes of data, identifier = key material
+            "          43 05 A2 02 C6 8F 94 48 9B 82 C4 99 C6 F2 1A 74 "
+            "          42 D7 FE C1 F5 EE AE 52 B5 C7 59 DE 32 14 91 98 "
+            "          44 4D 95 82 75 11 38 32 EA 7B 52 E9 1E 8E D4 14 "
+            "          51 DF 93 25 39 3F E1 18 9C E5 3E 7A E6 D0 2E 77 "
+            "01 00 04 01 "  # 1 byte data, identifier = key usage (KEY_USAGE_NGC)
+            "01 00 05 00 "  # 1 byte data, identifier = key source (KEY_SOURCE_AD)
+            "10 00 06"  # 16 bytes of data, identifier = Device Id
+            "          00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
+            "19 00"  # 16 bytes header + 9 bytes CKI info
+            "   07"  # Custom Key Information
+            "      01"  # Version 1
+            "      02"  # Flags MFA not used
+            "      01"  # Operating system volume
+            "      01"  # Notification supported
+            "      01"  # Fek Key Version
+            "      02"  # Normal key strength
+            "          00 00 00 00 00 00 00 00 00 00"  # reserved space
+            "          00 02 0D 0A"  # two bytes custom key info
+            "          00 03 01 02 03"  # three bytes custom key information
+            "08 00 08"  # 8 bytes of data, identifier = Approximate Last Logon
+            "          80 30 68 87 D0 D4 DB 01"  # Wed Jun 04 2025 09:43:22 GMT+1200
+            "08 00 09"  # 8 bytes of data, identifier = Key Creation Time
+            "          80 96 26 FA DE 4D B8 01"  # Sun Sep 26 1993 08:03:02 GMT+1200
+        )
+        blob = ndr_unpack(keycredlink.KEYCREDENTIALLINK_BLOB, source)
+        self.assertEqual(len(blob.entries), 9)
+
+        # Check the key Id
+        key_id = bytes.fromhex(
+            "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
+            "10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
+        )
+        self.assertEqual(blob.entries[0].length, 32)
+        self.assertEqual(blob.entries[0].identifier, keycredlink.KeyID)
+        self.assertEqual(bytes(blob.entries[0].value), key_id)
+
+        # Check the key hash
+        key_hash = bytes.fromhex(
+            "10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
+            "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
+        )
+        self.assertEqual(blob.entries[1].length, 32)
+        self.assertEqual(blob.entries[1].identifier, keycredlink.KeyHash)
+        self.assertEqual(bytes(blob.entries[1].value), key_hash)
+
+        # Check the key material
+        key_material = bytes.fromhex(
+            "43 05 A2 02 C6 8F 94 48 9B 82 C4 99 C6 F2 1A 74 "
+            "42 D7 FE C1 F5 EE AE 52 B5 C7 59 DE 32 14 91 98 "
+            "44 4D 95 82 75 11 38 32 EA 7B 52 E9 1E 8E D4 14 "
+            "51 DF 93 25 39 3F E1 18 9C E5 3E 7A E6 D0 2E 77 "
+        )
+        self.assertEqual(blob.entries[2].length, 64)
+        self.assertEqual(blob.entries[2].identifier, keycredlink.KeyMaterial)
+        self.assertEqual(bytes(blob.entries[2].value), key_material)
+
+        # Check the key usage
+        self.assertEqual(blob.entries[3].length, 1)
+        self.assertEqual(blob.entries[3].identifier, keycredlink.KeyUsage)
+        self.assertEqual(blob.entries[3].value, keycredlink.KEY_USAGE_NGC)
+
+        # Check the key source
+        self.assertEqual(blob.entries[4].length, 1)
+        self.assertEqual(blob.entries[4].identifier, keycredlink.KeySource)
+        self.assertEqual(blob.entries[4].value, keycredlink.KEY_SOURCE_AD)
+
+        # Check the key device id
+        device_id = bytes.fromhex("00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F")
+        self.assertEqual(blob.entries[5].length, 16)
+        self.assertEqual(blob.entries[5].identifier, keycredlink.DeviceId)
+        self.assertEqual(bytes(blob.entries[5].value), device_id)
+
+        # Check custom key information
+        reserved = bytes.fromhex("00 00 00 00 00 00 00 00 00 00")
+
+        self.assertEqual(blob.entries[6].length, 25)
+        self.assertEqual(blob.entries[6].identifier, keycredlink.CustomKeyInformation)
+        self.assertEqual(blob.entries[6].value.version, 1)
+        self.assertEqual(
+            blob.entries[6].value.flags, keycredlink.CUSTOM_KEY_INFO_FLAGS_MFA_NOT_USED
+        )
+        self.assertTrue(blob.entries[6].value.isExtended)
+        self.assertEqual(blob.entries[6].value.volType, keycredlink.OSV)
+        self.assertEqual(
+            blob.entries[6].value.supportsNotification, keycredlink.Supported
+        )
+        self.assertEqual(blob.entries[6].value.fekKeyVersion, 0x01)
+        self.assertEqual(blob.entries[6].value.keyStrength, keycredlink.Normal)
+        self.assertEqual(bytes(blob.entries[6].value.reserved), reserved)
+        # Check the EncodedExtendedCKI entries
+        self.assertEqual(blob.entries[6].value.count, 2)
+        self.assertEqual(len(blob.entries[6].value.cki), 2)
+
+        self.assertEqual(blob.entries[6].value.cki[0].size, 2)
+        self.assertEqual(blob.entries[6].value.cki[0].data, [13, 10])
+        self.assertEqual(blob.entries[6].value.cki[1].size, 3)
+        self.assertEqual(blob.entries[6].value.cki[1].data, [1, 2, 3])
+
+        # check last logon
+        last_logon = 0x1DBD4D087683080  # 133934606027600000 decimal
+        self.assertEqual(blob.entries[7].length, 8)
+        self.assertEqual(
+            blob.entries[7].identifier, keycredlink.KeyApproximateLastLogonTimeStamp
+        )
+        self.assertEqual(blob.entries[7].value, last_logon)
+
+        # check key creation time
+        key_created = 0x1B84DDEFA269680  # 123934609827600000 decimal
+        self.assertEqual(blob.entries[8].length, 8)
+        self.assertEqual(blob.entries[8].identifier, keycredlink.KeyCreationTime)
+        self.assertEqual(blob.entries[8].value, key_created)
+
+        # Check that when the object is packed, the bytes generated equal
+        # the source.
+        packed = ndr_pack(blob)
+        self.assertEqual(source, packed)
+
+
+if __name__ == "__main__":
+    import unittest
+
+    unittest.main()
index 534612296449f55479f10d2ad51e92efe2ebb51c..c5336c7130a05d831e840d69373ff0adea9ea2dc 100644 (file)
@@ -350,6 +350,7 @@ planpythontestsuite("none", "samba.tests.glue")
 planpythontestsuite("none", "samba.tests.tdb_util")
 planpythontestsuite("none", "samba.tests.samdb")
 planpythontestsuite("none", "samba.tests.samdb_api")
+planpythontestsuite("none", "samba.tests.key_credential_link")
 planpythontestsuite("none", "samba.tests.ndr.gkdi")
 planpythontestsuite("none", "samba.tests.ndr.gmsa")
 planpythontestsuite("none", "samba.tests.ndr.sd")
index d8c414f820bcafb4165df24c1a57899ac40b8ec9..2dd196066d36fe326030a7479e56b77ee52ace39 100644 (file)
@@ -268,6 +268,14 @@ bld.SAMBA_PYTHON('python_claims',
         cflags_end=gen_cflags
         )
 
+bld.SAMBA_PYTHON('python_keycredlink',
+        source=('../../librpc/gen_ndr/py_keycredlink.c '
+                '../../librpc/gen_ndr/ndr_keycredlink.c'),
+        deps='NDR_KEYCREDLINK %s %s' % (pytalloc_util, pyrpc_util),
+        realname='samba/dcerpc/keycredlink.so',
+        cflags_end=gen_cflags
+        )
+
 bld.SAMBA_PYTHON('python_gkdi',
         source='../../librpc/gen_ndr/py_gkdi.c',
         deps='RPC_NDR_GKDI %s %s' % (pytalloc_util, pyrpc_util),