Add end-to-end functional test for HSM-based signing of pre-load
headers. The test verifies that binman can use PKCS#11 libraries
to sign data via hardware security modules and can generate pre-load
headers with HSM signatures.

Signed-off-by: yan wang <[email protected]>
---
Changes in v2:
- Merge v1 patches 4 and 5
- Extend testPreLoadHsmSign (SoftHSM) to cover both pkcs-1.5 and PSS
- Add testPreLoadHsmEcdsaSign (SoftHSM) which verifies the signatures with
  the public key read from the token
- Add tests for an invalid URI, hash and algorithm, module-path in the URI,
  the data checks in _SignHsmData(), a bad signature length and a missing
  tool before the header signature
- Merge testPreLoadHsmSignRuntimeError into testPreLoadHsmSignError
- Drop testPreLoadDefaultPadding and testPreLoadHsmMissingLibrary; move
  testPreLoadMissingKeyName to patch 2
- Rework the devicetrees for the hsm-key URI and make them smaller

 tools/binman/ftest.py                         | 230 ++++++++++++++++++
 tools/binman/test/security/pre_load_hsm.dts   |  21 ++
 .../test/security/pre_load_hsm_ecdsa.dts      |  21 ++
 .../security/pre_load_hsm_invalid_algo.dts    |  21 ++
 .../security/pre_load_hsm_invalid_padding.dts |  22 ++
 .../security/pre_load_hsm_invalid_sha.dts     |  21 ++
 .../security/pre_load_hsm_invalid_uri.dts     |  21 ++
 .../security/pre_load_hsm_module_path.dts     |  21 ++
 .../test/security/pre_load_hsm_sign.dts       |  21 ++
 .../test/security/pre_load_hsm_sign_pss.dts   |  22 ++
 10 files changed, 421 insertions(+)
 create mode 100644 tools/binman/test/security/pre_load_hsm.dts
 create mode 100644 tools/binman/test/security/pre_load_hsm_ecdsa.dts
 create mode 100644 tools/binman/test/security/pre_load_hsm_invalid_algo.dts
 create mode 100644 tools/binman/test/security/pre_load_hsm_invalid_padding.dts
 create mode 100644 tools/binman/test/security/pre_load_hsm_invalid_sha.dts
 create mode 100644 tools/binman/test/security/pre_load_hsm_invalid_uri.dts
 create mode 100644 tools/binman/test/security/pre_load_hsm_module_path.dts
 create mode 100644 tools/binman/test/security/pre_load_hsm_sign.dts
 create mode 100644 tools/binman/test/security/pre_load_hsm_sign_pss.dts

diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index d4522e905e0..08523141b48 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -22,10 +22,15 @@ import unittest
 import unittest.mock
 import urllib.error
 
+from Cryptodome.Hash import SHA256
+from Cryptodome.PublicKey import ECC
+from Cryptodome.Signature import DSS
+
 from binman import bintool
 from binman import cbfs_util
 from binman import cmdline
 from binman import control
+from binman.btool import pkcs11_tool as pkcs11_tool_btool
 from binman import elf
 from binman import elf_test
 from binman import fip_util
@@ -5992,6 +5997,231 @@ fdt         fdtmap                Extract the 
devicetree blob from the fdtmap
         self.assertIn('key-name is required for file-based signing',
                       str(exc.exception))
 
+    def testPreLoadHsmSign(self):
+        """Test pre-load header signed by SoftHSM via PKCS#11 (pkcs-1.5 and 
PSS)
+
+        Sets up a SoftHSM v2 token, generates an RSA-2048 key pair on it, and
+        builds two pre-load images (one with pkcs-1.5, one with PSS). Both are
+        verified with preload_check_sign using the public key extracted from 
the
+        token.
+        """
+        softhsm2_util = bintool.Bintool.create('softhsm2_util')
+        self._CheckBintool(softhsm2_util)
+
+        pkcs11_tool = bintool.Bintool.create('pkcs11-tool')
+        self._CheckBintool(pkcs11_tool)
+
+        p11_kit = bintool.Bintool.create('p11-kit')
+        self._CheckBintool(p11_kit)
+
+        openssl = bintool.Bintool.create('openssl')
+        self._CheckBintool(openssl)
+
+        prefix = 'testPreLoadHsmSign.'
+
+        # Configure SoftHSMv2
+        softhsm2_data = tools.read_file(self.TestFile('fit/softhsm2.conf'))
+        softhsm2_conf = self._MakeInputFile(f'{prefix}softhsm2.conf',
+                                            softhsm2_data)
+        softhsm2_tokens_dir = self._MakeInputDir(f'{prefix}softhsm2.tokens')
+        with open(softhsm2_conf, 'a') as f:
+            f.write(f'directories.tokendir = {softhsm2_tokens_dir}\n')
+
+        # Locate the SoftHSM2 shared library via p11-kit
+        p11_kit_config = configparser.ConfigParser()
+        p11_kit_config.read_string(p11_kit.run_cmd('print-config'))
+        softhsm2_lib = p11_kit_config.get('softhsm2', 'module', fallback=None)
+        self.assertIsNotNone(softhsm2_lib,
+                             'softhsm2 module not found in p11-kit config')
+
+        pubkey_der = os.path.join(self._indir, 'pubkey.der')
+        pubkey_pem = os.path.join(self._indir, 'pubkey.pem')
+
+        with unittest.mock.patch.dict('os.environ',
+                                      {'SOFTHSM2_CONF': softhsm2_conf}):
+            softhsm2_util.run_cmd('--init-token', '--free', '--label',
+                                  'uboot-hsm', '--pin', '1234',
+                                  '--so-pin', '1234')
+            pkcs11_tool.run_cmd('--module', softhsm2_lib,
+                                '--keypairgen', '--key-type', 'rsa:2048',
+                                '--label', 'uboot-key', '--login',
+                                '--pin', '1234')
+            pkcs11_tool.run_cmd('--module', softhsm2_lib,
+                                '--read-object', '--type', 'pubkey',
+                                '--label', 'uboot-key', '--login',
+                                '--pin', '1234', '--output-file', pubkey_der)
+            openssl.run_cmd_result('rsa', '-pubin', '-inform', 'DER',
+                                   '-in', pubkey_der, '-out', pubkey_pem)
+
+            entry_args = {
+                'pre-load-hsm-library': softhsm2_lib,
+                'pre-load-hsm-pin': '1234',
+            }
+
+            # pkcs-1.5 (default padding)
+            data = self._DoReadFileDtb('security/pre_load_hsm_sign.dts',
+                                       entry_args=entry_args)[0]
+            image_fname = tools.get_output_filename('image.bin')
+            self.assertEqual(PRE_LOAD_MAGIC, data[:len(PRE_LOAD_MAGIC)])
+            self.assertEqual(PRE_LOAD_VERSION, data[4:4 + 
len(PRE_LOAD_VERSION)])
+            self.assertEqual(PRE_LOAD_HDR_SIZE,
+                             data[8:8 + len(PRE_LOAD_HDR_SIZE)])
+            self.assertTrue(self._CheckPreload(image_fname, pubkey_pem))
+
+            # PSS padding
+            data = self._DoReadFileDtb('security/pre_load_hsm_sign_pss.dts',
+                                       entry_args=entry_args)[0]
+            image_fname = tools.get_output_filename('image.bin')
+            self.assertEqual(PRE_LOAD_MAGIC, data[:len(PRE_LOAD_MAGIC)])
+            self.assertTrue(
+                self._CheckPreload(image_fname, pubkey_pem, padding='pss'))
+
+    def testPreLoadHsmEcdsaSign(self):
+        """Test pre-load header signed by SoftHSM with ECDSA."""
+        softhsm2_util = bintool.Bintool.create('softhsm2_util')
+        self._CheckBintool(softhsm2_util)
+        pkcs11_tool = bintool.Bintool.create('pkcs11-tool')
+        self._CheckBintool(pkcs11_tool)
+        p11_kit = bintool.Bintool.create('p11-kit')
+        self._CheckBintool(p11_kit)
+
+        prefix = 'testPreLoadHsmEcdsaSign.'
+        conf = self._MakeInputFile(
+            f'{prefix}softhsm2.conf',
+            tools.read_file(self.TestFile('fit/softhsm2.conf')))
+        token_dir = self._MakeInputDir(f'{prefix}tokens')
+        with open(conf, 'a') as fd:
+            fd.write(f'directories.tokendir = {token_dir}\n')
+
+        cfg = configparser.ConfigParser()
+        cfg.read_string(p11_kit.run_cmd('print-config'))
+        lib = cfg.get('softhsm2', 'module', fallback=None)
+        self.assertIsNotNone(lib)
+
+        with unittest.mock.patch.dict('os.environ', {'SOFTHSM2_CONF': conf}):
+            softhsm2_util.run_cmd('--init-token', '--free', '--label',
+                                  'uboot-hsm', '--pin', '1234',
+                                  '--so-pin', '1234')
+            pkcs11_tool.run_cmd('--module', lib, '--keypairgen',
+                                '--key-type', 'EC:prime256v1',
+                                '--label', 'uboot-ecdsa', '--login',
+                                '--pin', '1234')
+            pubkey_der = os.path.join(self._indir, 'ecpubkey.der')
+            pkcs11_tool.run_cmd('--module', lib, '--read-object',
+                                '--type', 'pubkey', '--label', 'uboot-ecdsa',
+                                '--login', '--pin', '1234',
+                                '--output-file', pubkey_der)
+            args = {'pre-load-hsm-library': lib, 'pre-load-hsm-pin': '1234'}
+            data = self._DoReadFileDtb('security/pre_load_hsm_ecdsa.dts',
+                                       entry_args=args)[0]
+            self.assertEqual(PRE_LOAD_MAGIC, data[:len(PRE_LOAD_MAGIC)])
+
+            # preload_check_sign needs the private key for ECDSA, which an HSM
+            # never exports, so verify with the public key read from the token.
+            # Layout: header (64 bytes), header signature (64 bytes), image
+            # signature (64 bytes), then padding up to the 4096-byte header.
+            pub = ECC.import_key(tools.read_file(pubkey_der))
+            first_header = data[:64]
+            hdr_sig = data[64:128]
+            img_sig = data[128:192]
+            image = data[4096:]
+            self.assertEqual(SHA256.new(img_sig).digest(), first_header[32:64])
+            DSS.new(pub, 'fips-186-3').verify(SHA256.new(image), img_sig)
+            DSS.new(pub, 'fips-186-3').verify(SHA256.new(first_header),
+                                              hdr_sig)
+
+    def testPreLoadHsmInvalidPadding(self):
+        """Test HSM signing with invalid padding-name (validated before tool 
check)"""
+        with self.assertRaises(ValueError) as exc:
+            self._DoReadFile('security/pre_load_hsm_invalid_padding.dts')
+        self.assertIn('Unsupported padding scheme', str(exc.exception))
+
+    def testPreLoadHsmMissingTool(self):
+        """Test that binman still produces an image if pkcs11-tool is 
missing"""
+        with terminal.capture() as (_, stderr):
+            self._DoTestFile('security/pre_load_hsm.dts',
+                             force_missing_bintools='pkcs11-tool')
+        err = stderr.getvalue()
+        self.assertRegex(err, "Image 'image'.*missing bintools.*: pkcs11-tool")
+
+    def testPreLoadHsmSignError(self):
+        """Test HSM signing when sign() raises ValueError or RuntimeError"""
+        Btool = pkcs11_tool_btool.Bintoolpkcs11_tool
+        for exc_type in (ValueError, RuntimeError):
+            with unittest.mock.patch.object(Btool, 'is_present',
+                                            return_value=True), \
+                 unittest.mock.patch.object(Btool, 'sign',
+                                            side_effect=exc_type('Mock HSM 
error')):
+                with self.assertRaises(ValueError) as exc:
+                    self._DoReadFile('security/pre_load_hsm.dts')
+                self.assertIn('HSM signing failed', str(exc.exception))
+
+    def testPreLoadHsmInvalidUri(self):
+        """Test HSM signing with a hsm-key that is not a PKCS#11 URI"""
+        with self.assertRaises(ValueError) as exc:
+            self._DoReadFile('security/pre_load_hsm_invalid_uri.dts')
+        self.assertIn('Not a PKCS#11 URI', str(exc.exception))
+
+    def testPreLoadHsmInvalidSha(self):
+        """Test HSM signing with an unsupported hash algorithm"""
+        with self.assertRaises(ValueError) as exc:
+            self._DoReadFile('security/pre_load_hsm_invalid_sha.dts')
+        self.assertIn('sha1 is not supported', str(exc.exception))
+
+    def testPreLoadHsmInvalidAlgo(self):
+        """Test HSM signing with an unsupported signing algorithm"""
+        with self.assertRaises(ValueError) as exc:
+            self._DoReadFile('security/pre_load_hsm_invalid_algo.dts')
+        self.assertIn('rsa1234 is not supported', str(exc.exception))
+
+    def testPreLoadHsmModulePath(self):
+        """Test that module-path in the hsm-key URI sets the HSM library"""
+        self._DoTestFile('security/pre_load_hsm_module_path.dts',
+                         force_missing_bintools='pkcs11-tool')
+        entry = control.images['image'].GetEntries()['pre-load']
+        self.assertEqual('/no/lib.so', entry.hsm_library)
+
+    def testPreLoadHsmSignDataChecks(self):
+        """Test _SignHsmData() re-validates algorithm and padding"""
+        self._DoTestFile('security/pre_load_hsm.dts',
+                         force_missing_bintools='pkcs11-tool')
+        entry = control.images['image'].GetEntries()['pre-load']
+        for algo, padding, msg in (
+                ('sha1,rsa2048', None, 'sha1 is not supported'),
+                ('sha256,rsa1234', None, 'rsa1234 is not supported'),
+                ('sha256,rsa2048', 'bad', 'Unsupported padding scheme')):
+            entry.algo_name = algo
+            entry.padding_name = padding
+            with self.assertRaises(ValueError) as exc:
+                entry._SignHsmData(b'data')
+            self.assertIn(msg, str(exc.exception))
+
+    def testPreLoadHsmBadSignatureLength(self):
+        """Test HSM signing when the returned signature has the wrong size"""
+        Btool = pkcs11_tool_btool.Bintoolpkcs11_tool
+        with unittest.mock.patch.object(Btool, 'is_present',
+                                        return_value=True), \
+             unittest.mock.patch.object(Btool, 'sign',
+                                        return_value=b'short'):
+            with self.assertRaises(ValueError) as exc:
+                self._DoReadFile('security/pre_load_hsm.dts')
+        self.assertIn('HSM returned 5-byte signature', str(exc.exception))
+
+    def testPreLoadHsmHeaderSignMissing(self):
+        """Test dummy header when the tool vanishes before the header sign"""
+        from binman.etype import pre_load as pre_load_etype
+        calls = []
+
+        def fake_sign(data):
+            calls.append(1)
+            return bytes(256) if len(calls) % 2 else None
+
+        with unittest.mock.patch.object(pre_load_etype.Entry_pre_load,
+                                        '_SignHsmData',
+                                        side_effect=fake_sign):
+            data = self._DoReadFile('security/pre_load_hsm.dts')
+        self.assertEqual(tools.get_bytes(0, 4096), data[:4096])
+
     def _CheckSafeUniqueNames(self, *images):
         """Check all entries of given images for unsafe unique names"""
         for image in images:
diff --git a/tools/binman/test/security/pre_load_hsm.dts 
b/tools/binman/test/security/pre_load_hsm.dts
new file mode 100644
index 00000000000..5671559c838
--- /dev/null
+++ b/tools/binman/test/security/pre_load_hsm.dts
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       binman {
+               pre-load {
+                       content = <&image>;
+                       algo-name = "sha256,rsa2048";
+                       header-size = <4096>;
+                       version = <0x11223344>;
+                       hsm-key = "pkcs11:token=uboot-hsm;object=uboot-key";
+               };
+
+               image: blob-ext {
+                       filename = "refcode.bin";
+               };
+       };
+};
diff --git a/tools/binman/test/security/pre_load_hsm_ecdsa.dts 
b/tools/binman/test/security/pre_load_hsm_ecdsa.dts
new file mode 100644
index 00000000000..5b00ed8d69d
--- /dev/null
+++ b/tools/binman/test/security/pre_load_hsm_ecdsa.dts
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       binman {
+               pre-load {
+                       content = <&image>;
+                       algo-name = "sha256,ecdsa256";
+                       header-size = <4096>;
+                       version = <0x11223344>;
+                       hsm-key = "pkcs11:token=uboot-hsm;object=uboot-ecdsa";
+               };
+
+               image: blob-ext {
+                       filename = "refcode.bin";
+               };
+       };
+};
diff --git a/tools/binman/test/security/pre_load_hsm_invalid_algo.dts 
b/tools/binman/test/security/pre_load_hsm_invalid_algo.dts
new file mode 100644
index 00000000000..49d1c18f478
--- /dev/null
+++ b/tools/binman/test/security/pre_load_hsm_invalid_algo.dts
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       binman {
+               pre-load {
+                       content = <&image>;
+                       algo-name = "sha256,rsa1234";
+                       header-size = <4096>;
+                       version = <0x11223344>;
+                       hsm-key = "pkcs11:token=uboot-hsm;object=uboot-key";
+               };
+
+               image: blob-ext {
+                       filename = "refcode.bin";
+               };
+       };
+};
diff --git a/tools/binman/test/security/pre_load_hsm_invalid_padding.dts 
b/tools/binman/test/security/pre_load_hsm_invalid_padding.dts
new file mode 100644
index 00000000000..6ef11a59bb1
--- /dev/null
+++ b/tools/binman/test/security/pre_load_hsm_invalid_padding.dts
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       binman {
+               pre-load {
+                       content = <&image>;
+                       algo-name = "sha256,rsa2048";
+                       padding-name = "invalid";
+                       header-size = <4096>;
+                       version = <0x11223344>;
+                       hsm-key = "pkcs11:token=uboot-hsm;object=uboot-key";
+               };
+
+               image: blob-ext {
+                       filename = "refcode.bin";
+               };
+       };
+};
diff --git a/tools/binman/test/security/pre_load_hsm_invalid_sha.dts 
b/tools/binman/test/security/pre_load_hsm_invalid_sha.dts
new file mode 100644
index 00000000000..e6c693e4954
--- /dev/null
+++ b/tools/binman/test/security/pre_load_hsm_invalid_sha.dts
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       binman {
+               pre-load {
+                       content = <&image>;
+                       algo-name = "sha1,rsa2048";
+                       header-size = <4096>;
+                       version = <0x11223344>;
+                       hsm-key = "pkcs11:token=uboot-hsm;object=uboot-key";
+               };
+
+               image: blob-ext {
+                       filename = "refcode.bin";
+               };
+       };
+};
diff --git a/tools/binman/test/security/pre_load_hsm_invalid_uri.dts 
b/tools/binman/test/security/pre_load_hsm_invalid_uri.dts
new file mode 100644
index 00000000000..17838935f61
--- /dev/null
+++ b/tools/binman/test/security/pre_load_hsm_invalid_uri.dts
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       binman {
+               pre-load {
+                       content = <&image>;
+                       algo-name = "sha256,rsa2048";
+                       header-size = <4096>;
+                       version = <0x11223344>;
+                       hsm-key = "http://not-a-pkcs11-uri";;
+               };
+
+               image: blob-ext {
+                       filename = "refcode.bin";
+               };
+       };
+};
diff --git a/tools/binman/test/security/pre_load_hsm_module_path.dts 
b/tools/binman/test/security/pre_load_hsm_module_path.dts
new file mode 100644
index 00000000000..ae25d63b53d
--- /dev/null
+++ b/tools/binman/test/security/pre_load_hsm_module_path.dts
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       binman {
+               pre-load {
+                       content = <&image>;
+                       algo-name = "sha256,rsa2048";
+                       header-size = <4096>;
+                       version = <0x11223344>;
+                       hsm-key = 
"pkcs11:token=uboot-hsm;object=uboot-key?module-path=/no/lib.so";
+               };
+
+               image: blob-ext {
+                       filename = "refcode.bin";
+               };
+       };
+};
diff --git a/tools/binman/test/security/pre_load_hsm_sign.dts 
b/tools/binman/test/security/pre_load_hsm_sign.dts
new file mode 100644
index 00000000000..5671559c838
--- /dev/null
+++ b/tools/binman/test/security/pre_load_hsm_sign.dts
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       binman {
+               pre-load {
+                       content = <&image>;
+                       algo-name = "sha256,rsa2048";
+                       header-size = <4096>;
+                       version = <0x11223344>;
+                       hsm-key = "pkcs11:token=uboot-hsm;object=uboot-key";
+               };
+
+               image: blob-ext {
+                       filename = "refcode.bin";
+               };
+       };
+};
diff --git a/tools/binman/test/security/pre_load_hsm_sign_pss.dts 
b/tools/binman/test/security/pre_load_hsm_sign_pss.dts
new file mode 100644
index 00000000000..452f9f6d8a6
--- /dev/null
+++ b/tools/binman/test/security/pre_load_hsm_sign_pss.dts
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+       #address-cells = <1>;
+       #size-cells = <1>;
+
+       binman {
+               pre-load {
+                       content = <&image>;
+                       algo-name = "sha256,rsa2048";
+                       padding-name = "pss";
+                       header-size = <4096>;
+                       version = <0x11223344>;
+                       hsm-key = "pkcs11:token=uboot-hsm;object=uboot-key";
+               };
+
+               image: blob-ext {
+                       filename = "refcode.bin";
+               };
+       };
+};
-- 
2.43.0

Reply via email to