[meta-security][PATCH v2] dm-verity: add PKCS#7 root hash signature support

Ayoub Zaki <[email protected]>
Newsgroups org.yoctoproject.lists.yocto-patches
Message-ID <[email protected]>
The dm-verity root hash stored in the initramfs is vulnerable to TOCTOU
attacks. Mitigate this by signing the root hash at build time and
verifying it from the kernel via CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG.
The signature is deployed into the initramfs while the signing certificate
is embedded in the kernel's built-in trusted keyring.

- dm-verity-img.bbclass: sign root hash with openssl smime when DM_VERITY_SIGN=1
- kernel-trusted-keys.bbclass: new shared class to collect PEM certs
  into trusted_keys.pem via KERNEL_TRUSTED_CERTS variable
- linux-yocto_security.inc: add kernel config fragments and append
  signing cert via KERNEL_TRUSTED_CERTS
- dm-verity-image-initramfs.bb: deploy .p7s signature
- dmverity initrd script: pass --root-hash-signature to veritysetup
- linux_ima.inc: use KERNEL_TRUSTED_CERTS instead of absolute path in
  CONFIG_SYSTEM_TRUSTED_KEYS, fixing buildpaths QA
- Add documentation

Signing is not enabled by default for backward compatibility but is
strongly recommended for production deployments.

Signed-off-by: Ayoub Zaki <[email protected]>
---
 classes/dm-verity-img.bbclass                 |  46 +++++++-
 classes/kernel-trusted-keys.bbclass           |  34 ++++++
 docs/dm-verity.txt                            | 110 ++++++++++++++++++
 .../recipes-kernel/linux/linux_ima.inc        |   9 +-
 .../images/dm-verity-image-initramfs.bb       |   6 +
 .../initramfs-framework-dm/dmverity           |   7 ++
 .../linux/files/dm-verity-verify.cfg          |  10 ++
 .../linux/files/dm-verity-verify.scc          |   5 +
 recipes-kernel/linux/linux-yocto_security.inc |  10 ++
 9 files changed, 231 insertions(+), 6 deletions(-)
 create mode 100644 classes/kernel-trusted-keys.bbclass
 create mode 100644 recipes-kernel/linux/files/dm-verity-verify.cfg
 create mode 100644 recipes-kernel/linux/files/dm-verity-verify.scc

diff --git a/classes/dm-verity-img.bbclass b/classes/dm-verity-img.bbclass
index 48557e9..4eec085 100644
--- a/classes/dm-verity-img.bbclass
+++ b/classes/dm-verity-img.bbclass
@@ -3,6 +3,12 @@
 # Copyright (C) 2020 BayLibre SAS
 # Author: Bartosz Golaszewski <[email protected]>
 #
+# Copyright (C) 2026 Embetrix Embedded Systems Solutions <[email protected]>
+# - Added PKCS#7 root hash signature support to mitigate TOCTOU attacks.
+#   The root hash is signed at build time and verified by the kernel at boot
+#   via CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG, removing the need to trust
+#   the initramfs as the sole root hash storage.
+#
 # This bbclass allows creating of dm-verity protected partition images. It
 # generates a device image file with dm-verity hash data appended at the end
 # plus the corresponding .env file containing additional information needed
@@ -49,6 +55,21 @@ DM_VERITY_SEPARATE_HASH ?= "0"
 # Additional arguments for veritysetup
 DM_VERITY_SETUP_ARGS ?= ""
 
+# Root hash signing: set to "1" to generate a PKCS#7 signature of the root
+# hash that the kernel verifies via CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG.
+DM_VERITY_SIGN ?= "0"
+
+# No default! Either this or DM_VERITY_SIGN_KEY/DM_VERITY_SIGN_CERT have
+# to be set explicitly in a local.conf before enabling DM_VERITY_SIGN.
+DM_VERITY_SIGN_KEY_DIR ?= "DM_VERITY_SIGN_KEY_DIR_NOT_SET"
+
+# Private key (PEM) used to sign the root hash.
+DM_VERITY_SIGN_KEY ?= "${DM_VERITY_SIGN_KEY_DIR}/privkey_verity.pem"
+
+# X.509 certificate (PEM) corresponding to the signing key. This cert must
+# also be embedded in the kernel via CONFIG_SYSTEM_TRUSTED_KEYS.
+DM_VERITY_SIGN_CERT ?= "${DM_VERITY_SIGN_KEY_DIR}/x509_verity.crt"
+
 # These are arch specific.  We could probably intelligently auto-assign these?
 # Take x86-64 values as defaults. No impact on functionality currently.
 # See SD_GPT_ROOT_X86_64 and SD_GPT_ROOT_X86_64_VERITY in the spec.
@@ -56,7 +77,7 @@ DM_VERITY_SETUP_ARGS ?= ""
 DM_VERITY_ROOT_GUID ?= "4f68bce3-e8cd-4db1-96e7-fbcaf984b709"
 DM_VERITY_RHASH_GUID ?= "2c7357ed-ebd2-46d9-aec1-23d437ec2bf5"
 
-DEPENDS += "bc-native"
+DEPENDS += "bc-native ${@bb.utils.contains('DM_VERITY_SIGN', '1', 'openssl-native', '', d)}"
 
 # Process the output from veritysetup and generate the corresponding .env
 # file. The output from veritysetup is not very machine-friendly so we need to
@@ -160,6 +181,29 @@ verity_setup() {
     # Let's drop the first line of output (doesn't contain any useful info)
     # and feed the rest to another function.
     veritysetup $SETUP_ARGS | tail -n +2 | process_verity
+
+    if [ ${DM_VERITY_SIGN} -eq 1 ]; then
+        if [ ! -f "${DM_VERITY_SIGN_KEY}" ]; then
+            bbfatal "DM_VERITY_SIGN_KEY not found: ${DM_VERITY_SIGN_KEY}"
+        fi
+        if [ ! -f "${DM_VERITY_SIGN_CERT}" ]; then
+            bbfatal "DM_VERITY_SIGN_CERT not found: ${DM_VERITY_SIGN_CERT}"
+        fi
+
+        local ENV="${STAGING_VERITY_DIR}/${DM_VERITY_IMAGE}.$TYPE.verity.env"
+        local SIG="${ENV}.p7s"
+        local ROOTHASH=$(cat $ENV | grep ^ROOT_HASH | sed 's/ROOT_HASH=//')
+        local HASH_HEX=$(mktemp)
+
+        echo -n "$ROOTHASH" > $HASH_HEX
+
+        openssl smime -sign -nocerts -noattr -binary \
+            -in $HASH_HEX \
+            -inkey ${DM_VERITY_SIGN_KEY} -signer ${DM_VERITY_SIGN_CERT} \
+            -outform der -out $SIG
+
+        rm -f $HASH_HEX
+    fi
 }
 
 # make "dateless" symlink for the hash so the wks can find it.
diff --git a/classes/kernel-trusted-keys.bbclass b/classes/kernel-trusted-keys.bbclass
new file mode 100644
index 0000000..3f30e68
--- /dev/null
+++ b/classes/kernel-trusted-keys.bbclass
@@ -0,0 +1,34 @@
+#
+# Copyright (C) 2026 Embetrix Embedded Systems Solutions <[email protected]>
+#
+# Collect PEM certificates into a shared trusted_keys.pem bundle
+# for CONFIG_SYSTEM_TRUSTED_KEYS. Multiple features (IMA, dm-verity
+# signing, etc.) can append their certs to KERNEL_TRUSTED_CERTS.
+#
+# Usage from other classes or .inc files:
+#   KERNEL_TRUSTED_CERTS:append = " ${MY_CERT_PATH}"
+#   inherit kernel-trusted-keys
+
+KERNEL_TRUSTED_CERTS ?= ""
+
+do_configure:append() {
+    if [ -f .config ]; then
+        for cert in ${KERNEL_TRUSTED_CERTS}; do
+            if [ -f "$cert" ]; then
+                if ! grep -q "BEGIN CERTIFICATE" "$cert"; then
+                    bbfatal "$cert is not in PEM format"
+                fi
+                cat "$cert" >> "${B}/trusted_keys.pem"
+            fi
+        done
+        if [ -f "${B}/trusted_keys.pem" ]; then
+            echo 'CONFIG_SYSTEM_TRUSTED_KEYS="trusted_keys.pem"' >> .config
+        fi
+    fi
+}
+
+do_shared_workdir:append() {
+    if [ -f trusted_keys.pem ]; then
+        cp trusted_keys.pem $kerneldir/
+    fi
+}
diff --git a/docs/dm-verity.txt b/docs/dm-verity.txt
index a538fa2..75bb801 100644
--- a/docs/dm-verity.txt
+++ b/docs/dm-verity.txt
@@ -121,3 +121,113 @@ INFO: The image(s) were created using OE kickstart file:
 The "direct" image contains the partition table, bootloader, and dm-verity
 enabled ext4 image all in one -- ready to write to a raw device, such as a
 u-SD card in the case of the beaglebone.
+
+Root Hash Signature Verification
+--------------------------------
+By default, dm-verity stores the root hash as plain text in the initramfs
+at /usr/share/misc/dm-verity.env. Even when the initramfs is verified at
+an earlier boot stage (e.g. bundled with the kernel or part of a signed
+FIT image), there is a window between that verification and the actual use
+of the root hash during which memory contents can potentially be modified
+using hardware-based techniques. This creates a TOCTOU (time-of-check to
+time-of-use) vulnerability that can defeat dm-verity.
+
+To mitigate this, the root hash can be cryptographically signed at build
+time using PKCS#7. The signature is deployed into the initramfs and passed
+to veritysetup via --root-hash-signature. The kernel then verifies the
+signature against certificates embedded in its built-in trusted keyring
+(CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG). No certificate is stored in the
+initramfs and the trust anchor is the kernel binary itself.
+
+This feature requires a secure boot chain to protect the kernel image.
+
+Signing Setup
+~~~~~~~~~~~~~
+1. Generate a signing key pair (do this once, keep the private key safe):
+
+   openssl req -new -x509 -newkey rsa:4096 -nodes \
+       -keyout privkey_verity.pem -out x509_verity.crt \
+       -days 3650 -subj "/CN=dm-verity signing key"
+
+2. Add the following to your conf/local.conf or distro config:
+
+   DM_VERITY_SIGN = "1"
+   DM_VERITY_SIGN_KEY_DIR = "/path/to/keys"
+
+   The key directory should contain privkey_verity.pem (private key) and
+   x509_verity.crt (X.509 certificate). The certificate serves a dual
+   purpose:
+   - It is used by openssl at build time to create the PKCS#7 signature
+   - It is embedded into the kernel via CONFIG_SYSTEM_TRUSTED_KEYS so the
+     kernel can verify the signature at boot
+
+   Alternatively, set DM_VERITY_SIGN_KEY and DM_VERITY_SIGN_CERT
+   individually to override the default filenames.
+
+3. The build will automatically:
+   - Enable CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG in the kernel
+   - Embed the signing certificate in the kernel's trusted keyring
+   - Sign the root hash and produce a .p7s signature file
+   - Deploy the signature into the initramfs
+
+4. At boot, the initramfs dmverity script passes the signature to
+   veritysetup, which forwards it to the kernel. The kernel verifies
+   the root hash against its built-in trusted keys before activating
+   the dm-verity target. If verification fails, the device will not
+   be created and the boot will fail.
+
+Hardening
+~~~~~~~~~
+By default CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG only verifies the
+signature when one is provided. A dm-verity target created without a
+signature will still be accepted. To enforce that ALL dm-verity targets
+must have a valid root hash signature, add the following to the kernel
+command line:
+
+   dm_verity.require_signatures=1
+
+With this parameter set, any attempt to create a dm-verity device
+without a valid PKCS#7 signature will be rejected by the kernel. This
+is strongly recommended for production deployments.
+
+Note on trusted_keys.pem
+~~~~~~~~~~~~~~~~~~~~~~~~
+The kernel-trusted-keys.bbclass aggregates certificates into a file called
+trusted_keys.pem in the kernel build directory and in work-shared. Despite
+the name (inherited from the kernel's CONFIG_SYSTEM_TRUSTED_KEYS), this
+file contains only X.509 certificates (public keys). No private key
+material is handled by the kernel's built-in trusted keyring.
+
+Vendor Kernel Integration
+~~~~~~~~~~~~~~~~~~~~~~~~~
+Since meta-security only bbappends linux-yocto, integrating root hash
+signature verification with a vendor kernel requires adding the following
+kernel configuration options:
+
+   CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG=y
+   CONFIG_KEYS=y
+   CONFIG_ASYMMETRIC_KEY_TYPE=y
+   CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE=y
+   CONFIG_X509_CERTIFICATE_PARSER=y
+   CONFIG_PKCS7_MESSAGE_PARSER=y
+   CONFIG_SYSTEM_TRUSTED_KEYRING=y
+   CONFIG_SYSTEM_TRUSTED_KEYS="trusted_keys.pem"
+   CONFIG_CRYPTO_RSA=y
+   CONFIG_CRYPTO_SHA256=y
+
+These can be applied as a fragment or via your kernel's
+configuration mechanism.
+
+Additionally, ensure your kernel recipe inherits kernel-trusted-keys and
+points the signing certificate via KERNEL_TRUSTED_CERTS:
+
+   KERNEL_TRUSTED_CERTS:append = "${DM_VERITY_SIGN_CERT}"
+   inherit kernel-trusted-keys
+
+Compatibility Notes
+~~~~~~~~~~~~~~~~~~~
+- Signing is optional but strongly recommended. It is backward compatible;
+  when DM_VERITY_SIGN is "0" (the default) the behavior is unchanged.
+- Both appended hash and separate hash partition modes support signing.
+- The signing certificate can coexist with other certificates already set
+  in CONFIG_SYSTEM_TRUSTED_KEYS (e.g. for IMA/EVM).
diff --git a/meta-integrity/recipes-kernel/linux/linux_ima.inc b/meta-integrity/recipes-kernel/linux/linux_ima.inc
index 415476a..fbedaf9 100644
--- a/meta-integrity/recipes-kernel/linux/linux_ima.inc
+++ b/meta-integrity/recipes-kernel/linux/linux_ima.inc
@@ -1,9 +1,8 @@
+IMA_EVM_KEY_DIR ?= "IMA_EVM_KEY_DIR_NOT_SET"
+IMA_EVM_ROOT_CA ?= "${IMA_EVM_KEY_DIR}/ima-local-ca.pem"
 
-do_configure:append() {
-    if [ "${@bb.utils.contains('DISTRO_FEATURES', 'ima', 'yes', '', d)}" = "yes" ] && [ -f .config ] ; then
-        sed -i "s|^CONFIG_SYSTEM_TRUSTED_KEYS=.*|CONFIG_SYSTEM_TRUSTED_KEYS=\"${IMA_EVM_ROOT_CA}\"|" .config
-    fi
-}
+KERNEL_TRUSTED_CERTS:append = " ${@d.getVar('IMA_EVM_ROOT_CA') if bb.utils.contains('DISTRO_FEATURES', 'ima', True, False, d) else ''}"
+inherit kernel-trusted-keys
 
 KERNEL_FEATURES:append = " ${@bb.utils.contains('DISTRO_FEATURES', 'modsign', ' features/ima/modsign.scc', '', d)}"
 KERNEL_FEATURES:append = " ${@bb.utils.contains('DISTRO_FEATURES', 'ima', ' features/ima/ima.scc', '', d)}"
diff --git a/recipes-core/images/dm-verity-image-initramfs.bb b/recipes-core/images/dm-verity-image-initramfs.bb
index 7ed83dc..41fa53a 100644
--- a/recipes-core/images/dm-verity-image-initramfs.bb
+++ b/recipes-core/images/dm-verity-image-initramfs.bb
@@ -39,5 +39,11 @@ deploy_verity_hash() {
     install -D -m 0644 \
         ${STAGING_VERITY_DIR}/${DM_VERITY_IMAGE}.${DM_VERITY_IMAGE_TYPE}.verity.env \
         ${IMAGE_ROOTFS}${datadir}/misc/dm-verity.env
+
+    local SIG="${STAGING_VERITY_DIR}/${DM_VERITY_IMAGE}.${DM_VERITY_IMAGE_TYPE}.verity.env.p7s"
+    if [ -f "$SIG" ]; then
+        install -D -m 0644 "$SIG" \
+            ${IMAGE_ROOTFS}${datadir}/misc/dm-verity.sig
+    fi
 }
 IMAGE_PREPROCESS_COMMAND += "deploy_verity_hash;"
diff --git a/recipes-core/initrdscripts/initramfs-framework-dm/dmverity b/recipes-core/initrdscripts/initramfs-framework-dm/dmverity
index 1923490..20b06f2 100644
--- a/recipes-core/initrdscripts/initramfs-framework-dm/dmverity
+++ b/recipes-core/initrdscripts/initramfs-framework-dm/dmverity
@@ -12,6 +12,11 @@ dmverity_run() {
 
     . /usr/share/misc/dm-verity.env
 
+    ROOTHASH_SIG_ARG=""
+    if [ -f /usr/share/misc/dm-verity.sig ]; then
+        ROOTHASH_SIG_ARG="--root-hash-signature=/usr/share/misc/dm-verity.sig"
+    fi
+
     C=0
     delay=${bootparam_rootdelay:-1}
     timeout=${bootparam_roottimeout:-5}
@@ -30,6 +35,7 @@ dmverity_run() {
 
         veritysetup \
             --data-block-size=${DATA_BLOCK_SIZE} \
+            ${ROOTHASH_SIG_ARG} \
             create rootfs \
             /dev/disk/by-partuuid/${ROOT_UUID} \
             /dev/disk/by-partuuid/${RHASH_UUID} \
@@ -81,6 +87,7 @@ dmverity_run() {
     veritysetup \
         --data-block-size=${DATA_BLOCK_SIZE} \
         --hash-offset=${DATA_SIZE} \
+        ${ROOTHASH_SIG_ARG} \
         create rootfs \
         ${RDEV} \
         ${RDEV} \
diff --git a/recipes-kernel/linux/files/dm-verity-verify.cfg b/recipes-kernel/linux/files/dm-verity-verify.cfg
new file mode 100644
index 0000000..aba6a67
--- /dev/null
+++ b/recipes-kernel/linux/files/dm-verity-verify.cfg
@@ -0,0 +1,10 @@
+CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG=y
+CONFIG_KEYS=y
+CONFIG_ASYMMETRIC_KEY_TYPE=y
+CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE=y
+CONFIG_X509_CERTIFICATE_PARSER=y
+CONFIG_PKCS7_MESSAGE_PARSER=y
+CONFIG_SYSTEM_TRUSTED_KEYRING=y
+CONFIG_SYSTEM_TRUSTED_KEYS="trusted_keys.pem"
+CONFIG_CRYPTO_RSA=y
+CONFIG_CRYPTO_SHA256=y
diff --git a/recipes-kernel/linux/files/dm-verity-verify.scc b/recipes-kernel/linux/files/dm-verity-verify.scc
new file mode 100644
index 0000000..2af676a
--- /dev/null
+++ b/recipes-kernel/linux/files/dm-verity-verify.scc
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: MIT
+define KFEATURE_DESCRIPTION "Enable dm-verity root hash signature verification"
+define KFEATURE_COMPATIBILITY board
+
+kconf hardware dm-verity-verify.cfg
diff --git a/recipes-kernel/linux/linux-yocto_security.inc b/recipes-kernel/linux/linux-yocto_security.inc
index 3a2ff96..505f0db 100644
--- a/recipes-kernel/linux/linux-yocto_security.inc
+++ b/recipes-kernel/linux/linux-yocto_security.inc
@@ -5,3 +5,13 @@ KERNEL_FEATURES:append = " ${@bb.utils.contains("DISTRO_FEATURES", "smack", " fe
 KERNEL_FEATURES:append = " ${@bb.utils.contains("IMAGE_CLASSES", "dm-verity-img", " features/device-mapper/dm-verity.scc", "" ,d)}"
 KERNEL_FEATURES:append = " features/ecryptfs/ecryptfs.scc"
 SRC_URI += " ${@bb.utils.contains("DISTRO_FEATURES", "lkrg", "file://lkrg.scc", "" ,d)}"
+
+DM_VERITY_SIGN ?= "0"
+DM_VERITY_SIGN_KEY_DIR ?= "DM_VERITY_SIGN_KEY_DIR_NOT_SET"
+DM_VERITY_SIGN_CERT ?= "${DM_VERITY_SIGN_KEY_DIR}/x509_verity.crt"
+
+SRC_URI += " ${@bb.utils.contains("DM_VERITY_SIGN", "1", "file://dm-verity-verify.scc file://dm-verity-verify.cfg", "" ,d)}"
+KERNEL_FEATURES:append = " ${@bb.utils.contains("DM_VERITY_SIGN", "1", " dm-verity-verify.scc", "" ,d)}"
+
+KERNEL_TRUSTED_CERTS:append = " ${@d.getVar('DM_VERITY_SIGN_CERT') if d.getVar('DM_VERITY_SIGN') == '1' else ''}"
+inherit kernel-trusted-keys
-- 
2.43.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.