diff mbox series

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

Message ID 20260423091102.26875-1-ayoub.zaki@embetrix.com
State New
Headers show
Series [meta-security,v2] dm-verity: add PKCS#7 root hash signature support | expand

Commit Message

Ayoub Zaki April 23, 2026, 9:11 a.m. UTC
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 <ayoub.zaki@embetrix.com>
---
 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 mbox series

Patch

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 <bgolaszewski@baylibre.com>
 #
+# Copyright (C) 2026 Embetrix Embedded Systems Solutions <ayoub.zaki@embetrix.com>
+# - 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 <ayoub.zaki@embetrix.com>
+#
+# 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