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

Reply via email to