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

Reply via email to