]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
librpc/idl: Add idl for BCRYPT_RSAKEY_BLOB
authorGary Lockyer <gary@catalyst.net.nz>
Mon, 23 Jun 2025 03:01:37 +0000 (15:01 +1200)
committerDouglas Bagnall <dbagnall@samba.org>
Tue, 29 Jul 2025 04:30:34 +0000 (04:30 +0000)
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 <gary@catalyst.net.nz>
Reviewed-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
librpc/idl/bcrypt_rsakey_blob.idl [new file with mode: 0644]
librpc/idl/keycredlink.idl
librpc/idl/wscript_build
librpc/wscript_build
python/samba/tests/bcrypt_rsakey_blob.py [new file with mode: 0755]
selftest/tests.py
source4/librpc/wscript_build

diff --git a/librpc/idl/bcrypt_rsakey_blob.idl b/librpc/idl/bcrypt_rsakey_blob.idl
new file mode 100644 (file)
index 0000000..fd5f613
--- /dev/null
@@ -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;
+}
index 6492df522a16672430c774712a68d9fdc57217bb..95b0ca6a3e434c0e71ebc9eaf141cb3e8fd35db1 100644 (file)
@@ -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;
index 8a8f97d8592fa30a2a4596ae051cdf5cff757dfd..2fc8956eb6a256b54c209271e953e2d6a2efca9d 100644 (file)
@@ -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
index f8a2a28cff1573a4efd5767567bdd33a7cb0a0a6..f907989c595d0571d8bb30e70a75d516b8be8911 100644 (file)
@@ -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 (executable)
index 0000000..ea98444
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+#
+
+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()
index 81c7f78e03987106a681397550cead9ca82484d0..b679a602f89f62a0b4cb69051e682ebf12147840 100644 (file)
@@ -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")
index 2dd196066d36fe326030a7479e56b77ee52ace39..9b7d3a0791e8d312b423b8207cd819d3cd6e8370 100644 (file)
@@ -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),