diff mbox series

AW: [EXT] [OE-core] [PATCH v7 11/20] kernel-fit-image.bbclass: add a new FIT image implementation

Message ID 0d773cb9aace446eb588342c42d17916@diehl.com
State New
Headers show
Series AW: [EXT] [OE-core] [PATCH v7 11/20] kernel-fit-image.bbclass: add a new FIT image implementation | expand

Commit Message

Denis OSTERLAND-HEIM Dec. 4, 2025, 2:04 p.m. UTC
Hi Adrian,

I try to use the new fit image implementation in conjunction with meta-openembeddeds signing class.
In this case PKCS11 URI is used instead of crt/key files and the sanity check fails.
Shall I add an additional variable to switch of the checks?

Regards Denis

-----Ursprüngliche Nachricht-----
Von: openembedded-core@lists.openembedded.org <openembedded-core@lists.openembedded.org> Im Auftrag von Adrian Freihofer via lists.openembedded.org
Gesendet: Dienstag, 3. Juni 2025 10:23
An: openembedded-core@lists.openembedded.org
Cc: Adrian Freihofer <adrian.freihofer@siemens.com>
Betreff: [EXT] [OE-core] [PATCH v7 11/20] kernel-fit-image.bbclass: add a new FIT image implementation

[EXTERNAL EMAIL]


From: Adrian Freihofer <adrian.freihofer@siemens.com>

The new recipe linux-yocto-fitimage.bb and the new
kernel-fit-image.bbclass are intended to become successors of the
kernel-fitimage.bbclass.

Instead of injecting the FIT image related build steps into the kernel
recipe, the new recipe takes the kernel artifacts from the kernel recipe
and creates the FIT image as an independent task.

This solves some basic problems:
* sstate does not work well when a fitImage contains an initramfs. The
  kernel is rebuilt from scratch if the build runs from an empty TMPDIR.
* A fitImage kernel is not available as a package, but all other kernel
  image types are.
* The task dependencies in the kernel are very complex and difficult to
  debug if something goes wrong. As a separate, downstream recipe, this
  is now much easier.

The recipe takes the kernel artifacts from the deploy folder. There was
also a test implementation passing the kernel artifacts via sysroot
directory. This requires changes on the kernel.bbclass to make it
copying the artifacts also to the sysroot directory while the same
artifacts are already in the sstate-cached deploy directory.

The new class kernel-fit-extra-artifacts.bbclass generates and deploys
the kernel binary intended for inclusion in a FIT image.
Note that the kernel used in a FIT image is a stripped (and optionally
compressed) vmlinux ELF binary - not a self-extracting format like
zImage, which is already available in the deploy directory if needed
separately.
The kernel-fit-extra-artifacts.bbclass can be used like this:
    KERNEL_CLASSES += "kernel-fit-extra-artifacts"
(if uImage support is not needed, or with :append otherwise)

The long story about this issue is here:
[YOCTO #12912]

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
---
 .../kernel-fit-extra-artifacts.bbclass        |  19 +
 meta/classes-recipe/kernel-fit-image.bbclass  | 187 ++++++
 meta/classes/multilib.bbclass                 |   1 +
 meta/lib/oe/fitimage.py                       | 547 ++++++++++++++++++
 .../linux/linux-yocto-fitimage.bb             |  13 +
 5 files changed, 767 insertions(+)
 create mode 100644 meta/classes-recipe/kernel-fit-extra-artifacts.bbclass
 create mode 100644 meta/classes-recipe/kernel-fit-image.bbclass
 create mode 100644 meta/lib/oe/fitimage.py
 create mode 100644 meta/recipes-kernel/linux/linux-yocto-fitimage.bb

--
2.49.0

Diehl Metering GmbH, Donaustrasse 120, 90451 Nuernberg
Sitz der Gesellschaft: Ansbach, Registergericht: Ansbach HRB 69
Geschaeftsfuehrer: Dr. Christof Bosbach (Sprecher), Dipl.-Dolm. Annette Geuther, Dipl.-Kfm. Reiner Edel, Jean-Claude Luttringer

Bitte denken Sie an die Umwelt, bevor Sie diese E-Mail drucken. Diese E-Mail kann vertrauliche Informationen enthalten. Sollten die in dieser E-Mail enthaltenen Informationen nicht für Sie bestimmt sein, informieren Sie bitte unverzueglich den Absender per E-Mail und loeschen Sie diese E-Mail in Ihrem System. Jede unberechtigte Form der Reproduktion, Bekanntgabe, Aenderung, Verteilung und/oder Publikation dieser E-Mail ist strengstens untersagt. Informationen zum Datenschutz finden Sie auf unserer Homepage<https://www.diehl.com/metering/de/impressum-und-rechtliche-hinweise/>.

Before printing, think about environmental responsibility.This message may contain confidential information. If you are not authorized to receive this information please advise the sender immediately by reply e-mail and delete this message without making any copies. Any form of unauthorized use, publication, reproduction, copying or disclosure of the e-mail is not permitted. Information about data protection can be found on our homepage<https://www.diehl.com/metering/en/data-protection/>.

Comments

Freihofer, Adrian Dec. 4, 2025, 3:01 p.m. UTC | #1
Hi Denis

I'm not entirely sure which FIT image implementation you are using: the one from oe-core or the one from meta-openembedded. Since you are asking me personally, I assume it is the one from oe-core.

I suspect that the classes from meta-openembedded are not compatible with the FIT image implementation in oe-core. But I'm probably not the right person to answer this question, as I don't even understand why a second FIT image implementation was necessary instead of improving the one in oe-core. From my perspective, it would probably be better to extend the run_mkimage_sign function in oe-core with PKCS#11 support than to maintain a second FIT image implementation in meta-openembedded.

Another approach is to perform the signing within bitbake using a key from a file. Signing with a more secure PKCS#11 backend can be performed as a re-signing, which is done independently of bitbake as the final step in the release process.

Regards,
Adrian
diff mbox series

Patch

diff --git a/meta/classes-recipe/kernel-fit-extra-artifacts.bbclass b/meta/classes-recipe/kernel-fit-extra-artifacts.bbclass
new file mode 100644
index 00000000000..385fe9895a3
--- /dev/null
+++ b/meta/classes-recipe/kernel-fit-extra-artifacts.bbclass
@@ -0,0 +1,19 @@ 
+#
+# Copyright OpenEmbedded Contributors
+#
+# SPDX-License-Identifier: MIT
+#
+
+# Generate and deploy additional artifacts required for FIT image creation.
+# To use this class, add it to the KERNEL_CLASSES variable.
+
+inherit kernel-uboot
+
+kernel_do_deploy:append() {
+# Provide the kernel artifacts to post processing recipes e.g. for creating a FIT image
+uboot_prep_kimage "$deployDir"
+# For x86 a setup.bin needs to be include"d in a fitImage as well
+if [ -e ${KERNEL_OUTPUT_DIR}/setup.bin ]; then
+install -D "${B}/${KERNEL_OUTPUT_DIR}/setup.bin" "$deployDir/"
+fi
+}
diff --git a/meta/classes-recipe/kernel-fit-image.bbclass b/meta/classes-recipe/kernel-fit-image.bbclass
new file mode 100644
index 00000000000..6d80cd4bb47
--- /dev/null
+++ b/meta/classes-recipe/kernel-fit-image.bbclass
@@ -0,0 +1,187 @@ 
+
+inherit kernel-arch kernel-artifact-names uboot-config deploy
+require conf/image-fitimage.conf
+
+S = "${WORKDIR}/sources"
+UNPACKDIR = "${S}"
+
+PACKAGE_ARCH = "${MACHINE_ARCH}"
+
+DEPENDS += "\
+    u-boot-tools-native dtc-native \
+    ${@'kernel-signing-keys-native' if d.getVar('FIT_GENERATE_KEYS') == '1' else ''} \
+"
+
+python () {
+    image = d.getVar('INITRAMFS_IMAGE')
+    if image and d.getVar('INITRAMFS_IMAGE_BUNDLE') != '1':
+        if d.getVar('INITRAMFS_MULTICONFIG'):
+            mc = d.getVar('BB_CURRENT_MC')
+            d.appendVarFlag('do_compile', 'mcdepends', ' mc:' + mc + ':${INITRAMFS_MULTICONFIG}:${INITRAMFS_IMAGE}:do_image_complete')
+        else:
+            d.appendVarFlag('do_compile', 'depends', ' ${INITRAMFS_IMAGE}:do_image_complete')
+
+    #check if there are any dtb providers
+    providerdtb = d.getVar("PREFERRED_PROVIDER_virtual/dtb")
+    if providerdtb:
+        d.appendVarFlag('do_compile', 'depends', ' virtual/dtb:do_populate_sysroot')
+        d.setVar('EXTERNAL_KERNEL_DEVICETREE', "${RECIPE_SYSROOT}/boot/devicetree")
+}
+
+do_configure[noexec] = "1"
+
+UBOOT_MKIMAGE_KERNEL_TYPE ?= "kernel"
+KERNEL_IMAGEDEST ?= "/boot"
+
+python do_compile() {
+    import shutil
+    import oe.fitimage
+
+    itsfile = "fit-image.its"
+    fitname = "fitImage"
+    kernel_deploydir = d.getVar('DEPLOY_DIR_IMAGE')
+    kernel_deploysubdir = d.getVar('KERNEL_DEPLOYSUBDIR')
+    if kernel_deploysubdir:
+        kernel_deploydir = os.path.join(kernel_deploydir, kernel_deploysubdir)
+
+    # Collect all the its nodes before the its file is generated and mkimage gets executed
+    root_node = oe.fitimage.ItsNodeRootKernel(
+        d.getVar("FIT_DESC"), d.getVar("FIT_ADDRESS_CELLS"),
+        d.getVar('HOST_PREFIX'), d.getVar('UBOOT_ARCH'),  d.getVar("FIT_CONF_PREFIX"),
+        oe.types.boolean(d.getVar('UBOOT_SIGN_ENABLE')), d.getVar("UBOOT_SIGN_KEYDIR"),
+        d.getVar("UBOOT_MKIMAGE"), d.getVar("UBOOT_MKIMAGE_DTCOPTS"),
+        d.getVar("UBOOT_MKIMAGE_SIGN"), d.getVar("UBOOT_MKIMAGE_SIGN_ARGS"),
+        d.getVar('FIT_HASH_ALG'), d.getVar('FIT_SIGN_ALG'), d.getVar('FIT_PAD_ALG'),
+        d.getVar('UBOOT_SIGN_KEYNAME'),
+        oe.types.boolean(d.getVar('FIT_SIGN_INDIVIDUAL')), d.getVar('UBOOT_SIGN_IMG_KEYNAME')
+    )
+
+    # Prepare a kernel image section.
+    shutil.copyfile(os.path.join(kernel_deploydir, "linux.bin"), "linux.bin")
+    with open(os.path.join(kernel_deploydir, "linux_comp")) as linux_comp_f:
+        linux_comp = linux_comp_f.read()
+    root_node.fitimage_emit_section_kernel("kernel-1", "linux.bin", linux_comp,
+        d.getVar('UBOOT_LOADADDRESS'), d.getVar('UBOOT_ENTRYPOINT'),
+        d.getVar('UBOOT_MKIMAGE_KERNEL_TYPE'), d.getVar("UBOOT_ENTRYSYMBOL"))
+
+    # Prepare a DTB image section
+    kernel_devicetree = d.getVar('KERNEL_DEVICETREE')
+    external_kernel_devicetree = d.getVar("EXTERNAL_KERNEL_DEVICETREE")
+    if kernel_devicetree:
+        for dtb in kernel_devicetree.split():
+            # In deploy_dir the DTBs are without sub-directories also with KERNEL_DTBVENDORED = "1"
+            dtb_name = os.path.basename(dtb)
+
+            # Skip DTB if it's also provided in EXTERNAL_KERNEL_DEVICETREE directory
+            if external_kernel_devicetree:
+                ext_dtb_path = os.path.join(external_kernel_devicetree, dtb_name)
+                if os.path.exists(ext_dtb_path) and os.path.getsize(ext_dtb_path) > 0:
+                    continue
+
+            # Copy the dtb or dtbo file into the FIT image assembly directory
+            shutil.copyfile(os.path.join(kernel_deploydir, dtb_name), dtb_name)
+            root_node.fitimage_emit_section_dtb(dtb_name, dtb_name,
+                d.getVar("UBOOT_DTB_LOADADDRESS"), d.getVar("UBOOT_DTBO_LOADADDRESS"))
+
+    if external_kernel_devicetree:
+        # iterate over all .dtb and .dtbo files in the external kernel devicetree directory
+        # and copy them to the FIT image assembly directory
+        for dtb_name in sorted(os.listdir(external_kernel_devicetree)):
+            if dtb_name.endswith('.dtb') or dtb_name.endswith('.dtbo'):
+                dtb_path = os.path.join(external_kernel_devicetree, dtb_name)
+
+                # For symlinks, add a configuration node that refers to the DTB image node to which the symlink points
+                symlink_target = oe.fitimage.symlink_points_below(dtb_name, external_kernel_devicetree)
+                if symlink_target:
+                    root_node.fitimage_emit_section_dtb_alias(dtb_name, symlink_target, True)
+                # For real DTB files add an image node and a configuration node
+                else:
+                    shutil.copyfile(dtb_path, dtb_name)
+                    root_node.fitimage_emit_section_dtb(dtb_name, dtb_name,
+                        d.getVar("UBOOT_DTB_LOADADDRESS"), d.getVar("UBOOT_DTBO_LOADADDRESS"), True)
+
+    # Prepare a u-boot script section
+    fit_uboot_env = d.getVar("FIT_UBOOT_ENV")
+    if fit_uboot_env:
+        root_node.fitimage_emit_section_boot_script("bootscr-"+fit_uboot_env , fit_uboot_env)
+
+    # Prepare a setup section (For x86)
+    setup_bin_path = os.path.join(kernel_deploydir, "setup.bin")
+    if os.path.exists(setup_bin_path):
+        shutil.copyfile(setup_bin_path, "setup.bin")
+        root_node.fitimage_emit_section_setup("setup-1", "setup.bin")
+
+    # Prepare a ramdisk section.
+    initramfs_image = d.getVar('INITRAMFS_IMAGE')
+    if initramfs_image and d.getVar("INITRAMFS_IMAGE_BUNDLE") != '1':
+        # Find and use the first initramfs image archive type we find
+        found = False
+        for img in d.getVar("FIT_SUPPORTED_INITRAMFS_FSTYPES").split():
+            initramfs_path = os.path.join(d.getVar("DEPLOY_DIR_IMAGE"), "%s.%s" % (d.getVar('INITRAMFS_IMAGE_NAME'), img))
+            if os.path.exists(initramfs_path):
+                bb.note("Found initramfs image: " + initramfs_path)
+                found = True
+                root_node.fitimage_emit_section_ramdisk("ramdisk-1", initramfs_path,
+                    initramfs_image,
+                    d.getVar("UBOOT_RD_LOADADDRESS"),
+                    d.getVar("UBOOT_RD_ENTRYPOINT"))
+                break
+            else:
+                bb.note("Did not find initramfs image: " + initramfs_path)
+
+        if not found:
+            bb.fatal("Could not find a valid initramfs type for %s, the supported types are: %s" % (d.getVar('INITRAMFS_IMAGE_NAME'), d.getVar('FIT_SUPPORTED_INITRAMFS_FSTYPES')))
+
+    # Generate the configuration section
+    root_node.fitimage_emit_section_config(d.getVar("FIT_CONF_DEFAULT_DTB"))
+
+    # Write the its file
+    root_node.write_its_file(itsfile)
+
+    # Assemble the FIT image
+    root_node.run_mkimage_assemble(itsfile, fitname)
+
+    # Sign the FIT image if required
+    root_node.run_mkimage_sign(fitname)
+}
+do_compile[depends] += "virtual/kernel:do_deploy"
+
+do_install() {
+    install -d "${D}/${KERNEL_IMAGEDEST}"
+    install -m 0644 "${B}/fitImage" "${D}/${KERNEL_IMAGEDEST}/fitImage"
+}
+
+FILES:${PN} = "${KERNEL_IMAGEDEST}"
+
+
+do_deploy() {
+    deploy_dir="${DEPLOYDIR}"
+    if [ -n "${KERNEL_DEPLOYSUBDIR}" ]; then
+        deploy_dir="${DEPLOYDIR}/${KERNEL_DEPLOYSUBDIR}"
+    fi
+    install -d "$deploy_dir"
+    install -m 0644 "${B}/fitImage" "$deploy_dir/fitImage"
+    install -m 0644 "${B}/fit-image.its" "$deploy_dir/fit-image.its"
+
+    if [ "${INITRAMFS_IMAGE_BUNDLE}" != "1" ]; then
+        ln -snf fit-image.its "$deploy_dir/fitImage-its-${KERNEL_FIT_NAME}.its"
+        if [ -n "${KERNEL_FIT_LINK_NAME}" ] ; then
+            ln -snf fit-image.its "$deploy_dir/fitImage-its-${KERNEL_FIT_LINK_NAME}"
+        fi
+    fi
+
+    if [ -n "${INITRAMFS_IMAGE}" ]; then
+        ln -snf fit-image-its "$deploy_dir/fitImage-its-${INITRAMFS_IMAGE_NAME}-${KERNEL_FIT_NAME}.its"
+        if [ -n "${KERNEL_FIT_LINK_NAME}" ]; then
+            ln -snf fit-image.its "$deploy_dir/fitImage-its-${INITRAMFS_IMAGE_NAME}-${KERNEL_FIT_LINK_NAME}"
+        fi
+
+        if [ "${INITRAMFS_IMAGE_BUNDLE}" != "1" ]; then
+            ln -snf fitImage "$deploy_dir/fitImage-${INITRAMFS_IMAGE_NAME}-${KERNEL_FIT_NAME}${KERNEL_FIT_BIN_EXT}"
+            if [ -n "${KERNEL_FIT_LINK_NAME}" ] ; then
+                ln -snf fitImage "$deploy_dir/fitImage-${INITRAMFS_IMAGE_NAME}-${KERNEL_FIT_LINK_NAME}"
+            fi
+        fi
+    fi
+}
+addtask deploy after do_compile before do_build
diff --git a/meta/classes/multilib.bbclass b/meta/classes/multilib.bbclass
index a4151658a62..b959bbd93c0 100644
--- a/meta/classes/multilib.bbclass
+++ b/meta/classes/multilib.bbclass
@@ -21,6 +21,7 @@  python multilib_virtclass_handler () {
     bpn = d.getVar("BPN")
     if ("virtual/kernel" in provides
             or bb.data.inherits_class('module-base', d)
+            or bb.data.inherits_class('kernel-fit-image', d)
             or bpn in non_ml_recipes):
         raise bb.parse.SkipRecipe("We shouldn't have multilib variants for %s" % bpn)

diff --git a/meta/lib/oe/fitimage.py b/meta/lib/oe/fitimage.py
new file mode 100644
index 00000000000..f3037991558
--- /dev/null
+++ b/meta/lib/oe/fitimage.py
@@ -0,0 +1,547 @@ 
+#
+# Copyright OpenEmbedded Contributors
+#
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# This file contains common functions for the fitimage generation
+
+import os
+import shlex
+import subprocess
+import bb
+
+from oeqa.utils.commands import runCmd
+
+class ItsNode:
+    INDENT_SIZE = 8
+
+    def __init__(self, name, parent_node, sub_nodes=None, properties=None):
+        self.name = name
+        self.parent_node = parent_node
+
+        self.sub_nodes = []
+        if sub_nodes:
+            self.sub_nodes = sub_nodes
+
+        self.properties = {}
+        if properties:
+            self.properties = properties
+
+        if parent_node:
+            parent_node.add_sub_node(self)
+
+    def add_sub_node(self, sub_node):
+        self.sub_nodes.append(sub_node)
+
+    def add_property(self, key, value):
+        self.properties[key] = value
+
+    def emit(self, f, indent):
+        indent_str_name = " " * indent
+        indent_str_props = " " * (indent + self.INDENT_SIZE)
+        f.write("%s%s {\n" % (indent_str_name, self.name))
+        for key, value in self.properties.items():
+            bb.debug(1, "key: %s, value: %s" % (key, str(value)))
+            # Single integer: <0x12ab>
+            if isinstance(value, int):
+                f.write(indent_str_props + key + ' = <0x%x>;\n' % value)
+            # list of strings: "string1", "string2" or integers: <0x12ab 0x34cd>
+            elif isinstance(value, list):
+                if len(value) == 0:
+                    f.write(indent_str_props + key + ' = "";\n')
+                elif isinstance(value[0], int):
+                    list_entries = ' '.join('0x%x' % entry for entry in value)
+                    f.write(indent_str_props + key + ' = <%s>;\n' % list_entries)
+                else:
+                    list_entries = ', '.join('"%s"' % entry for entry in value)
+                    f.write(indent_str_props + key + ' = %s;\n' % list_entries)
+            elif isinstance(value, str):
+                # path: /incbin/("path/to/file")
+                if key in ["data"] and value.startswith('/incbin/('):
+                    f.write(indent_str_props + key + ' = %s;\n' % value)
+                # Integers which are already string formatted
+                elif value.startswith("<") and value.endswith(">"):
+                    f.write(indent_str_props + key + ' = %s;\n' % value)
+                else:
+                    f.write(indent_str_props + key + ' = "%s";\n' % value)
+            else:
+                bb.fatal("%s has unexpexted data type." % str(value))
+        for sub_node in self.sub_nodes:
+            sub_node.emit(f, indent + self.INDENT_SIZE)
+        f.write(indent_str_name + '};\n')
+
+class ItsNodeImages(ItsNode):
+    def __init__(self, parent_node):
+        super().__init__("images", parent_node)
+
+class ItsNodeConfigurations(ItsNode):
+    def __init__(self, parent_node):
+        super().__init__("configurations", parent_node)
+
+class ItsNodeHash(ItsNode):
+    def __init__(self, name, parent_node, algo, opt_props=None):
+        properties = {
+            "algo": algo
+        }
+        if opt_props:
+            properties.update(opt_props)
+        super().__init__(name, parent_node, None, properties)
+
+class ItsImageSignature(ItsNode):
+    def __init__(self, name, parent_node, algo, keyname, opt_props=None):
+        properties = {
+            "algo": algo,
+            "key-name-hint": keyname
+        }
+        if opt_props:
+            properties.update(opt_props)
+        super().__init__(name, parent_node, None, properties)
+
+class ItsNodeImage(ItsNode):
+    def __init__(self, name, parent_node, description, type, compression, sub_nodes=None, opt_props=None):
+        properties = {
+            "description": description,
+            "type": type,
+            "compression": compression,
+        }
+        if opt_props:
+            properties.update(opt_props)
+        super().__init__(name, parent_node, sub_nodes, properties)
+
+class ItsNodeDtb(ItsNodeImage):
+    def __init__(self, name, parent_node, description, type, compression,
+                 sub_nodes=None, opt_props=None, compatible=None):
+        super().__init__(name, parent_node, description, type, compression, sub_nodes, opt_props)
+        self.compatible = compatible
+
+class ItsNodeDtbAlias(ItsNode):
+    """Additional Configuration Node for a DTB
+
+    Symlinks pointing to a DTB file are handled by an addtitional
+    configuration node referring to another DTB image node.
+    """
+    def __init__(self, name, alias_name, compatible=None):
+        super().__init__(name, parent_node=None, sub_nodes=None, properties=None)
+        self.alias_name = alias_name
+        self.compatible = compatible
+
+class ItsNodeConfigurationSignature(ItsNode):
+    def __init__(self, name, parent_node, algo, keyname, opt_props=None):
+        properties = {
+            "algo": algo,
+            "key-name-hint": keyname
+        }
+        if opt_props:
+            properties.update(opt_props)
+        super().__init__(name, parent_node, None, properties)
+
+class ItsNodeConfiguration(ItsNode):
+    def __init__(self, name, parent_node, description, sub_nodes=None, opt_props=None):
+        properties = {
+            "description": description,
+        }
+        if opt_props:
+            properties.update(opt_props)
+        super().__init__(name, parent_node, sub_nodes, properties)
+
+class ItsNodeRootKernel(ItsNode):
+    """Create FIT images for the kernel
+
+    Currently only a single kernel (no less or more) can be added to the FIT
+    image along with 0 or more device trees and 0 or 1 ramdisk.
+
+    If a device tree included in the FIT image, the default configuration is the
+    firt DTB. If there is no dtb present than the default configuation the kernel.
+    """
+    def __init__(self, description, address_cells, host_prefix, arch, conf_prefix,
+                 sign_enable=False, sign_keydir=None,
+                 mkimage=None, mkimage_dtcopts=None,
+                 mkimage_sign=None, mkimage_sign_args=None,
+                 hash_algo=None, sign_algo=None, pad_algo=None,
+                 sign_keyname_conf=None,
+                 sign_individual=False, sign_keyname_img=None):
+        props = {
+            "description": description,
+            "#address-cells": f"<{address_cells}>"
+        }
+        super().__init__("/", None, None, props)
+        self.images = ItsNodeImages(self)
+        self.configurations = ItsNodeConfigurations(self)
+
+        self._host_prefix = host_prefix
+        self._arch = arch
+        self._conf_prefix = conf_prefix
+
+        # Signature related properties
+        self._sign_enable = sign_enable
+        self._sign_keydir = sign_keydir
+        self._mkimage = mkimage
+        self._mkimage_dtcopts = mkimage_dtcopts
+        self._mkimage_sign = mkimage_sign
+        self._mkimage_sign_args = mkimage_sign_args
+        self._hash_algo = hash_algo
+        self._sign_algo = sign_algo
+        self._pad_algo = pad_algo
+        self._sign_keyname_conf = sign_keyname_conf
+        self._sign_individual = sign_individual
+        self._sign_keyname_img = sign_keyname_img
+        self._sanitize_sign_config()
+
+        self._dtbs = []
+        self._dtb_alias = []
+        self._kernel = None
+        self._ramdisk = None
+        self._bootscr = None
+        self._setup = None
+
+    def _sanitize_sign_config(self):
+        if self._sign_enable:
+            if not self._hash_algo:
+                bb.fatal("FIT image signing is enabled but no hash algorithm is provided.")
+            if not self._sign_algo:
+                bb.fatal("FIT image signing is enabled but no signature algorithm is provided.")
+            if not self._pad_algo:
+                bb.fatal("FIT image signing is enabled but no padding algorithm is provided.")
+            if not self._sign_keyname_conf:
+                bb.fatal("FIT image signing is enabled but no configuration key name is provided.")
+            if self._sign_individual and not self._sign_keyname_img:
+                bb.fatal("FIT image signing is enabled for individual images but no image key name is provided.")
+
+    def write_its_file(self, itsfile):
+        with open(itsfile, 'w') as f:
+            f.write("/dts-v1/;\n\n")
+            self.emit(f, 0)
+
+    def its_add_node_image(self, image_id, description, image_type, compression, opt_props):
+        image_node = ItsNodeImage(
+            image_id,
+            self.images,
+            description,
+            image_type,
+            compression,
+            opt_props=opt_props
+        )
+        if self._hash_algo:
+            ItsNodeHash(
+                "hash-1",
+                image_node,
+                self._hash_algo
+            )
+        if self._sign_individual:
+            ItsImageSignature(
+                "signature-1",
+                image_node,
+                f"{self._hash_algo},{self._sign_algo}",
+                self._sign_keyname_img
+            )
+        return image_node
+
+    def its_add_node_dtb(self, image_id, description, image_type, compression, opt_props, compatible):
+        dtb_node = ItsNodeDtb(
+            image_id,
+            self.images,
+            description,
+            image_type,
+            compression,
+            opt_props=opt_props,
+            compatible=compatible
+        )
+        if self._hash_algo:
+            ItsNodeHash(
+                "hash-1",
+                dtb_node,
+                self._hash_algo
+            )
+        if self._sign_individual:
+            ItsImageSignature(
+                "signature-1",
+                dtb_node,
+                f"{self._hash_algo},{self._sign_algo}",
+                self._sign_keyname_img
+            )
+        return dtb_node
+
+    def fitimage_emit_section_kernel(self, kernel_id, kernel_path, compression,
+        load, entrypoint, mkimage_kernel_type, entrysymbol=None):
+        """Emit the fitImage ITS kernel section"""
+        if self._kernel:
+            bb.fatal("Kernel section already exists in the ITS file.")
+        if entrysymbol:
+            result = subprocess.run([self._host_prefix + "nm", "vmlinux"], capture_output=True, text=True)
+            for line in result.stdout.splitlines():
+                parts = line.split()
+                if len(parts) == 3 and parts[2] == entrysymbol:
+                    entrypoint = "<0x%s>" % parts[0]
+                    break
+        kernel_node = self.its_add_node_image(
+            kernel_id,
+            "Linux kernel",
+            mkimage_kernel_type,
+            compression,
+            {
+                "data": '/incbin/("' + kernel_path + '")',
+                "arch": self._arch,
+                "os": "linux",
+                "load": f"<{load}>",
+                "entry": f"<{entrypoint}>"
+            }
+        )
+        self._kernel = kernel_node
+
+    def fitimage_emit_section_dtb(self, dtb_id, dtb_path, dtb_loadaddress=None,
+                                  dtbo_loadaddress=None, add_compatible=False):
+        """Emit the fitImage ITS DTB section"""
+        load=None
+        dtb_ext = os.path.splitext(dtb_path)[1]
+        if dtb_ext == ".dtbo":
+            if dtbo_loadaddress:
+                load = dtbo_loadaddress
+        elif dtb_loadaddress:
+            load = dtb_loadaddress
+
+        opt_props = {
+            "data": '/incbin/("' + dtb_path + '")',
+            "arch": self._arch
+        }
+        if load:
+            opt_props["load"] = f"<{load}>"
+
+        # Preserve the DTB's compatible string to be added to the configuration node
+        compatible = None
+        if add_compatible:
+            compatible = get_compatible_from_dtb(dtb_path)
+
+        dtb_node = self.its_add_node_dtb(
+            "fdt-" + dtb_id,
+            "Flattened Device Tree blob",
+            "flat_dt",
+            "none",
+            opt_props,
+            compatible
+        )
+        self._dtbs.append(dtb_node)
+
+    def fitimage_emit_section_dtb_alias(self, dtb_alias_id, dtb_path, add_compatible=False):
+        """Add a configuration node referring to another DTB"""
+        # Preserve the DTB's compatible string to be added to the configuration node
+        compatible = None
+        if add_compatible:
+            compatible = get_compatible_from_dtb(dtb_path)
+
+        dtb_id = os.path.basename(dtb_path)
+        dtb_alias_node = ItsNodeDtbAlias("fdt-" + dtb_id, dtb_alias_id, compatible)
+        self._dtb_alias.append(dtb_alias_node)
+        bb.warn(f"compatible: {compatible}, dtb_alias_id: {dtb_alias_id}, dtb_id: {dtb_id}, dtb_path: {dtb_path}")
+
+    def fitimage_emit_section_boot_script(self, bootscr_id, bootscr_path):
+        """Emit the fitImage ITS u-boot script section"""
+        if self._bootscr:
+            bb.fatal("U-boot script section already exists in the ITS file.")
+        bootscr_node = self.its_add_node_image(
+            bootscr_id,
+            "U-boot script",
+            "script",
+            "none",
+            {
+                "data": '/incbin/("' + bootscr_path + '")',
+                "arch": self._arch,
+                "type": "script"
+            }
+        )
+        self._bootscr = bootscr_node
+
+    def fitimage_emit_section_setup(self, setup_id, setup_path):
+        """Emit the fitImage ITS setup section"""
+        if self._setup:
+            bb.fatal("Setup section already exists in the ITS file.")
+        load = "<0x00090000>"
+        entry = "<0x00090000>"
+        setup_node = self.its_add_node_image(
+            setup_id,
+            "Linux setup.bin",
+            "x86_setup",
+            "none",
+            {
+                "data": '/incbin/("' + setup_path + '")',
+                "arch": self._arch,
+                "os": "linux",
+                "load": load,
+                "entry": entry
+            }
+        )
+        self._setup = setup_node
+
+    def fitimage_emit_section_ramdisk(self, ramdisk_id, ramdisk_path, description="ramdisk", load=None, entry=None):
+        """Emit the fitImage ITS ramdisk section"""
+        if self._ramdisk:
+            bb.fatal("Ramdisk section already exists in the ITS file.")
+        opt_props = {
+            "data": '/incbin/("' + ramdisk_path + '")',
+            "type": "ramdisk",
+            "arch": self._arch,
+            "os": "linux"
+        }
+        if load:
+            opt_props["load"] = f"<{load}>"
+        if entry:
+            opt_props["entry"] = f"<{entry}>"
+
+        ramdisk_node = self.its_add_node_image(
+            ramdisk_id,
+            description,
+            "ramdisk",
+            "none",
+            opt_props
+        )
+        self._ramdisk = ramdisk_node
+
+    def _fitimage_emit_one_section_config(self, conf_node_name, dtb=None):
+        """Emit the fitImage ITS configuration section"""
+        opt_props = {}
+        conf_desc = []
+        sign_entries = []
+
+        if self._kernel:
+            conf_desc.append("Linux kernel")
+            opt_props["kernel"] = self._kernel.name
+            if self._sign_enable:
+                sign_entries.append("kernel")
+
+        if dtb:
+            conf_desc.append("FDT blob")
+            opt_props["fdt"] = dtb.name
+            if dtb.compatible:
+                opt_props["compatible"] = dtb.compatible
+            if self._sign_enable:
+                sign_entries.append("fdt")
+
+        if self._ramdisk:
+            conf_desc.append("ramdisk")
+            opt_props["ramdisk"] = self._ramdisk.name
+            if self._sign_enable:
+                sign_entries.append("ramdisk")
+
+        if self._bootscr:
+            conf_desc.append("u-boot script")
+            opt_props["bootscr"] = self._bootscr.name
+            if self._sign_enable:
+                sign_entries.append("bootscr")
+
+        if self._setup:
+            conf_desc.append("setup")
+            opt_props["setup"] = self._setup.name
+            if self._sign_enable:
+                sign_entries.append("setup")
+
+        # First added configuration is the default configuration
+        default_flag = "0"
+        if len(self.configurations.sub_nodes) == 0:
+            default_flag = "1"
+
+        conf_node = ItsNodeConfiguration(
+            conf_node_name,
+            self.configurations,
+            f"{default_flag} {', '.join(conf_desc)}",
+            opt_props=opt_props
+        )
+        if self._hash_algo:
+            ItsNodeHash(
+                "hash-1",
+                conf_node,
+                self._hash_algo
+            )
+        if self._sign_enable:
+            ItsNodeConfigurationSignature(
+                "signature-1",
+                conf_node,
+                f"{self._hash_algo},{self._sign_algo}",
+                self._sign_keyname_conf,
+                opt_props={
+                    "padding": self._pad_algo,
+                    "sign-images": sign_entries
+                }
+            )
+
+    def fitimage_emit_section_config(self, default_dtb_image=None):
+        if self._dtbs:
+            for dtb in self._dtbs:
+                dtb_name = dtb.name
+                if dtb.name.startswith("fdt-"):
+                    dtb_name = dtb.name[len("fdt-"):]
+                self._fitimage_emit_one_section_config(self._conf_prefix + dtb_name, dtb)
+            for dtb in self._dtb_alias:
+                self._fitimage_emit_one_section_config(self._conf_prefix + dtb.alias_name, dtb)
+        else:
+            # Currently exactly one kernel is supported.
+            self._fitimage_emit_one_section_config(self._conf_prefix + "1")
+
+        default_conf = self.configurations.sub_nodes[0].name
+        if default_dtb_image and self._dtbs:
+            default_conf = self._conf_prefix + default_dtb_image
+        self.configurations.add_property('default', default_conf)
+
+    def run_mkimage_assemble(self, itsfile, fitfile):
+        cmd = [
+            self._mkimage,
+            '-f', itsfile,
+            fitfile
+        ]
+        if self._mkimage_dtcopts:
+            cmd.insert(1, '-D')
+            cmd.insert(2, self._mkimage_dtcopts)
+        try:
+            subprocess.run(cmd, check=True, capture_output=True)
+        except subprocess.CalledProcessError as e:
+            bb.fatal(f"Command '{' '.join(cmd)}' failed with return code {e.returncode}\nstdout: {e.stdout.decode()}\nstderr: {e.stderr.decode()}\nitsflile: {os.path.abspath(itsfile)}")
+
+    def run_mkimage_sign(self, fitfile):
+        if not self._sign_enable:
+            bb.debug(1, "FIT image signing is disabled. Skipping signing.")
+            return
+
+        # Some sanity checks because mkimage exits with 0 also without needed keys
+        sign_key_path = os.path.join(self._sign_keydir, self._sign_keyname_conf)
+        if not os.path.exists(sign_key_path + '.key') or not os.path.exists(sign_key_path + '.crt'):
+            bb.fatal("%s.key or .crt does not exist" % sign_key_path)
+        if self._sign_individual:
+            sign_key_img_path = os.path.join(self._sign_keydir, self._sign_keyname_img)
+            if not os.path.exists(sign_key_img_path + '.key') or not os.path.exists(sign_key_img_path + '.crt'):
+                bb.fatal("%s.key or .crt does not exist" % sign_key_img_path)
+
+        cmd = [
+            self._mkimage_sign,
+            '-F',
+            '-k', self._sign_keydir,
+            '-r', fitfile
+        ]
+        if self._mkimage_dtcopts:
+            cmd.extend(['-D', self._mkimage_dtcopts])
+        if self._mkimage_sign_args:
+            cmd.extend(shlex.split(self._mkimage_sign_args))
+        try:
+            subprocess.run(cmd, check=True, capture_output=True)
+        except subprocess.CalledProcessError as e:
+            bb.fatal(f"Command '{' '.join(cmd)}' failed with return code {e.returncode}\nstdout: {e.stdout.decode()}\nstderr: {e.stderr.decode()}")
+
+
+def symlink_points_below(file_or_symlink, expected_parent_dir):
+    """returns symlink destination if it points below directory"""
+    file_path = os.path.join(expected_parent_dir, file_or_symlink)
+    if not os.path.islink(file_path):
+        return None
+
+    realpath = os.path.relpath(os.path.realpath(file_path), expected_parent_dir)
+    if realpath.startswith(".."):
+        return None
+
+    return realpath
+
+def get_compatible_from_dtb(dtb_path, fdtget_path="fdtget"):
+    compatible = None
+    cmd = [fdtget_path, "-t", "s", dtb_path, "/", "compatible"]
+    try:
+        ret = subprocess.run(cmd, check=True, capture_output=True, text=True)
+        compatible = ret.stdout.strip().split()
+    except subprocess.CalledProcessError:
+        compatible = None
+    return compatible
diff --git a/meta/recipes-kernel/linux/linux-yocto-fitimage.bb b/meta/recipes-kernel/linux/linux-yocto-fitimage.bb
new file mode 100644
index 00000000000..6ce1960a871
--- /dev/null
+++ b/meta/recipes-kernel/linux/linux-yocto-fitimage.bb
@@ -0,0 +1,13 @@ 
+SUMMARY = "The Linux kernel as a FIT image (optionally with initramfs)"
+SECTION = "kernel"
+
+# If an initramfs is included in the FIT image more licenses apply.
+# But also the kernel uses more than one license (see Documentation/process/license-rules.rst)
+LICENSE = "GPL-2.0-with-Linux-syscall-note"
+LIC_FILES_CHKSUM = "file://${COREBASE}/meta/files/common-licenses/GPL-2.0-with-Linux-syscall-note;md5=0bad96c422c41c3a94009dcfe1bff992"
+
+inherit linux-kernel-base kernel-fit-image
+
+# Set the version of this recipe to the version of the included kernel
+# (without taking the long way around via PV)
+PKGV = "${@get_kernelversion_file("${STAGING_KERNEL_BUILDDIR}")}"