Exercise the three new methods added to the pkcs11_tool btool: sign(), _build_mechanism() and _verify_mechanism().
Signed-off-by: Yan WANG <[email protected]> --- tools/binman/bintool_test.py | 374 +++++++++++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) diff --git a/tools/binman/bintool_test.py b/tools/binman/bintool_test.py index 7e8dafea94e..a3c8fb70422 100644 --- a/tools/binman/bintool_test.py +++ b/tools/binman/bintool_test.py @@ -21,6 +21,10 @@ from u_boot_pylib import terminal from u_boot_pylib import test_util from u_boot_pylib import tools +# Shared namedtuple for mocking run_cmd_result() return values +_CmdResult = collections.namedtuple( + '_CmdResult', ['return_code', 'stdout', 'stderr']) + # pylint: disable=R0904 class TestBintool(unittest.TestCase): """Tests for the Bintool class""" @@ -38,6 +42,12 @@ class TestBintool(unittest.TestCase): shutil.rmtree(self._indir) self._indir = None + def _make_module_path(self): + """Create and return a path to a fake PKCS#11 module file""" + path = os.path.join(self._indir, 'fake_module.so') + tools.write_file(path, b'') + return path + def test_missing_btype(self): """Test that unknown bintool types are detected""" with self.assertRaises(ValueError) as exc: @@ -354,6 +364,370 @@ class TestBintool(unittest.TestCase): result = btool.run_cmd_result('fred') self.assertIsNone(result) + def test_pkcs11_tool_sign_errors(self): + """Test error handling in pkcs11-tool sign method""" + btool = Bintool.create('pkcs11_tool') + data = b'test data' + + # Test unsupported signing algorithm family (dsa cannot be mapped) + with self.assertRaises(ValueError) as exc: + btool.sign(data, '/fake/module.so', token_label='test', + key_label='test', hash_algo='sha256', + sign_algo='dsa2048', padding='pkcs1v15') + self.assertIn('Unsupported signing algorithm', str(exc.exception)) + + # Test missing token identification + with self.assertRaises(ValueError) as exc: + btool.sign(data, '/fake/module.so', key_label='test') + self.assertIn('Must specify either token_label or token_serial', + str(exc.exception)) + + # Test missing key identification + with self.assertRaises(ValueError) as exc: + btool.sign(data, '/fake/module.so', token_label='test') + self.assertIn('Must specify either key_label or key_id', + str(exc.exception)) + + # Test missing module path + with self.assertRaises(ValueError) as exc: + btool.sign(data, None, token_label='test', key_label='test') + self.assertIn('PKCS#11 module path is required', + str(exc.exception)) + + # Test module not found + with self.assertRaises(ValueError) as exc: + btool.sign(data, '/nonexistent/module.so', token_label='test', + key_label='test') + self.assertIn('PKCS#11 module not found', str(exc.exception)) + + def test_pkcs11_tool_sign_failures(self): + """Test pkcs11-tool sign method failure cases""" + btool = Bintool.create('pkcs11_tool') + btool.present = True + data = b'test data' + + module_path = self._make_module_path() + + # Test tool not available + def fake_run_none(*args, **kwargs): + return None + + with unittest.mock.patch.object(btool, 'run_cmd_result', + side_effect=fake_run_none): + with self.assertRaises(RuntimeError) as exc: + btool.sign(data, module_path, token_label='test', + key_label='test') + self.assertIn('pkcs11-tool not available', str(exc.exception)) + + # Test signing failure (non-zero return code) + def fake_run_error(*args, **kwargs): + return _CmdResult(1, b'', b'Error: PIN incorrect') + + with unittest.mock.patch.object(btool, 'run_cmd_result', + side_effect=fake_run_error): + with self.assertRaises(RuntimeError) as exc: + btool.sign(data, module_path, token_label='test', + key_label='test') + self.assertIn('pkcs11-tool signing failed', str(exc.exception)) + self.assertIn('PIN incorrect', str(exc.exception)) + + # Test no signature output + def fake_run_no_output(*args, **kwargs): + return _CmdResult(0, b'', b'') + + with unittest.mock.patch.object(btool, 'run_cmd_result', + side_effect=fake_run_no_output): + with self.assertRaises(RuntimeError) as exc: + btool.sign(data, module_path, token_label='test', + key_label='test') + self.assertIn('no signature output', str(exc.exception)) + + def test_pkcs11_tool_cleanup_error(self): + """Test that cleanup errors in pkcs11-tool are handled gracefully""" + btool = Bintool.create('pkcs11_tool') + btool.present = True + data = b'test data' + + module_path = self._make_module_path() + + fake_sig = b'\x00' * 256 + + def fake_run_result(*args, **kwargs): + """Mock successful signing""" + return _CmdResult(0, fake_sig, b'') + + # Mock os.unlink to raise OSError + original_unlink = os.unlink + + def fake_unlink(path): + if 'binman_sign_' in path: + raise OSError("Permission denied") + return original_unlink(path) + + with unittest.mock.patch.object(btool, 'run_cmd_result', + side_effect=fake_run_result): + with unittest.mock.patch('os.unlink', side_effect=fake_unlink): + # Should not raise exception, cleanup error is silently ignored + sig = btool.sign(data, module_path, token_label='test', + key_label='test') + self.assertEqual(fake_sig, sig) + + def test_pkcs11_tool_find_slot_by_serial(self): + """Test find_slot_by_serial method""" + btool = Bintool.create('pkcs11_tool') + btool.present = True + + # Test finding a slot with matching serial + slot_output = """Available slots: +Slot 0 (0x123): SoftHSM slot ID 0x123 + token label : test-token + token manufacturer : SoftHSM project + token model : SoftHSM v2 + token flags : login required, rng, token initialized, PIN initialized + hardware version : 2.6 + firmware version : 2.6 + Token serial num : abc123xyz + +Slot 1 (0x456): SoftHSM slot ID 0x456 + token label : another-token + token manufacturer : SoftHSM project + token model : SoftHSM v2 + token flags : login required, rng, token initialized, PIN initialized + hardware version : 2.6 + firmware version : 2.6 + TOKEN SERIAL NUM : def456uvw +""" + def fake_run_cmd(*args, **kwargs): + result = collections.namedtuple('Result', ['stdout', 'stderr']) + return result(slot_output, '') + + with unittest.mock.patch.object(btool, 'run_cmd', + side_effect=fake_run_cmd): + # Test finding first slot + slot_idx = btool.find_slot_by_serial('/fake/module.so', 'abc123xyz') + self.assertEqual('0', slot_idx) + + # Test finding second slot + slot_idx = btool.find_slot_by_serial('/fake/module.so', 'def456uvw') + self.assertEqual('1', slot_idx) + + # Test not finding a slot + slot_idx = btool.find_slot_by_serial('/fake/module.so', 'nonexistent') + self.assertIsNone(slot_idx) + + def test_pkcs11_tool_sign_success_slot_index(self): + """Test successful signing with slot_index""" + btool = Bintool.create('pkcs11_tool') + btool.present = True + data = b'test data to sign' + + module_path = self._make_module_path() + + fake_sig = b'\x01\x02\x03\x04' * 64 + + def fake_run_result(*args, **kwargs): + if '--sign' in args: + self.assertIn('--slot-index', args) + self.assertIn('0', args) + return _CmdResult(0, fake_sig, b'') + + with unittest.mock.patch.object(btool, 'run_cmd_result', + side_effect=fake_run_result): + sig = btool.sign(data, module_path, slot_index='0', + key_label='test-key') + self.assertEqual(fake_sig, sig) + + def test_pkcs11_tool_sign_success_key_id(self): + """Test successful signing with key_id instead of key_label""" + btool = Bintool.create('pkcs11_tool') + btool.present = True + data = b'test data to sign' + + module_path = self._make_module_path() + + fake_sig = b'\x05\x06\x07\x08' * 64 + + def fake_run_result(*args, **kwargs): + if '--sign' in args: + self.assertIn('--id', args) + self.assertIn('01', args) + return _CmdResult(0, fake_sig, b'') + + with unittest.mock.patch.object(btool, 'run_cmd_result', + side_effect=fake_run_result): + sig = btool.sign(data, module_path, token_label='test-token', + key_id='01') + self.assertEqual(fake_sig, sig) + + def test_pkcs11_tool_sign_success_with_pin(self): + """Test successful signing with PIN provided""" + btool = Bintool.create('pkcs11_tool') + btool.present = True + data = b'test data to sign' + + module_path = self._make_module_path() + + fake_sig = b'\x09\x0a\x0b\x0c' * 64 + + def fake_run_result(*args, **kwargs): + # Verify PIN is in the sign command args (not the mechanism check) + if '--sign' in args: + self.assertIn('--pin', args) + self.assertIn('1234', args) + return _CmdResult(0, fake_sig, b'') + + with unittest.mock.patch.object(btool, 'run_cmd_result', + side_effect=fake_run_result): + sig = btool.sign(data, module_path, token_label='test-token', + key_label='test-key', pin='1234') + self.assertEqual(fake_sig, sig) + + def test_pkcs11_tool_sign_token_serial_not_found(self): + """Test signing with token_serial when token is not found""" + btool = Bintool.create('pkcs11_tool') + btool.present = True + data = b'test data to sign' + + module_path = self._make_module_path() + + def fake_find_slot(module, serial): + return None + + with unittest.mock.patch.object(btool, 'find_slot_by_serial', + side_effect=fake_find_slot): + with self.assertRaises(ValueError) as exc: + btool.sign(data, module_path, token_serial='nonexistent', + key_label='test-key') + self.assertIn('Token with serial nonexistent not found', + str(exc.exception)) + + def test_pkcs11_tool_sign_token_serial_found(self): + """Test that a resolved serial is forwarded as slot-index to the sign command""" + btool = Bintool.create('pkcs11_tool') + btool.present = True + data = b'test data to sign' + module_path = self._make_module_path() + fake_sig = b'\x01\x02\x03\x04' * 64 + + def fake_run_result(*args, **kwargs): + if '--sign' in args: + self.assertIn('--slot-index', args) + self.assertIn('0', args) + return _CmdResult(0, fake_sig, b'') + + with unittest.mock.patch.object(btool, 'find_slot_by_serial', + return_value='0'): + with unittest.mock.patch.object(btool, 'run_cmd_result', + side_effect=fake_run_result): + sig = btool.sign(data, module_path, token_serial='SN12345', + key_label='test-key') + self.assertEqual(fake_sig, sig) + + def test_pkcs11_tool_build_mechanism(self): + """Test dynamic PKCS#11 mechanism name construction""" + btool = Bintool.create('pkcs11_tool') + + # RSA PKCS#1 v1.5 combinations — key-size suffix is stripped + self.assertEqual('SHA256-RSA-PKCS', + btool._build_mechanism('sha256', 'rsa2048', 'pkcs1v15')) + self.assertEqual('SHA384-RSA-PKCS', + btool._build_mechanism('sha384', 'rsa3072', 'pkcs1v15')) + self.assertEqual('SHA512-RSA-PKCS', + btool._build_mechanism('sha512', 'rsa4096', 'pkcs1v15')) + + # RSA PSS combinations + self.assertEqual('SHA256-RSA-PKCS-PSS', + btool._build_mechanism('sha256', 'rsa2048', 'pss')) + self.assertEqual('SHA512-RSA-PKCS-PSS', + btool._build_mechanism('sha512', 'rsa4096', 'pss')) + + # 'pkcs-1.5' alias is normalized to 'pkcs1v15' + self.assertEqual('SHA256-RSA-PKCS', + btool._build_mechanism('sha256', 'rsa2048', 'pkcs-1.5')) + + # EC / ECDSA families + self.assertEqual('ECDSA-SHA256', + btool._build_mechanism('sha256', 'ec256', 'pkcs1v15')) + self.assertEqual('ECDSA-SHA384', + btool._build_mechanism('sha384', 'ecdsa384', 'pkcs1v15')) + + # Previously hard-coded combinations still resolve correctly + self.assertEqual('SHA384-RSA-PKCS', + btool._build_mechanism('sha384', 'rsa4096', 'pkcs1v15')) + + # Novel (non-table) combination: any hash works with RSA + self.assertEqual('SHA224-RSA-PKCS', + btool._build_mechanism('sha224', 'rsa2048', 'pkcs1v15')) + + # Unknown algorithm family raises ValueError + with self.assertRaises(ValueError) as exc: + btool._build_mechanism('sha256', 'dsa2048', 'pkcs1v15') + self.assertIn('Unsupported signing algorithm', str(exc.exception)) + + # sign_algo with no leading letters cannot be parsed by the regex + with self.assertRaises(ValueError) as exc: + btool._build_mechanism('sha256', '2048rsa', 'pkcs1v15') + self.assertIn('Cannot parse signing algorithm', str(exc.exception)) + + def test_pkcs11_tool_verify_mechanism(self): + """Test mechanism verification against token's allowed mechanisms list""" + btool = Bintool.create('pkcs11_tool') + + mechanisms_output = ( + 'Using slot 0 with a present token (0x1)\n' + 'Supported mechanisms:\n' + 'SHA256-RSA-PKCS, keySize={512,4096}, hw, sign, verify\n' + 'SHA384-RSA-PKCS, keySize={512,4096}, hw, sign, verify\n' + 'SHA256-RSA-PKCS-PSS, keySize={512,4096}, hw, sign, verify\n' + ) + + def fake_run_ok(*args, **kwargs): + return _CmdResult(0, mechanisms_output.encode(), b'') + + # Mechanism present in the list — no error + with unittest.mock.patch.object(btool, 'run_cmd_result', + side_effect=fake_run_ok): + btool._verify_mechanism('SHA256-RSA-PKCS', '/fake/module.so') + + # Mechanism absent from the list — raises ValueError + with unittest.mock.patch.object(btool, 'run_cmd_result', + side_effect=fake_run_ok): + with self.assertRaises(ValueError) as exc: + btool._verify_mechanism('SHA512-RSA-PKCS', '/fake/module.so') + self.assertIn("not in the token's allowed mechanisms", + str(exc.exception)) + self.assertIn('SHA512-RSA-PKCS', str(exc.exception)) + + # Tool unavailable (returns None) — verification is skipped silently + def fake_run_none(*args, **kwargs): + return None + + with unittest.mock.patch.object(btool, 'run_cmd_result', + side_effect=fake_run_none): + btool._verify_mechanism('SHA512-RSA-PKCS', '/fake/module.so') + + # Non-zero exit code — verification is skipped silently + def fake_run_error(*args, **kwargs): + return _CmdResult(1, b'', b'error') + + with unittest.mock.patch.object(btool, 'run_cmd_result', + side_effect=fake_run_error): + btool._verify_mechanism('SHA512-RSA-PKCS', '/fake/module.so') + + # Slot index is forwarded to --list-mechanisms + received_args = [] + + def fake_run_capture(*args, **kwargs): + received_args.extend(args) + return _CmdResult(0, b'', b'') + + with unittest.mock.patch.object(btool, 'run_cmd_result', + side_effect=fake_run_capture): + btool._verify_mechanism('SHA256-RSA-PKCS', '/fake/module.so', + slot_index='2') + self.assertIn('--slot-index', received_args) + self.assertIn('2', received_args) + if __name__ == "__main__": unittest.main() -- 2.43.0
