Test RSA and ECDSA signing with each suffix alone. Test both suffixes with different keys to check which key is selected. Verify each image signature against the expected public key.
Check that an invalid or unreadable preferred key prevents fallback. Check that an unreadable fallback key makes mkimage fail. Check that missing-key errors name both attempted paths. Check that explicit -G paths do not fall back to another suffix. Missing keys remain optional. Signed-off-by: Ayoub Zaki <[email protected]> --- Changes in v2: - New patch covering RSA and ECDSA key lookup and signature verification. test/py/tests/test_fit_key_fallback.py | 121 +++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 test/py/tests/test_fit_key_fallback.py diff --git a/test/py/tests/test_fit_key_fallback.py b/test/py/tests/test_fit_key_fallback.py new file mode 100644 index 00000000000..b7031340324 --- /dev/null +++ b/test/py/tests/test_fit_key_fallback.py @@ -0,0 +1,121 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright (c) 2026 Ayoub Zaki <[email protected]> + +"""Test FIT private key lookup and verify the selected signing key.""" + +import os +from pathlib import Path +import subprocess + +import pytest +import utils +from Cryptodome.Hash import SHA256 +from Cryptodome.PublicKey import ECC, RSA +from Cryptodome.Signature import DSS, pkcs1_15 + + [email protected](scope='module', params=['rsa2048', 'ecdsa256']) +def signing_keys(request): + """Generate distinct preferred and alternate keys for each algorithm.""" + algo = request.param + if algo == 'rsa2048': + return algo, '.key', '.pem', RSA.generate(2048), RSA.generate(2048) + return (algo, '.pem', '.key', ECC.generate(curve='P-256'), + ECC.generate(curve='P-256')) + + [email protected]('fit_signature') [email protected]('dtc') [email protected]('fdtget') [email protected]('fdtput') [email protected]('case', [ + 'preferred', 'fallback', 'both', 'invalid', 'unreadable', + 'fallback-unreadable', 'missing', 'explicit', 'explicit-missing', +]) +def test_fit_key_fallback(ubman, tmp_path, signing_keys, case): + """Only absent preferred keys permit fallback; -G uses the exact path.""" + algo, preferred_ext, fallback_ext, key, other_key = signing_keys + preferred = tmp_path / f'dev{preferred_ext}' + fallback = tmp_path / f'dev{fallback_ext}' + + def write_key(path, private_key): + pem = private_key.export_key(format='PEM') + path.write_bytes(pem.encode() if isinstance(pem, str) else pem) + + if case in ('preferred', 'both', 'unreadable'): + write_key(preferred, key) + if case == 'both': + write_key(fallback, other_key) + elif case != 'preferred' and case != 'missing': + write_key(fallback, key) + if case in ('invalid', 'explicit'): + preferred.write_text('Not a PEM private key\n') + if case in ('unreadable', 'fallback-unreadable'): + unreadable = preferred if case == 'unreadable' else fallback + unreadable.chmod(0) + if os.access(unreadable, os.R_OK): + pytest.skip('Cannot make key unreadable with this user') + + mkimage = str(Path(ubman.config.build_dir) / 'tools/mkimage') + datadir = Path(ubman.config.source_dir) / 'test/py/tests/vboot' + fit = str(tmp_path / 'test.fit') + (tmp_path / 'test-kernel.bin').write_bytes(bytes(500)) + utils.run_and_log(ubman, ['dtc', '-O', 'dtb', '-o', + str(tmp_path / 'sandbox-kernel.dtb'), + str(datadir / 'sandbox-kernel.dts')]) + utils.run_and_log(ubman, [mkimage, '-D', f'-I dts -O dtb -i {tmp_path}', + '-f', str(datadir / 'sign-images-sha256.its'), + fit]) + nodes = ['/images/kernel', '/images/fdt-1'] + for node in nodes: + utils.run_and_log(ubman, ['fdtput', '-ts', fit, f'{node}/signature', + 'algo', f'sha256,{algo}']) + + if case == 'explicit': + key_args = ['-G', str(fallback)] + elif case == 'explicit-missing': + key_args = ['-G', str(preferred)] + else: + key_args = ['-k', str(tmp_path)] + cmd = [mkimage, '-F', fit] + key_args + result = subprocess.run(cmd, capture_output=True, text=True) + output = result.stdout + result.stderr + ubman.log.info(output) + + if case in ('invalid', 'unreadable', 'fallback-unreadable'): + assert result.returncode != 0, output + if case == 'unreadable': + assert str(preferred) in output + assert str(fallback) not in output + elif case == 'fallback-unreadable': + assert str(preferred) in output + assert str(fallback) in output + return + + assert result.returncode == 0, output + if case in ('missing', 'explicit-missing'): + assert str(preferred) in output + if case == 'missing': + assert str(fallback) in output + else: + assert str(fallback) not in output + for node in nodes: + props = utils.run_and_log(ubman, ['fdtget', '-p', fit, + f'{node}/signature']) + assert 'value' not in props.split() + return + + def read_binary(node, prop): + values = utils.run_and_log(ubman, ['fdtget', '-t', 'bx', fit, + node, prop]) + return bytes(int(value, 16) for value in values.split()) + + # Distinct keys in the 'both' case prove that the preferred key was used. + public_key = key.public_key() + for node in nodes: + digest = SHA256.new(read_binary(node, 'data')) + signature = read_binary(f'{node}/signature', 'value') + if algo == 'rsa2048': + pkcs1_15.new(public_key).verify(digest, signature) + else: + DSS.new(public_key, 'fips-186-3').verify(digest, signature) -- 2.53.0
