sandbox_defconfig has enabled CONFIG_ECDSA_VERIFY for some time but with no UCLASS_ECDSA driver the option could not do anything, so ECDSA verification has never been exercised on the target. test_vboot.py covers RSA only and test_fit_ecdsa.py states that it "doesn't run the sandbox. It only checks the host tool 'mkimage'".
Enable CONFIG_ECDSA_VERIFY_MBEDTLS and the P-384 curve in sandbox_defconfig and add tests that sign a FIT for each supported curve and boot it under sandbox. Each case checks that a valid signature verifies that a tampered signature is rejected, and that a valid signature checked against a different public key is rejected. Signed-off-by: Ayoub Zaki <[email protected]> --- configs/sandbox_defconfig | 2 + test/py/tests/test_vboot_ecdsa.py | 148 ++++++++++++++++++ test/py/tests/vboot/sign-configs-ecdsa256.its | 45 ++++++ test/py/tests/vboot/sign-configs-ecdsa384.its | 45 ++++++ test/py/tests/vboot/sign-images-ecdsa256.its | 42 +++++ test/py/tests/vboot/sign-images-ecdsa384.its | 42 +++++ 6 files changed, 324 insertions(+) create mode 100644 test/py/tests/test_vboot_ecdsa.py create mode 100644 test/py/tests/vboot/sign-configs-ecdsa256.its create mode 100644 test/py/tests/vboot/sign-configs-ecdsa384.its create mode 100644 test/py/tests/vboot/sign-images-ecdsa256.its create mode 100644 test/py/tests/vboot/sign-images-ecdsa384.its diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig index ba800f7d19d..794da7716c3 100644 --- a/configs/sandbox_defconfig +++ b/configs/sandbox_defconfig @@ -381,8 +381,10 @@ CONFIG_PANIC_HANG=y CONFIG_CMD_DHRYSTONE=y CONFIG_MBEDTLS_LIB=y CONFIG_HKDF_MBEDTLS=y +CONFIG_MBEDTLS_ECP_DP_SECP384R1=y CONFIG_ECDSA=y CONFIG_ECDSA_VERIFY=y +CONFIG_ECDSA_VERIFY_MBEDTLS=y CONFIG_RSASSA_PSS=y CONFIG_TPM=y CONFIG_ERRNO_STR=y diff --git a/test/py/tests/test_vboot_ecdsa.py b/test/py/tests/test_vboot_ecdsa.py new file mode 100644 index 00000000000..751f10a02b2 --- /dev/null +++ b/test/py/tests/test_vboot_ecdsa.py @@ -0,0 +1,148 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright 2026 Embetrix Embedded Systems Solutions <[email protected]> +# +# U-Boot ECDSA Verified Boot Test + +""" +This tests ECDSA-signed FIT images, verified in software by the MbedTLS-backed +UCLASS_ECDSA driver (CONFIG_ECDSA_VERIFY_MBEDTLS). + +For each supported curve: +- Create a FIT with signed images and sign it, writing the public key into the + control DTB +- Check that 'bootm' reports the signature as verified and boots +- Corrupt the signature and check that verification now fails +- Check a correctly-signed FIT against a different public key and check that it + is rejected + +The corresponding RSA tests live in test_vboot.py. ECDSA is kept separate +because the key generation differs: ECDSA needs an EC key on a specific curve +and no certificate. +""" + +import os +import pytest +import utils +from test_vboot import dtc, make_fit + +# (FIT signature algorithm, OpenSSL curve name, Kconfig symbol for the curve) +TESTDATA = [ + ('ecdsa256', 'prime256v1', 'mbedtls_ecp_dp_secp256r1'), + ('ecdsa384', 'secp384r1', 'mbedtls_ecp_dp_secp384r1'), +] + + [email protected]('sandbox') [email protected]('fit_signature') [email protected]('ecdsa_verify_mbedtls') [email protected]('dtc') [email protected]('fdtget') [email protected]('fdtput') [email protected]('openssl') [email protected]("algo,curve,curve_config", TESTDATA) +def test_vboot_ecdsa(ubman, algo, curve, curve_config): + """Test ECDSA signing with mkimage and verification with 'bootm'. + + Like test_vboot() this works on sandbox only, since it needs to update the + device tree that U-Boot uses to hold the public keys. + """ + if ubman.config.buildconfig.get('config_%s' % curve_config, 'n') != 'y': + pytest.skip('%s is not enabled in this build' % curve_config) + + def create_key(keydir, name='dev'): + """Generate an EC private key for the curve under test.""" + if not os.path.exists(keydir): + os.makedirs(keydir) + utils.run_and_log( + ubman, 'openssl ecparam -name %s -genkey -noout -out %s/%s.key' % + (curve, keydir, name)) + + def sign_fit(keydir, dtb_out, fit_in): + """Sign a FIT, writing the public key into a control device tree.""" + utils.run_and_log(ubman, [mkimage, '-F', '-k', keydir, '-K', dtb_out, + '-r', fit_in]) + + def run_bootm(test_type, expect_string, boots, dtb_in, fit_in): + """Run 'bootm' on a fresh U-Boot instance and check the output. + + A fresh instance is needed because the control device tree carries the + public key, and that differs between the cases below. + """ + ubman.config.dtb = dtb_in + ubman.restart_uboot() + with ubman.log.section('Verified boot %s %s' % (algo, test_type)): + output = ubman.run_command_list([ + 'host load hostfs - 100 %s' % fit_in, + 'fdt addr 100', + 'bootm 100']) + assert expect_string in ''.join(output) + if boots: + assert 'sandbox: continuing, as we cannot run' in ''.join(output) + else: + assert ('sandbox: continuing, as we cannot run' + not in ''.join(output)) + + def corrupt_signature(fit_in, node): + """Flip the last byte of a signature value inside a FIT.""" + value = utils.run_and_log( + ubman, ['fdtget', '-tbx', fit_in, node, 'value']).split() + value[-1] = '%02x' % ((int(value[-1], 16) + 1) % 256) + utils.run_and_log(ubman, + ['fdtput', '-tbx', fit_in, node, 'value'] + value) + + tmpdir = os.path.join(ubman.config.result_dir, algo) + '/' + if not os.path.exists(tmpdir): + os.makedirs(tmpdir) + datadir = ubman.config.source_dir + '/test/py/tests/vboot/' + mkimage = ubman.config.build_dir + '/tools/mkimage' + fit_check_sign = ubman.config.build_dir + '/tools/fit_check_sign' + dtc_args = '-I dts -O dtb -i %s' % tmpdir + dtb = '%ssandbox-u-boot.dtb' % tmpdir + fit = '%stest.fit' % tmpdir + keydir = tmpdir.rstrip('/') + + old_dtb = ubman.config.dtb + try: + create_key(keydir) + + # A dummy kernel image for the FIT to carry + with open('%stest-kernel.bin' % tmpdir, 'wb') as fd: + fd.write(500 * b'\0') + + dtc('sandbox-kernel.dts', ubman, dtc_args, datadir, tmpdir, dtb) + dtc('sandbox-u-boot.dts', ubman, dtc_args, datadir, tmpdir, dtb) + + # A correctly signed FIT must verify and boot + make_fit('sign-images-%s.its' % algo, ubman, mkimage, dtc_args, + datadir, fit) + sign_fit(keydir, dtb, fit) + written = utils.run_and_log( + ubman, ['fdtget', '-ts', dtb, '/signature/dev', 'ecdsa,curve']) + assert curve == written.strip() + run_bootm('signed images', '%s:dev+ OK' % algo, True, dtb, fit) + + # The host-side checker must agree + utils.run_and_log(ubman, [fit_check_sign, '-f', fit, '-k', dtb]) + + # A tampered signature must be rejected + bad_fit = '%stest.badsig.fit' % tmpdir + utils.run_and_log(ubman, ['cp', fit, bad_fit]) + corrupt_signature(bad_fit, '/images/kernel/signature') + run_bootm('corrupted signature', '%s:dev-' % algo, False, dtb, bad_fit) + + # A good signature checked against the wrong public key must be + # rejected + evildir = '%sevilkeys' % tmpdir + evil_dtb = '%sevil-u-boot.dtb' % tmpdir + evil_fit = '%stest.evil.fit' % tmpdir + create_key(evildir) + utils.run_and_log(ubman, ['cp', fit, evil_fit]) + utils.run_and_log(ubman, 'dtc %s %ssandbox-u-boot.dts -O dtb -o %s' % + (dtc_args, datadir, evil_dtb)) + # Signing the copy captures the impostor's public key in evil_dtb + sign_fit(evildir, evil_dtb, evil_fit) + run_bootm('wrong public key', '%s:dev-' % algo, False, evil_dtb, fit) + finally: + # Go back to the original U-Boot with the correct dtb + ubman.config.dtb = old_dtb + ubman.restart_uboot() diff --git a/test/py/tests/vboot/sign-configs-ecdsa256.its b/test/py/tests/vboot/sign-configs-ecdsa256.its new file mode 100644 index 00000000000..4d0ef903a78 --- /dev/null +++ b/test/py/tests/vboot/sign-configs-ecdsa256.its @@ -0,0 +1,45 @@ +/dts-v1/; + +/ { + description = "Chrome OS kernel image with one or more FDT blobs"; + #address-cells = <1>; + + images { + kernel { + data = /incbin/("test-kernel.bin"); + type = "kernel_noload"; + arch = "sandbox"; + os = "linux"; + compression = "none"; + load = <0x4>; + entry = <0x8>; + kernel-version = <1>; + hash-1 { + algo = "sha256"; + }; + }; + fdt-1 { + description = "snow"; + data = /incbin/("sandbox-kernel.dtb"); + type = "flat_dt"; + arch = "sandbox"; + compression = "none"; + fdt-version = <1>; + hash-1 { + algo = "sha256"; + }; + }; + }; + configurations { + default = "conf-1"; + conf-1 { + kernel = "kernel"; + fdt = "fdt-1"; + signature { + algo = "sha256,ecdsa256"; + key-name-hint = "dev"; + sign-images = "fdt", "kernel"; + }; + }; + }; +}; diff --git a/test/py/tests/vboot/sign-configs-ecdsa384.its b/test/py/tests/vboot/sign-configs-ecdsa384.its new file mode 100644 index 00000000000..923bfff2277 --- /dev/null +++ b/test/py/tests/vboot/sign-configs-ecdsa384.its @@ -0,0 +1,45 @@ +/dts-v1/; + +/ { + description = "Chrome OS kernel image with one or more FDT blobs"; + #address-cells = <1>; + + images { + kernel { + data = /incbin/("test-kernel.bin"); + type = "kernel_noload"; + arch = "sandbox"; + os = "linux"; + compression = "none"; + load = <0x4>; + entry = <0x8>; + kernel-version = <1>; + hash-1 { + algo = "sha256"; + }; + }; + fdt-1 { + description = "snow"; + data = /incbin/("sandbox-kernel.dtb"); + type = "flat_dt"; + arch = "sandbox"; + compression = "none"; + fdt-version = <1>; + hash-1 { + algo = "sha256"; + }; + }; + }; + configurations { + default = "conf-1"; + conf-1 { + kernel = "kernel"; + fdt = "fdt-1"; + signature { + algo = "sha384,ecdsa384"; + key-name-hint = "dev"; + sign-images = "fdt", "kernel"; + }; + }; + }; +}; diff --git a/test/py/tests/vboot/sign-images-ecdsa256.its b/test/py/tests/vboot/sign-images-ecdsa256.its new file mode 100644 index 00000000000..009003bb601 --- /dev/null +++ b/test/py/tests/vboot/sign-images-ecdsa256.its @@ -0,0 +1,42 @@ +/dts-v1/; + +/ { + description = "Chrome OS kernel image with one or more FDT blobs"; + #address-cells = <1>; + + images { + kernel { + data = /incbin/("test-kernel.bin"); + type = "kernel_noload"; + arch = "sandbox"; + os = "linux"; + compression = "none"; + load = <0x4>; + entry = <0x8>; + kernel-version = <1>; + signature { + algo = "sha256,ecdsa256"; + key-name-hint = "dev"; + }; + }; + fdt-1 { + description = "snow"; + data = /incbin/("sandbox-kernel.dtb"); + type = "flat_dt"; + arch = "sandbox"; + compression = "none"; + fdt-version = <1>; + signature { + algo = "sha256,ecdsa256"; + key-name-hint = "dev"; + }; + }; + }; + configurations { + default = "conf-1"; + conf-1 { + kernel = "kernel"; + fdt = "fdt-1"; + }; + }; +}; diff --git a/test/py/tests/vboot/sign-images-ecdsa384.its b/test/py/tests/vboot/sign-images-ecdsa384.its new file mode 100644 index 00000000000..f7d498f7d85 --- /dev/null +++ b/test/py/tests/vboot/sign-images-ecdsa384.its @@ -0,0 +1,42 @@ +/dts-v1/; + +/ { + description = "Chrome OS kernel image with one or more FDT blobs"; + #address-cells = <1>; + + images { + kernel { + data = /incbin/("test-kernel.bin"); + type = "kernel_noload"; + arch = "sandbox"; + os = "linux"; + compression = "none"; + load = <0x4>; + entry = <0x8>; + kernel-version = <1>; + signature { + algo = "sha384,ecdsa384"; + key-name-hint = "dev"; + }; + }; + fdt-1 { + description = "snow"; + data = /incbin/("sandbox-kernel.dtb"); + type = "flat_dt"; + arch = "sandbox"; + compression = "none"; + fdt-version = <1>; + signature { + algo = "sha384,ecdsa384"; + key-name-hint = "dev"; + }; + }; + }; + configurations { + default = "conf-1"; + conf-1 { + kernel = "kernel"; + fdt = "fdt-1"; + }; + }; +}; -- 2.43.0
