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
