From: Gary Lockyer Date: Mon, 23 Jun 2025 03:01:37 +0000 (+1200) Subject: librpc/idl: Add idl for BCRYPT_RSAKEY_BLOB X-Git-Tag: tdb-1.4.14~76 X-Git-Url: http://git.ipfire.org/gitweb/gitweb.cgi?a=commitdiff_plain;h=acd0fccfdd10dab245595fbfb9934c0674ed9e76;p=thirdparty%2Fsamba.git librpc/idl: Add idl for BCRYPT_RSAKEY_BLOB Idl and tests for BCRYPT_RSAKEY_BLOB See https://learn.microsoft.com/en-us/windows/win32/api/ bcrypt/ns-bcrypt-bcrypt_rsakey_blob This is one of the encodings of msDSKeyCredentialLink KeyMaterial when KeyUsage is KEY_USAGE_NGC. As there appears to be no official documentation on the contents of KeyMaterial have based this on. https://github.com/p0dalirius/pydsinternals/blob/271dd969e07a8939044bfc498d94443082ec6fa9/ dsinternals/common/data/hello/KeyCredential.py#L75-L92 Note: only RSA public keys are handled Signed-off-by: Gary Lockyer Reviewed-by: Douglas Bagnall --- diff --git a/librpc/idl/bcrypt_rsakey_blob.idl b/librpc/idl/bcrypt_rsakey_blob.idl new file mode 100644 index 00000000000..fd5f6134e34 --- /dev/null +++ b/librpc/idl/bcrypt_rsakey_blob.idl @@ -0,0 +1,43 @@ +/* + Definitions for packing and unpacking of BCRYPT_RSAPUBLIC_BLOB + structures, derived from + https://learn.microsoft.com/en-us/windows/win32/api/ + bcrypt/ns-bcrypt-bcrypt_rsakey_blob + + Note: - Currently only handles RSA public keys +*/ + +#include "idl_types.h" + +[ + pointer_default(unique) +] +interface bcrypt_rsakey_blob +{ + const uint32 BCRYPT_RSAPUBLIC_MAGIC = 0x31415352; /* RSA1 */ + + /* Public structures. */ + typedef [public] struct { + /* Currently only handle RSA Public Key blobs */ + [value(0x31415352), range(0x31415352, 0x31415352)] + uint32 magic; /* RSA1 */ + uint32 bit_length; + /* + * As of Windows 10 version 1903, public exponents larger + * than (2^64 - 1) are no longer supported. + */ + [range(0x0,0x8)] uint32 public_exponent_len; + uint32 modulus_len; + /* + * We're only supporting public keys, so the private + * key prime lengths should be zero + */ + [value(0), range(0x0,0x0)] uint32 prime1_len_unused; + [value(0), range(0x0,0x0)] uint32 prime2_len_unused; + /* + * The exponent and modulus are big-endian + */ + uint8 public_exponent[public_exponent_len]; + uint8 modulus[modulus_len]; + } BCRYPT_RSAPUBLIC_BLOB; +} diff --git a/librpc/idl/keycredlink.idl b/librpc/idl/keycredlink.idl index 6492df522a1..95b0ca6a3e4 100644 --- a/librpc/idl/keycredlink.idl +++ b/librpc/idl/keycredlink.idl @@ -32,6 +32,11 @@ interface keycredlink typedef [enum8bit, public] enum { KEY_USAGE_NGC = 0x01, + /* + * KeyMaterial is a 2048 bit RSA (RFC8017) public key + * encoded as a BCRYPT_RSAKEY_BLOB, + * see bcrypt_rsakey_blob.idl + */ KEY_USAGE_FIDO = 0x02, KEY_USAGE_FEK = 0x03 } KEYCREDENTIALLINK_ENTRY_KeyUsage; diff --git a/librpc/idl/wscript_build b/librpc/idl/wscript_build index 8a8f97d8592..2fc8956eb6a 100644 --- a/librpc/idl/wscript_build +++ b/librpc/idl/wscript_build @@ -141,6 +141,7 @@ bld.SAMBA_PIDL_LIST('PIDL', nbt.idl ntlmssp.idl preg.idl + bcrypt_rsakey_blob.idl security.idl server_id.idl smb_acl.idl diff --git a/librpc/wscript_build b/librpc/wscript_build index f8a2a28cff1..f907989c595 100644 --- a/librpc/wscript_build +++ b/librpc/wscript_build @@ -648,7 +648,7 @@ bld.SAMBA_LIBRARY('ndr-samba', 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_KEYCREDLINK''', + NDR_KEYCREDLINK NDR_BCRYPT_RSAKEY_BLOB''', private_library=True, grouping_library=True ) @@ -761,6 +761,11 @@ bld.SAMBA_SUBSYSTEM('NDR_KEYCREDLINK', source='ndr/ndr_keycredlink.c gen_ndr/ndr_keycredlink.c', public_deps='ndr' ) + +bld.SAMBA_SUBSYSTEM('NDR_BCRYPT_RSAKEY_BLOB', + source='gen_ndr/ndr_bcrypt_rsakey_blob.c', + public_deps='ndr' + ) # # Cmocka tests # diff --git a/python/samba/tests/bcrypt_rsakey_blob.py b/python/samba/tests/bcrypt_rsakey_blob.py new file mode 100755 index 00000000000..ea984445d24 --- /dev/null +++ b/python/samba/tests/bcrypt_rsakey_blob.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +# Tests for NDR packing and unpacking of BCRYPT_RSAPUBLIC_BLOB structures +# +# See https://learn.microsoft.com/en-us/windows/win32/api/ +# bcrypt/ns-bcrypt-bcrypt_rsakey_blob +# +# 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 . +# + +import sys +import os + +sys.path.insert(0, "bin/python") +os.environ["PYTHONUNBUFFERED"] = "1" + +from samba.dcerpc import bcrypt_rsakey_blob +from samba.ndr import ndr_pack, ndr_unpack +from samba.tests import TestCase + + +class BcryptRsaKeyBlobTests(TestCase): + def test_unpack_empty_key_blob(self): + """ + Ensure that a minimal header only BCRYPT_RSAPUBLIC_BLOB + can be unpacked, then packed into identical bytes + """ + empty_key_blob = bytes.fromhex( + "52 53 41 31" # Magic value RSA1 + "00 00 00 00" # bit length + "00 00 00 00" # public exponent length + "00 00 00 00" # modulus length" + "00 00 00 00" # prime one length" + "00 00 00 00" # prime two length" + ) + blob = ndr_unpack( + bcrypt_rsakey_blob.BCRYPT_RSAPUBLIC_BLOB, empty_key_blob) + + self.assertEqual(blob.magic, 0x31415352) + self.assertEqual(blob.bit_length, 0) + self.assertEqual(blob.public_exponent_len, 0) + self.assertEqual(blob.modulus_len, 0) + self.assertEqual(blob.prime1_len_unused, 0) + self.assertEqual(blob.prime2_len_unused, 0) + self.assertEqual(len(blob.public_exponent), 0) + self.assertEqual(len(blob.modulus), 0) + + packed = ndr_pack(blob) + self.assertEqual(empty_key_blob, packed) + + def test_unpack_invalid_magic(self): + """ + Ensure that a BCRYPT_RSAPUBLIC_BLOB with an invalid magic value is + rejected + """ + invalid_magic_key_blob = bytes.fromhex( + "52 53 41 30" # Magic value RSA0 + "00 00 00 00" # bit length + "00 00 00 00" # public exponent length + "00 00 00 00" # modulus length + "00 00 00 00" # prime one length + "00 00 00 00" # prime two length" + ) + with self.assertRaises(RuntimeError) as e: + ndr_unpack(bcrypt_rsakey_blob.BCRYPT_RSAPUBLIC_BLOB, + invalid_magic_key_blob) + + self.assertEqual(e.exception.args[0], 13) + self.assertEqual(e.exception.args[1], "Range Error") + + def test_unpack_extra_data(self): + """ + Ensure that a BCRYPT_RSAPUBLIC_BLOB with extra data is + rejected + """ + extra_data_key_blob = bytes.fromhex( + "52 53 41 31" # Magic value RSA1 + "00 00 00 00" # bit length + "00 00 00 00" # public exponent length + "00 00 00 00" # modulus length + "00 00 00 00" # prime one length + "00 00 00 00" # prime two length + "01" # a trailing byte of data + ) + with self.assertRaises(RuntimeError) as e: + ndr_unpack(bcrypt_rsakey_blob.BCRYPT_RSAPUBLIC_BLOB, + extra_data_key_blob) + + self.assertEqual(e.exception.args[0], 18) + self.assertEqual(e.exception.args[1], "Unread Bytes") + + def test_unpack_missing_data(self): + """ + Ensure that a BCRYPT_RSAPUBLIC_BLOB with missing data is + rejected + """ + short_key_blob = bytes.fromhex( + "52 53 41 31" # Magic value RSA1 + "08 00 00 00" # bit length, 2048 + "01 00 00 00" # public exponent length, one byte + "02 00 00 00" # modulus length, two bytes + "00 00 00 00" # prime one length must be zero + "00 00 00 00" # prime two length must be zero + ) + with self.assertRaises(RuntimeError) as e: + ndr_unpack(bcrypt_rsakey_blob.BCRYPT_RSAPUBLIC_BLOB, short_key_blob) + + self.assertEqual(e.exception.args[0], 11) + self.assertEqual(e.exception.args[1], "Buffer Size Error") + + def test_unpack_invalid_exponent_length(self): + """ + Ensure that a BCRYPT_RSAPUBLIC_BLOB with an invalid exponent length is + rejected + """ + invalid_magic_key_blob = bytes.fromhex( + "52 53 41 31" # Magic value RSA1 + "00 00 00 00" # bit length + "09 00 00 00" # public exponent length, 9 bytes + "00 00 00 00" # modulus length + "00 00 00 00" # prime one length + "00 00 00 00" # prime two length" + ) + with self.assertRaises(RuntimeError) as e: + ndr_unpack(bcrypt_rsakey_blob.BCRYPT_RSAPUBLIC_BLOB, + invalid_magic_key_blob) + + self.assertEqual(e.exception.args[0], 13) + self.assertEqual(e.exception.args[1], "Range Error") + + def test_unpack_non_zero_prime1(self): + """ + Ensure that a BCRYPT_RSAPUBLIC_BLOB with a non zero prime 1 length is + rejected + """ + invalid_prime1_key_blob = bytes.fromhex( + "52 53 41 31" # Magic value RSA1 + "00 00 00 00" # bit length + "00 00 00 00" # public exponent length, 9 bytes + "00 00 00 00" # modulus length + "01 00 00 00" # prime one length + "00 00 00 00" # prime two length" + ) + with self.assertRaises(RuntimeError) as e: + ndr_unpack(bcrypt_rsakey_blob.BCRYPT_RSAPUBLIC_BLOB, + invalid_prime1_key_blob) + + self.assertEqual(e.exception.args[0], 13) + self.assertEqual(e.exception.args[1], "Range Error") + + def test_unpack_non_zero_prime2(self): + """ + Ensure that a BCRYPT_RSAPUBLIC_BLOB with a non zero prime 2 length is + rejected + """ + invalid_prime2_key_blob = bytes.fromhex( + "52 53 41 31" # Magic value RSA1 + "00 00 00 00" # bit length + "00 00 00 00" # public exponent length, 9 bytes + "00 00 00 00" # modulus length + "00 00 00 00" # prime one length + "01 00 00 00" # prime two length" + ) + with self.assertRaises(RuntimeError) as e: + ndr_unpack(bcrypt_rsakey_blob.BCRYPT_RSAPUBLIC_BLOB, + invalid_prime2_key_blob) + + self.assertEqual(e.exception.args[0], 13) + self.assertEqual(e.exception.args[1], "Range Error") + + def test_unpack(self): + """ + Ensure that a fully populated BCRYPT_RSAPUBLIC_BLOB + can be unpacked, then packed into identical bytes + """ + key_blob = bytes.fromhex( + "52 53 41 31" # Magic value RSA1 + "00 08 00 00" # bit length, 2048 + "01 00 00 00" # public exponent length + "02 00 00 00" # modulus length" + "00 00 00 00" # prime one length" + "00 00 00 00" # prime two length" + "01" # public exponent + "02 03" # modulus + ) + blob = ndr_unpack(bcrypt_rsakey_blob.BCRYPT_RSAPUBLIC_BLOB, key_blob) + + self.assertEqual(blob.magic, 0x31415352) + self.assertEqual(blob.bit_length, 2048) + + self.assertEqual(blob.public_exponent_len, 1) + self.assertEqual(len(blob.public_exponent), 1) + self.assertEqual(bytes(blob.public_exponent), bytes.fromhex("01")) + + self.assertEqual(blob.modulus_len, 2) + self.assertEqual(len(blob.modulus), 2) + self.assertEqual(bytes(blob.modulus), bytes.fromhex("02 03")) + + self.assertEqual(blob.prime1_len_unused, 0) + self.assertEqual(blob.prime2_len_unused, 0) + + packed = ndr_pack(blob) + self.assertEqual(key_blob, packed) + + +if __name__ == "__main__": + import unittest + + unittest.main() diff --git a/selftest/tests.py b/selftest/tests.py index 81c7f78e039..b679a602f89 100644 --- a/selftest/tests.py +++ b/selftest/tests.py @@ -351,6 +351,7 @@ 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.bcrypt_rsakey_blob") planpythontestsuite("none", "samba.tests.ndr.gkdi") planpythontestsuite("none", "samba.tests.ndr.gmsa") planpythontestsuite("none", "samba.tests.ndr.sd") diff --git a/source4/librpc/wscript_build b/source4/librpc/wscript_build index 2dd196066d3..9b7d3a0791e 100644 --- a/source4/librpc/wscript_build +++ b/source4/librpc/wscript_build @@ -276,6 +276,14 @@ bld.SAMBA_PYTHON('python_keycredlink', cflags_end=gen_cflags ) +bld.SAMBA_PYTHON('python_bycrypt_rsakey_blob', + source=('../../librpc/gen_ndr/py_bcrypt_rsakey_blob.c ' + '../../librpc/gen_ndr/ndr_bcrypt_rsakey_blob.c'), + deps='NDR_BCRYPT_RSAKEY_BLOB %s %s' % (pytalloc_util, pyrpc_util), + realname='samba/dcerpc/bcrypt_rsakey_blob.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),