From 0d31b2b28b3849b4954416f45fc39b4009bcbea8 Mon Sep 17 00:00:00 2001 From: Gary Lockyer Date: Fri, 11 Jul 2025 13:41:42 +1200 Subject: [PATCH] librpc: keycredlink support X509 public keys Add support for X509 encoded public keys in msDSKeyCredentialLink KeyMaterial. Note: Only RSA public keys are supported. Signed-off-by: Gary Lockyer Reviewed-by: Douglas Bagnall --- librpc/idl/keycredlink.idl | 22 +- librpc/ndr/ndr_keycredlink.c | 559 ++++++++++++++++++++++ python/samba/tests/key_credential_link.py | 232 +++++++++ 3 files changed, 812 insertions(+), 1 deletion(-) diff --git a/librpc/idl/keycredlink.idl b/librpc/idl/keycredlink.idl index 8934ff681bd..7190c958652 100644 --- a/librpc/idl/keycredlink.idl +++ b/librpc/idl/keycredlink.idl @@ -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; } diff --git a/librpc/ndr/ndr_keycredlink.c b/librpc/ndr/ndr_keycredlink.c index 967202978b6..2222e3bf666 100644 --- a/librpc/ndr/ndr_keycredlink.c +++ b/librpc/ndr/ndr_keycredlink.c @@ -23,9 +23,16 @@ */ #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 /* @@ -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"); +} diff --git a/python/samba/tests/key_credential_link.py b/python/samba/tests/key_credential_link.py index b73c6a19e24..092d6e0cf5f 100755 --- a/python/samba/tests/key_credential_link.py +++ b/python/samba/tests/key_credential_link.py @@ -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 -- 2.47.2