]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
librpc: keycredlink support X509 public keys
authorGary Lockyer <gary@catalyst.net.nz>
Fri, 11 Jul 2025 01:41:42 +0000 (13:41 +1200)
committerDouglas Bagnall <dbagnall@samba.org>
Tue, 29 Jul 2025 04:30:34 +0000 (04:30 +0000)
Add support for X509 encoded public keys in msDSKeyCredentialLink
KeyMaterial.

Note: Only RSA public keys are supported.

Signed-off-by: Gary Lockyer <gary@catalyst.net.nz>
Reviewed-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
librpc/idl/keycredlink.idl
librpc/ndr/ndr_keycredlink.c
python/samba/tests/key_credential_link.py

index 8934ff681bd338319f2e2ef38d55708b67b7289e..7190c9586526b53b21d9fca6a8462ea021d85860 100644 (file)
@@ -37,6 +37,7 @@ interface keycredlink
                        * encoded as a
                        *   BCRYPT_RSAKEY_BLOB, see bcrypt_rsakey_blob.idl
                        *   TPM20_RSAKEY_BLOB, see tpm20_rsakey_blob.idl
+                       *   X509 public key
                        */
                KEY_USAGE_FIDO = 0x02,
                KEY_USAGE_FEK  = 0x03
@@ -153,9 +154,13 @@ interface keycredlink
                KEYCREDENTIALLINK_ENTRY_KeyUsage keyUsage;
        [case(KeySource), value(KEY_SOURCE_AD)]
                KEYCREDENTIALLINK_ENTRY_KeySource keySource;
+               /*
+                * Currently treating Key Material as an opaque binary blob
+                * But if needed it can be partially decoded with
+                * ndr_pull_KeyMaterialInternal
+                */
        [case(KeyMaterial)] [flag(NDR_REMAINING)]
                DATA_BLOB keyMaterial;
-               /* Currently treating Key Material as an opaque binary blob */
        [case(DeviceId)]
                uint8 deviceId[16];
        [case(CustomKeyInformation)]
@@ -180,4 +185,19 @@ interface keycredlink
                uint32 count;
                KEYCREDENTIALLINK_ENTRY entries[count];
        } KEYCREDENTIALLINK_BLOB;
+
+       /* Internal representation of KeyMaterial, that
+        *   - BCRYPT_RSAKEY_BLOB
+        *   - TPM20_RSAKEY_BLOB
+        *   - X509 encoded
+        * public keys are converted to.
+        *
+        * Note: that push is NOT implemented, and currently returns
+        * NDR_ERR_VALIDATE.
+        */
+       typedef [public, nopull, nopush] struct {
+               uint16 bit_size;
+               DATA_BLOB modulus;
+               DATA_BLOB exponent;
+       } KeyMaterialInternal;
 }
index 967202978b6bb4076ff252c8a4e230c3f11a68fe..2222e3bf666b613e5688a720e1989b2d80b848ea 100644 (file)
 */
 
 #include "lib/replace/replace.h"
+
 #include "librpc/gen_ndr/ndr_keycredlink.h"
 #include "gen_ndr/keycredlink.h"
+#include "lib/util/data_blob.h"
+#include "lib/util/debug.h"
 #include "libndr.h"
+#include "librpc/gen_ndr/ndr_bcrypt_rsakey_blob.h"
+#include "librpc/gen_ndr/ndr_tpm20_rsakey_blob.h"
+#include "util/asn1.h"
+#include "util/data_blob.h"
 #include <assert.h>
 
 /*
@@ -439,3 +446,555 @@ enum ndr_err_code ndr_pull_KEYCREDENTIALLINK_ENTRY(
        ndr->flags = _flags_save_STRUCT;
        return NDR_ERR_SUCCESS;
 }
+
+/* @brief Check that the AlgorithmIdentifier element is correct
+ *
+ * AlgorithmIdentifier ::= SEQUENCE {
+ *     algorithm       OBJECT IDENTIFIER,
+ *     parameters      ANY DEFINED BY algorithm OPTIONAL
+ *                     -- Should be NULL for RSA
+ * }
+ *
+ * @param[in]     ndr ndr pull context
+ * @param[in,out] asn ASN data context
+ *
+ * @return NDR_ERR_SUCCESS if the element is valid.
+ */
+static enum ndr_err_code check_algorithm_identifier(struct ndr_pull *ndr,
+                                                   struct asn1_data *asn)
+{
+       static const char *RSA_ENCRYPTION_OID = "1.2.840.113549.1.1.1";
+       uint8_t asn1_null[2];
+       if (!asn1_start_tag(asn, ASN1_SEQUENCE(0))) {
+               return ndr_pull_error(
+                       ndr,
+                       NDR_ERR_VALIDATE,
+                       "Invalid ASN1 tag, expecting SEQUENCE 0x30");
+       }
+       if (!asn1_check_OID(asn, RSA_ENCRYPTION_OID)) {
+               return ndr_pull_error(
+                       ndr,
+                       NDR_ERR_VALIDATE,
+                       "Invalid ASN1 algorithm OID, expecting %s",
+                       RSA_ENCRYPTION_OID);
+       }
+
+       /* For an RSA public key, parameters should be null 0x0500 */
+       if (!asn1_read(asn, asn1_null, 2)) {
+               return ndr_pull_error(
+                       ndr,
+                       NDR_ERR_VALIDATE,
+                       "Unexpected ASN1 element, expecting NULL 0x05");
+       }
+       if (!asn1_end_tag(asn)) { /* AlgorithmIdentifier */
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_UNREAD_BYTES,
+                                     "ASN1 element AlgorithmIdentifier");
+       }
+       return NDR_ERR_SUCCESS;
+}
+
+/**
+ * @brief start processing a BIT STRING
+ *
+ * The caller will need to call asn1_end_tag
+ *
+ * @param[in]     ndr         ndr pull context
+ * @param[in,out] asn         ASN data context
+ * @param[out]    unused_bits the number of unused bits in the least
+ *                            significant byte (LSB) of the BIT String
+ *
+ * @return NDR_ERR_SUCCESS if successful
+ *         The contents of unused_bits are undefined on an error
+ */
+static enum ndr_err_code start_bit_string(struct ndr_pull *ndr,
+                                         struct asn1_data *asn,
+                                         uint8_t *unused_bits)
+{
+       if (!asn1_start_tag(asn, ASN1_BIT_STRING)) {
+               return ndr_pull_error(
+                       ndr,
+                       NDR_ERR_VALIDATE,
+                       "Invalid ASN1 tag, expecting BIT STRING 0x03");
+       }
+
+       /*
+        * The first byte of a BIT STRING contains the number of unused bits
+        * in the final byte.
+        */
+       if (!asn1_read_uint8(asn, unused_bits)) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_VALIDATE,
+                                     "Invalid ASN1 BIT STRING, unable to read "
+                                     "number of unused bits");
+       }
+       if (*unused_bits > 8) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_RANGE,
+                                     "Invalid ASN1 BIT STRING, "
+                                     "number of unused bits exceeds 9");
+       }
+       return NDR_ERR_SUCCESS;
+}
+
+/**
+ * @brief Read a DER encoded INTEGER into a data_blob
+ *
+ * @param[in]     mem_ctx memory context to allocate the data_blob data on
+ * @param[in]     ndr     ndr pull context
+ * @param[in,out] asn     ASN data context
+ * @param[in]     name    the name of the INTEGER for diagnostic messages
+ * @param[out]    blob    the data blob to populate
+ *                        using mem_ctx for allocation
+ *
+ * @return NDR_ERR_SUCCESS if successful
+ *         The contents of blob are undefined on an error
+ */
+static enum ndr_err_code read_integer(TALLOC_CTX *mem_ctx,
+                                     struct ndr_pull *ndr,
+                                     struct asn1_data *asn,
+                                     const char *name,
+                                     DATA_BLOB *blob)
+{
+       static const int MAX_SIZE = 2 * 2048; /* 16384 bits */
+       uint8_t msb = 0;
+       int tag_size = 0;
+
+       if (!asn1_start_tag(asn, ASN1_INTEGER)) {
+               return ndr_pull_error(
+                       ndr,
+                       NDR_ERR_VALIDATE,
+                       "Invalid ASN1 tag, expecting INTEGER 0x02");
+       }
+       if (!asn1_peek_uint8(asn, &msb)) {
+               return ndr_pull_error(
+                       ndr,
+                       NDR_ERR_VALIDATE,
+                       "Invalid ASN1 tag, unable to inspect first byte of %s",
+                       name);
+       }
+       /* skip a leading 0 byte if present */
+       if (msb == 0) {
+               if (!asn1_read_uint8(asn, &msb)) {
+                       return ndr_pull_error(ndr,
+                                             NDR_ERR_VALIDATE,
+                                             "Invalid ASN1 tag, unable to "
+                                             "read first byte of %s",
+                                             name);
+               }
+       }
+
+       tag_size = asn1_tag_remaining(asn);
+       if (tag_size > MAX_SIZE) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_LENGTH,
+                                     "INTEGER %s size of %d "
+                                     "bytes is too large",
+                                     name,
+                                     tag_size);
+       }
+       if (tag_size <= 0) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_LENGTH,
+                                     "INTEGER %s size of %d "
+                                     "bytes is too small",
+                                     name,
+                                     tag_size);
+       }
+       *blob = data_blob_talloc(mem_ctx, NULL, tag_size);
+       if (blob->data == NULL) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_ALLOC,
+                                     "Unable to allocate DATA_BLOB for %s",
+                                     name);
+       }
+
+       if (!asn1_read(asn, blob->data, tag_size)) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_VALIDATE,
+                                     "Unable to read %s",
+                                     name);
+       }
+       if (!asn1_end_tag(asn)) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_UNREAD_BYTES,
+                                     "ASN1 INTEGER element %s",
+                                     name);
+       }
+       return NDR_ERR_SUCCESS;
+}
+
+/**
+ * @brief Convert a DER encoded X509 PublicKey into the Internal public key
+ *        representation
+ *
+ * publicKey BIT STRING -- containing an RSAPublicKey
+ * RSAPublicKey ::= SEQUENCE {
+ *     modulus            INTEGER,
+ *     publicExponent     INTEGER
+ * }
+ *
+ * @param[in,out] ndr       ndr pull context
+ * @param[in]     ndr_flags
+ * @param[out]    kmi       the KeyMaterialInternal structure to populate
+ *                         kmi needs to be a talloc context.
+ *
+ * @return NDR_ERR_SUCCESS if successful
+ *         The contents of kmi are undefined on an error
+ */
+static enum ndr_err_code read_public_key(struct ndr_pull *ndr,
+                                        struct asn1_data *asn,
+                                        struct KeyMaterialInternal *kmi)
+{
+       uint8_t unused_bits = 0;
+
+       /*
+        * publicKey BIT STRING
+        * The RSAPublicKey is encoded in a BIT STRING
+        */
+       NDR_CHECK(start_bit_string(ndr, asn, &unused_bits));
+
+       /* RSAPublicKey ::= SEQUENCE {
+        *     modulus            INTEGER,    -- n
+        *     publicExponent     INTEGER  }  -- e
+        */
+       if (!asn1_start_tag(asn, ASN1_SEQUENCE(0))) {
+               return ndr_pull_error(
+                       ndr,
+                       NDR_ERR_VALIDATE,
+                       "Invalid ASN1 tag, expecting SEQUENCE 0x30");
+       }
+
+       /* modulus INTEGER  */
+       NDR_CHECK(read_integer(kmi, ndr, asn, "MODULUS", &kmi->modulus));
+       kmi->bit_size = (kmi->modulus.length * 8) - unused_bits;
+
+       /* public exponent INTEGER */
+       NDR_CHECK(read_integer(kmi, ndr, asn, "EXPONENT", &kmi->exponent));
+
+       if (!asn1_end_tag(asn)) { /* RSAPublicKey */
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_UNREAD_BYTES,
+                                     "ASN1 element RSAPublicKey");
+       }
+       if (!asn1_end_tag(asn)) { /* PublicKey */
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_UNREAD_BYTES,
+                                     "ASN1 element PublicKey");
+       }
+       return NDR_ERR_SUCCESS;
+}
+
+/**
+ * @brief Convert a DER encoded X509 public key into the Internal public key
+ *        representation
+ *
+ * @param[in,out] ndr ndr pull context
+ * @param[in]     ndr_flags
+ * @param[out]    kmi the KeyMaterialInternal structure to populate
+ *                    kmi needs to be a talloc context.
+ * @param[in]     size number of bytes to process from the ndr context
+ *
+ * @return NDR_ERR_SUCCESS if successful
+ *         The contents of r are undefined on an error
+ */
+static enum ndr_err_code pull_DER_RSA_KEY(struct ndr_pull *ndr,
+                                         ndr_flags_type ndr_flags,
+                                         struct KeyMaterialInternal *kmi,
+                                         uint32_t size)
+{
+       enum ndr_err_code ret = NDR_ERR_SUCCESS;
+       struct asn1_data *asn = NULL;
+
+       TALLOC_CTX *tmp_ctx = talloc_new(ndr->current_mem_ctx);
+       if (tmp_ctx == NULL) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_ALLOC,
+                                     "Unable to allocate temporary memory "
+                                     "context");
+       }
+       asn = asn1_init(tmp_ctx, 5);
+       if (asn == NULL) {
+               TALLOC_FREE(tmp_ctx);
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_ALLOC,
+                                     "Unable to initialize ASN1 context");
+       }
+       asn1_load_nocopy(asn, ndr->data, size);
+
+       /*
+        * PublicKeyInfo  ::=  SEQUENCE  {
+        *     algorithm  AlgorithmIdentifier,
+        *     publicKey  BIT STRING
+        *     }
+        */
+       if (!asn1_start_tag(asn, ASN1_SEQUENCE(0))) {
+               ret = ndr_pull_error(
+                       ndr,
+                       NDR_ERR_VALIDATE,
+                       "Invalid ASN1 tag, expecting SEQUENCE 0x30");
+               goto out;
+       }
+
+       ret = check_algorithm_identifier(ndr, asn);
+       if (ret != NDR_ERR_SUCCESS) {
+               goto out;
+       }
+
+       ret = read_public_key(ndr, asn, kmi);
+       if (ret != NDR_ERR_SUCCESS) {
+               goto out;
+       }
+       if (!asn1_end_tag(asn)) { /* PublicKeyInfo */
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_UNREAD_BYTES,
+                                     "ASN1 element PublicKeyInfo");
+               goto out;
+       }
+
+       /* Successfully parsed the key data */
+       ret = NDR_ERR_SUCCESS;
+       ndr->offset += size; /* signal to NDR that the data has been consumed */
+
+out:
+       asn1_free(asn);
+       TALLOC_FREE(tmp_ctx);
+       return ret;
+}
+
+/**
+ * @brief Convert a TPM20_RSA_KEY_BLOB into the Internal public key
+ *        representation
+ * @param[in,out] ndr       ndr pull context
+ * @param[in]     ndr_flags
+ * @param[out]    kmi       the KeyMaterialInternal structure to populate
+ *                              kmi needs to be a talloc context.
+ *
+ * @return NDR_ERR_SUCCESS if successful
+ *         The contents of kmi are undefined on an error
+ */
+static enum ndr_err_code pull_TPM20_RSAKEY_BLOB(struct ndr_pull *ndr,
+                                               ndr_flags_type ndr_flags,
+                                               struct KeyMaterialInternal *kmi)
+{
+       enum ndr_err_code ret = NDR_ERR_SUCCESS;
+       struct TPM20_RSAKEY_BLOB *km = NULL;
+
+       TALLOC_CTX *tmp_ctx = talloc_new(ndr->current_mem_ctx);
+       if (tmp_ctx == NULL) {
+               return ndr_pull_error(
+                       ndr,
+                       NDR_ERR_ALLOC,
+                       "Unable to allocate temporary memory context");
+       }
+
+       km = talloc_zero(tmp_ctx, struct TPM20_RSAKEY_BLOB);
+       if (km == NULL) {
+               ret = ndr_pull_error(ndr,
+                                    NDR_ERR_ALLOC,
+                                    "Unable to allocate TPM20_RSAKEY_BLOB");
+               goto out;
+       }
+
+       ret = ndr_pull_TPM20_RSAKEY_BLOB(ndr, ndr_flags, km);
+       if (ret != NDR_ERR_SUCCESS) {
+               goto out_km;
+       }
+       kmi->bit_size = km->public_key.rsa_detail.keyBits;
+       kmi->modulus = data_blob_talloc(kmi,
+                                       km->public_key.rsa.buffer,
+                                       km->public_key.rsa.size);
+       if (kmi->modulus.data == NULL) {
+               ret = ndr_pull_error(
+                       ndr,
+                       NDR_ERR_ALLOC,
+                       "Unable to allocate TPM20_RSAKEY_BLOB modulus");
+               goto out_km;
+       }
+
+       kmi->exponent = data_blob_talloc(kmi,
+                                        km->public_key.rsa_detail.exponent,
+                                        TPM_RSA_EXPONENT_SIZE);
+       if (kmi->exponent.data == NULL) {
+               ret = ndr_pull_error(
+                       ndr,
+                       NDR_ERR_ALLOC,
+                       "Unable to allocate TPM20_RSAKEY_BLOB exponent");
+               goto out_km;
+       }
+       ret = NDR_ERR_SUCCESS;
+
+out_km:
+       TALLOC_FREE(km);
+out:
+       TALLOC_FREE(tmp_ctx);
+       return ret;
+}
+
+
+/**
+ * @brief Convert a BCRYPT_RSAPUBLIC_BLOB public key into the Internal public key
+ *        representation
+ *
+ * @param[in,out] ndr       ndr pull context
+ * @param[in]     ndr_flags
+ * @param[out]    kmi       the KeyMaterialInternal structure to populate
+ *                              kmi needs to be a talloc context.
+ *
+ * @return NDR_ERR_SUCCESS if successful
+ *         The contents of kmi are undefined on an error
+ */
+static enum ndr_err_code pull_BCRYPT_RSAPUBLIC_BLOB(
+       struct ndr_pull *ndr,
+       ndr_flags_type ndr_flags,
+       struct KeyMaterialInternal *kmi)
+{
+       enum ndr_err_code ret = NDR_ERR_SUCCESS;
+       struct BCRYPT_RSAPUBLIC_BLOB *km = NULL;
+
+       TALLOC_CTX *tmp_ctx = talloc_new(ndr->current_mem_ctx);
+       if (tmp_ctx == NULL) {
+               return ndr_pull_error(
+                       ndr,
+                       NDR_ERR_ALLOC,
+                       "Unable to allocate temporary memory context");
+       }
+
+       km = talloc_zero(tmp_ctx, struct BCRYPT_RSAPUBLIC_BLOB);
+       if (km == NULL) {
+               ret = ndr_pull_error(ndr,
+                                    NDR_ERR_ALLOC,
+                                    "Unable to allocate BCRYPT_RSAPUBLIC_BLOB");
+               goto out;
+       }
+
+       ret = ndr_pull_BCRYPT_RSAPUBLIC_BLOB(ndr, ndr_flags, km);
+       if (ret != NDR_ERR_SUCCESS) {
+               goto out_km;
+       }
+
+       kmi->bit_size = km->bit_length;
+
+       kmi->modulus = data_blob_talloc(kmi,
+                                       km->modulus,
+                                       km->modulus_len);
+       if (kmi->modulus.data == NULL) {
+               ret = ndr_pull_error(
+                       ndr,
+                       NDR_ERR_ALLOC,
+                       "Unable to allocate BCRYPT_RSAPUBLIC_BLOB modulus");
+               goto out_km;
+       }
+
+       kmi->exponent = data_blob_talloc(kmi,
+                                        km->public_exponent,
+                                        km->public_exponent_len);
+       if (kmi->exponent.data == NULL) {
+               ret = ndr_pull_error(
+                       ndr,
+                       NDR_ERR_ALLOC,
+                       "Unable to allocate BCRYPT_RSAPUBLIC_BLOB exponent");
+               goto out_km;
+       }
+
+       ret = NDR_ERR_SUCCESS;
+
+out_km:
+       TALLOC_FREE(km);
+out:
+       TALLOC_FREE(tmp_ctx);
+       return ret;
+}
+
+
+/**
+ * @brief Convert a KeyMaterial blob into the Internal public key
+ *        representation KeyMaterialInternal
+ *
+ * @param[in,out] ndr       ndr pull context
+ * @param[in]     ndr_flags
+ * @param[out]    kmi       the KeyMaterialInternal structure to populate
+ *                              kmi needs to be a talloc context.
+ *
+ * @return NDR_ERR_SUCCESS if successful
+ *         The contents of kmi are undefined on an error
+ */
+enum ndr_err_code ndr_pull_KeyMaterialInternal(struct ndr_pull *ndr,
+                                              ndr_flags_type ndr_flags,
+                                              struct KeyMaterialInternal *kmi)
+{
+       static const uint8_t BCRYPT_HEADER[] = {'R', 'S', 'A', '1'};
+       static const uint8_t TPM20_HEADER[] = {'P', 'C', 'P', 'M'};
+       static const uint32_t MIN_KEY_MATERIAL_SIZE = 5;
+       static const uint32_t MAX_KEY_MATERIAL_SIZE = 64 * 1024;
+
+       uint32_t size = 0;
+
+       if (ndr->offset > ndr->data_size) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_LENGTH,
+                                     "ndr->offset (%" PRIu32
+                                     ") is greater than "
+                                     "ndr->data_size (%" PRIu32 ")",
+                                     ndr->offset,
+                                     ndr->data_size);
+       }
+       size = ndr->data_size - ndr->offset;
+       if (size < MIN_KEY_MATERIAL_SIZE) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_LENGTH,
+                                     "KeyMaterial size of %" PRIu32
+                                     " bytes is too small",
+                                     size);
+       }
+       if (size > MAX_KEY_MATERIAL_SIZE) {
+               return ndr_pull_error(ndr,
+                                     NDR_ERR_LENGTH,
+                                     "KeyMaterial size of %" PRIu32
+                                     " bytes is too large",
+                                     size);
+       }
+
+       if (memcmp(BCRYPT_HEADER, ndr->data, sizeof(BCRYPT_HEADER)) == 0) {
+               return pull_BCRYPT_RSAPUBLIC_BLOB(ndr, ndr_flags, kmi);
+       } else if (memcmp(TPM20_HEADER, ndr->data, sizeof(TPM20_HEADER)) == 0) {
+               return pull_TPM20_RSAKEY_BLOB(ndr, ndr_flags, kmi);
+       } else if (*ndr->data == ASN1_SEQUENCE(0)) {
+               /*
+                * If the first byte is an ASN1 sequence marker assume that
+                * this is an x509 public key
+                */
+               return pull_DER_RSA_KEY(ndr, ndr_flags, kmi, size);
+       } else {
+               return ndr_pull_error(
+                       ndr,
+                       NDR_ERR_VALIDATE,
+                       "Unknown KeyMaterial type, could not be decoded");
+       }
+}
+
+/**
+ * @brief Push a representation of a KeyMaterialInternal onto the
+ *        ndr_push context.
+ *
+ * @param[in,out] ndr       ndr push context
+ * @param[in]     ndr_flags
+ * @param[out]    kmi       the KeyMaterialInternal structure to populate
+ *                              kmi needs to be a talloc context.
+ *
+ * @note This is not currently implemented and will always return
+ *       NDR_ERR_VALIDATE
+ *
+ * @return NDR_ERR_VALIDATE
+ *
+ */
+enum ndr_err_code ndr_push_KeyMaterialInternal(
+       struct ndr_push *ndr,
+       ndr_flags_type ndr_flags,
+       const struct KeyMaterialInternal *kmi)
+{
+       return ndr_push_error(
+               ndr,
+               NDR_ERR_VALIDATE,
+               "NDR Push for KeyMaterialInternal not currently supported");
+}
index b73c6a19e247bb8058caf1f378aa231e0ce00cef..092d6e0cf5f152eed5d88ab0dd496b2d1f047535 100755 (executable)
@@ -548,6 +548,238 @@ class KeyCredentialLinkTests(TestCase):
         packed = ndr_pack(blob)
         self.assertEqual(source, packed)
 
+    def test_unpack_tpm_key_material(self):
+        """
+        ensure that sample TPM 20 key material can be unpacked
+        into a KeyMaterialInternal structure
+        """
+        key_material = bytes.fromhex(
+            "50 43 50 4D"  # Magic value PCPM
+            "2E 00 00 00"  # header length
+            "02 00 00 00"  # type TPM 2.0
+            "00 00 00 00"  # flags
+            "00 00 00 00"  # public_length
+            "00 00 00 00"  # private length
+            "00 00 00 00"  # migration public length
+            "00 00 00 00"  # migration private length
+            "00 00 00 00"  # policy digest list length
+            "00 00 00 00"  # PCR binding length
+            "00 00 00 00"  # PCR digest length
+            "00 00 00 00"  # Encrypted secret length
+            "00 00 00 00"  # TPM 1.2 hostage blob length
+            "00 00"  # PCRA Algorithm Id
+            "18 01"  # size 280 bytes
+            "00 01"  # type
+            "00 0B"  # hash algorithm
+            "00 05 24 72"  # attributes
+            "00 00"  # auth policy"
+            "00 10"  # algorithm
+            "00 14"  # scheme
+            "00 0B"  # hash algorithm
+            "08 00"  # key bits
+            "01 02 03 04"  # exponent
+            "01 00"  # size 256 bytes
+            "9A 9E F6 5D E2 92 D6 D0 E5 B3 C4 35 B1 5B 36 F3"
+            "9E 83 7B A9 34 AB D9 67 E1 1C 75 43 E5 B6 48 9B"
+            "6E CD 8D FC 30 5F 4C B6 8E A0 69 A4 07 21 E7 D7"
+            "A1 74 4A 29 BC C9 5D 78 70 C4 3B E4 20 54 BC D0"
+            "AA FF 21 44 54 FC 09 08 2A CC DE 44 68 ED 9F B2"
+            "3E F7 ED 82 D7 2D 28 74 42 2A 2F 55 A2 E0 DA 45"
+            "F1 08 C0 83 8C 95 81 6D 92 CC A8 5D A4 B8 06 8C"
+            "76 F5 68 94 E7 60 E6 F4 EE 40 50 28 6C 82 47 89"
+            "07 E7 BC 0D 56 5D DA 86 57 E2 CE D3 19 A1 A2 7F"
+            "56 F8 99 8B 4A 71 32 6A 57 3B F9 E5 2D 39 35 6E"
+            "13 3E 84 DC 5C 96 E1 75 38 C3 AA 23 5B 68 BE 41"
+            "52 49 72 7A F6 2A 8F C5 C5 E0 6C DB 99 D1 A8 84"
+            "5F 70 21 87 2E A0 D2 68 D3 76 5C 9E D4 9C B5 E1"
+            "72 9D 17 8B DC 11 55 09 90 8D 96 F3 68 34 DD 50"
+            "63 AC 4A 74 A7 AF 0D DC 15 06 07 D7 5A B3 86 1A"
+            "54 96 E0 FA 66 25 31 F5 B4 C7 97 C7 7C 70 94 E3"
+        )
+
+        exponent = bytes.fromhex("01 02 03 04")
+        modulus = bytes.fromhex(
+            "9A 9E F6 5D E2 92 D6 D0 E5 B3 C4 35 B1 5B 36 F3"
+            "9E 83 7B A9 34 AB D9 67 E1 1C 75 43 E5 B6 48 9B"
+            "6E CD 8D FC 30 5F 4C B6 8E A0 69 A4 07 21 E7 D7"
+            "A1 74 4A 29 BC C9 5D 78 70 C4 3B E4 20 54 BC D0"
+            "AA FF 21 44 54 FC 09 08 2A CC DE 44 68 ED 9F B2"
+            "3E F7 ED 82 D7 2D 28 74 42 2A 2F 55 A2 E0 DA 45"
+            "F1 08 C0 83 8C 95 81 6D 92 CC A8 5D A4 B8 06 8C"
+            "76 F5 68 94 E7 60 E6 F4 EE 40 50 28 6C 82 47 89"
+            "07 E7 BC 0D 56 5D DA 86 57 E2 CE D3 19 A1 A2 7F"
+            "56 F8 99 8B 4A 71 32 6A 57 3B F9 E5 2D 39 35 6E"
+            "13 3E 84 DC 5C 96 E1 75 38 C3 AA 23 5B 68 BE 41"
+            "52 49 72 7A F6 2A 8F C5 C5 E0 6C DB 99 D1 A8 84"
+            "5F 70 21 87 2E A0 D2 68 D3 76 5C 9E D4 9C B5 E1"
+            "72 9D 17 8B DC 11 55 09 90 8D 96 F3 68 34 DD 50"
+            "63 AC 4A 74 A7 AF 0D DC 15 06 07 D7 5A B3 86 1A"
+            "54 96 E0 FA 66 25 31 F5 B4 C7 97 C7 7C 70 94 E3"
+        )
+        kmi = ndr_unpack(keycredlink.KeyMaterialInternal, key_material)
+        self.assertEqual(kmi.bit_size, 2048)
+        self.assertEqual(len(kmi.exponent), 4)
+        self.assertEqual(kmi.exponent, exponent)
+        self.assertEqual(len(kmi.modulus), 256)
+        self.assertEqual(kmi.modulus, modulus)
+
+    def test_unpack_bcrypt_key_material(self):
+        """
+        ensure that sample bcrypt key material can be unpacked
+        into a KeyMaterialInternal structure
+        """
+        key_material = bytes.fromhex(
+            "52 53 41 31"  # Magic value RSA1
+            "00 08 00 00"  # bit length, 2048
+            "04 00 00 00"  # public exponent length
+            "00 01 00 00"  # modulus length, 256
+            "00 00 00 00"  # prime one length"
+            "00 00 00 00"  # prime two length"
+            "01 02 03 04"  # public exponent
+            "9A 9E F6 5D E2 92 D6 D0 E5 B3 C4 35 B1 5B 36 F3"
+            "9E 83 7B A9 34 AB D9 67 E1 1C 75 43 E5 B6 48 9B"
+            "6E CD 8D FC 30 5F 4C B6 8E A0 69 A4 07 21 E7 D7"
+            "A1 74 4A 29 BC C9 5D 78 70 C4 3B E4 20 54 BC D0"
+            "AA FF 21 44 54 FC 09 08 2A CC DE 44 68 ED 9F B2"
+            "3E F7 ED 82 D7 2D 28 74 42 2A 2F 55 A2 E0 DA 45"
+            "F1 08 C0 83 8C 95 81 6D 92 CC A8 5D A4 B8 06 8C"
+            "76 F5 68 94 E7 60 E6 F4 EE 40 50 28 6C 82 47 89"
+            "07 E7 BC 0D 56 5D DA 86 57 E2 CE D3 19 A1 A2 7F"
+            "56 F8 99 8B 4A 71 32 6A 57 3B F9 E5 2D 39 35 6E"
+            "13 3E 84 DC 5C 96 E1 75 38 C3 AA 23 5B 68 BE 41"
+            "52 49 72 7A F6 2A 8F C5 C5 E0 6C DB 99 D1 A8 84"
+            "5F 70 21 87 2E A0 D2 68 D3 76 5C 9E D4 9C B5 E1"
+            "72 9D 17 8B DC 11 55 09 90 8D 96 F3 68 34 DD 50"
+            "63 AC 4A 74 A7 AF 0D DC 15 06 07 D7 5A B3 86 1A"
+            "54 96 E0 FA 66 25 31 F5 B4 C7 97 C7 7C 70 94 E3"
+        )
+        exponent = bytes.fromhex("01 02 03 04")
+        modulus = bytes.fromhex(
+            "9A 9E F6 5D E2 92 D6 D0 E5 B3 C4 35 B1 5B 36 F3"
+            "9E 83 7B A9 34 AB D9 67 E1 1C 75 43 E5 B6 48 9B"
+            "6E CD 8D FC 30 5F 4C B6 8E A0 69 A4 07 21 E7 D7"
+            "A1 74 4A 29 BC C9 5D 78 70 C4 3B E4 20 54 BC D0"
+            "AA FF 21 44 54 FC 09 08 2A CC DE 44 68 ED 9F B2"
+            "3E F7 ED 82 D7 2D 28 74 42 2A 2F 55 A2 E0 DA 45"
+            "F1 08 C0 83 8C 95 81 6D 92 CC A8 5D A4 B8 06 8C"
+            "76 F5 68 94 E7 60 E6 F4 EE 40 50 28 6C 82 47 89"
+            "07 E7 BC 0D 56 5D DA 86 57 E2 CE D3 19 A1 A2 7F"
+            "56 F8 99 8B 4A 71 32 6A 57 3B F9 E5 2D 39 35 6E"
+            "13 3E 84 DC 5C 96 E1 75 38 C3 AA 23 5B 68 BE 41"
+            "52 49 72 7A F6 2A 8F C5 C5 E0 6C DB 99 D1 A8 84"
+            "5F 70 21 87 2E A0 D2 68 D3 76 5C 9E D4 9C B5 E1"
+            "72 9D 17 8B DC 11 55 09 90 8D 96 F3 68 34 DD 50"
+            "63 AC 4A 74 A7 AF 0D DC 15 06 07 D7 5A B3 86 1A"
+            "54 96 E0 FA 66 25 31 F5 B4 C7 97 C7 7C 70 94 E3"
+        )
+
+        kmi = ndr_unpack(keycredlink.KeyMaterialInternal, key_material)
+        self.assertEqual(kmi.bit_size, 2048)
+        self.assertEqual(len(kmi.exponent), 4)
+        self.assertEqual(kmi.exponent, exponent)
+        self.assertEqual(len(kmi.modulus), 256)
+        self.assertEqual(kmi.modulus, modulus)
+
+    def test_unpack_der_key_material(self):
+        """
+        ensure that sample X509 public key material can be unpacked
+        into a KeyMaterialInternal structure
+        """
+        key_material = bytes.fromhex(
+            "30 82 01 22"  # Sequence 290 bytes, 2 elements
+            "30 0d"  # Sequence 13 bytes, 2 elements
+            # OID 9 bytes, 1.2.840.113549.1.1.1
+            "06 09 2a 86 48 86 f7 0d 01 01 01"
+            "05 00"  # Null
+            "03 82 01 0f 00"  # Bit string, 2160 bits, 0 unused bits
+            "30 82 01 0a"  # Sequence 266 bytes, 2 elements
+            "02 82 01 01"  # Integer 2048 bit, 257 bytes
+            # MODULUS is 257 bytes as it's most significant byte
+            # is 0xbd 0b10111101 and has bit 8 set,
+            # which DER Integer encoding uses as the sign bit,
+            # so need the leading 00 byte to prevent the value
+            # being interpreted as a negative integer
+            "00 bd ae 45 8b 17 cd 3e 62 71 66 67 7f a2 46 c4"
+            "47 78 79 f2 8c d4 2e 0c a0 90 1c f6 33 e1 94 89"
+            "b9 44 15 e3 29 e7 b6 91 ca ab 7e c6 25 60 e3 7a"
+            "c4 09 97 8a 4e 79 cb a6 1f f8 29 3f 8a 0d 45 58"
+            "9b 0e bf a5 fa 1c a2 5e 31 a1 e7 ba 7e 17 62 03"
+            "79 c0 07 48 11 8b fa 58 17 56 1a a1 62 d2 02 02"
+            "2a 64 8d 8c 53 fa 28 7c 89 18 34 70 64 a7 08 10"
+            "c9 3b 1b 2c 23 88 9c 35 50 78 d1 89 33 ce 82 b2"
+            "84 f4 99 d8 3e 67 11 a1 5c 1a 64 b8 6a 3e e6 95"
+            "2e 47 33 51 7e b7 62 b4 08 2c c4 87 52 00 9e 28"
+            "f2 16 9f 1b c1 3a 93 6d a3 38 9b 34 39 88 85 ea"
+            "38 ad c2 2b c3 7c 15 cb 8f 15 37 ed 88 62 5c 34"
+            "75 6f b0 eb 5c 42 6a cd 03 cc 49 bc b4 78 14 e1"
+            "5e 98 83 6f e7 19 a8 43 cb ca 07 b2 4e a4 36 60"
+            "95 ac 6f e2 1d 3a 33 f6 0e 94 ae fb d2 ac 9f c2"
+            "9f 5b 77 8f 46 3c ee 13 27 19 8e 68 71 27 3f 50"
+            "59"
+            "02 03 01 00 01"  # Integer 3 bytes EXPONENT
+        )
+
+        modulus = bytes.fromhex(
+            "bd ae 45 8b 17 cd 3e 62 71 66 67 7f a2 46 c4"
+            "47 78 79 f2 8c d4 2e 0c a0 90 1c f6 33 e1 94 89"
+            "b9 44 15 e3 29 e7 b6 91 ca ab 7e c6 25 60 e3 7a"
+            "c4 09 97 8a 4e 79 cb a6 1f f8 29 3f 8a 0d 45 58"
+            "9b 0e bf a5 fa 1c a2 5e 31 a1 e7 ba 7e 17 62 03"
+            "79 c0 07 48 11 8b fa 58 17 56 1a a1 62 d2 02 02"
+            "2a 64 8d 8c 53 fa 28 7c 89 18 34 70 64 a7 08 10"
+            "c9 3b 1b 2c 23 88 9c 35 50 78 d1 89 33 ce 82 b2"
+            "84 f4 99 d8 3e 67 11 a1 5c 1a 64 b8 6a 3e e6 95"
+            "2e 47 33 51 7e b7 62 b4 08 2c c4 87 52 00 9e 28"
+            "f2 16 9f 1b c1 3a 93 6d a3 38 9b 34 39 88 85 ea"
+            "38 ad c2 2b c3 7c 15 cb 8f 15 37 ed 88 62 5c 34"
+            "75 6f b0 eb 5c 42 6a cd 03 cc 49 bc b4 78 14 e1"
+            "5e 98 83 6f e7 19 a8 43 cb ca 07 b2 4e a4 36 60"
+            "95 ac 6f e2 1d 3a 33 f6 0e 94 ae fb d2 ac 9f c2"
+            "9f 5b 77 8f 46 3c ee 13 27 19 8e 68 71 27 3f 50"
+            "59"
+        )
+        exponent = bytes.fromhex("01 00 01")
+
+        kmi = ndr_unpack(keycredlink.KeyMaterialInternal, key_material)
+        self.assertEqual(kmi.bit_size, 2048)
+        self.assertEqual(len(kmi.exponent), 3)
+        self.assertEqual(kmi.exponent, exponent)
+        self.assertEqual(len(kmi.modulus), 256)
+        self.assertEqual(kmi.modulus, modulus)
+
+    def test_unpack_invalid_key_material(self):
+        """
+        ensure that an unknown key is rejected
+        """
+        key_material = b"NOT REALLY A KEY POSSIBLY A PASSWORD"
+        with self.assertRaises(RuntimeError) as e:
+            ndr_unpack(keycredlink.KeyMaterialInternal, key_material)
+
+        self.assertEqual(e.exception.args[0], 10)
+        self.assertEqual(e.exception.args[1], "Validate Error")
+
+    def test_unpack_too_short_key_material(self):
+        """
+        ensure that key material shorter than 5 bytes is rejected
+        """
+        key_material = b"1234"
+        with self.assertRaises(RuntimeError) as e:
+            ndr_unpack(keycredlink.KeyMaterialInternal, key_material)
+
+        self.assertEqual(e.exception.args[0], 6)
+        self.assertEqual(e.exception.args[1], "Length Error")
+
+    def test_unpack_too_long_key_material(self):
+        """
+        ensure that key material longer than 64KiB is rejected
+        """
+        key_material = b"4" * ((64 * 1024) + 1)
+        with self.assertRaises(RuntimeError) as e:
+            ndr_unpack(keycredlink.KeyMaterialInternal, key_material)
+
+        self.assertEqual(e.exception.args[0], 6)
+        self.assertEqual(e.exception.args[1], "Length Error")
+
 
 if __name__ == "__main__":
     import unittest