[PATCH v2 3/7] tools: add mkmbn tool for Qualcomm

Casey Connolly casey.connolly at linaro.org
Mon Jun 2 18:10:54 CEST 2025


This is a fork of qtestsign[1] with modifications to integrate with the
U-Boot build system.

New Qualcomm dev boards flash U-Boot to the "uefi" partition, the format
is a standard ELF file with custom program headers containing Qualcomm
signatures, hashes and other metadata.

Since different boards require different load addresses, the traditional
CONFIG_REMAKE_ELF with CONFIG_TEXT_BASE requires introducing a new
defconfig for each platform, even though the binary is otherwise
identical.

Since we already need to process the ELF file to produce a valid "MBN"
which sbl1 will accept, mkmbn additionally inspects the U-Boot binary,
finding the DTB and then checking for known compatible strings to
identify the board or platform.

With this, one can build U-Boot with qcom_defconfig:

$ make DEVICE_TREE=qcom/qcs6490-rb3gen2

Then build an MBN with

$ ./tools/qcom/mkmbn/mkmbn.py u-boot.bin

The resulting u-boot.mbn will have the correct load address and can be
directly flashed to the board from the bootROM with edl.py

[1]: https://github.com/msm8916-mainline/qtestsign

Signed-off-by: Casey Connolly <casey.connolly at linaro.org>
---
 tools/mkmbn                 |   1 +
 tools/qcom/mkmbn/cert.py    | 158 +++++++++++++++++++++++
 tools/qcom/mkmbn/elf.py     | 243 ++++++++++++++++++++++++++++++++++
 tools/qcom/mkmbn/hashseg.py | 308 ++++++++++++++++++++++++++++++++++++++++++++
 tools/qcom/mkmbn/mkmbn.py   | 154 ++++++++++++++++++++++
 5 files changed, 864 insertions(+)

diff --git a/tools/mkmbn b/tools/mkmbn
new file mode 120000
index 0000000000000000000000000000000000000000..a7b2096756f76c07ca21e73c63f3b8be28a4cf59
--- /dev/null
+++ b/tools/mkmbn
@@ -0,0 +1 @@
+qcom/mkmbn/mkmbn.py
\ No newline at end of file
diff --git a/tools/qcom/mkmbn/cert.py b/tools/qcom/mkmbn/cert.py
new file mode 100644
index 0000000000000000000000000000000000000000..2fb195dce0d59f739bf40249e1fcf04e7546f315
--- /dev/null
+++ b/tools/qcom/mkmbn/cert.py
@@ -0,0 +1,158 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2021-2022 Stephan Gerhold
+# See https://www.qualcomm.com/media/documents/files/secure-boot-and-image-authentication-technical-overview-v1-0.pdf
+# Somewhat based on code snippets from https://cryptography.io/en/latest/x509/tutorial.html
+from __future__ import annotations
+
+from datetime import datetime
+from typing import List
+
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.x509.oid import NameOID
+
+# NOTE: The certificate chain generated by qtestsign is NOT meant
+# to be secure. The private keys are listed here to make the
+# resulting files reproducible. THESE KEYS SHOULD ONLY BE USED
+# FOR TESTING AND NOT FOR A PROPER SECURE BOOT SETUP.
+
+ROOT_KEY = serialization.load_pem_private_key(
+    b"""
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCjZqF/BwggY4Rs
+Q1/wSNPLEKQEROZ9i/d+7CXZCukWph+SKHlv652oiAp+TgzIGQQXDlaA+qUoXUjp
+g2KTmoulfQjrgc5CSCk6yA01VxNBqR81JorJx8aD9ApOFVoERlmWhZcR3B/LsVyd
+vYgwFNNkqUh7fyywyy1Z1ijk4SyJVak1VxfdkTTeb1wr5Awjvh82PrdRQfOvctFH
+mVITqdMckdRD3Sx7y8EvypAYpAUiWklNgditetXFjMoV6XyXTPCRkH9zzskrXP6i
+neCyS7xUfEYPYNpabzhpdvkx9Is2PlCJA1fZ1ERZsWcag5vDZa3SHslH5Kh9+ssH
+ps0Ul1j5AgMBAAECggEAHUEzOdBy+oWGwHFhnF4VmT4t91u0npawJYe3EQBckgMF
+FQBtGYYoMHPG2S01KaAc9NnK0AXQCwWEl9Y/kGizhtn3fl67pG9R/mWxw7KGzpMu
+dLAlWhIL7zUCoU8+UhScVpAtZ3OvN6NWDyHPX7hizptmUEIJKM//mx12LeBIvn+P
+8tSiBXxoDGl0JZ+QMzmshOUXLLnxKITgBGL+G9A1qTZHIs6VV7HWH1ptfObulBZf
+yEBK1YBzI6GnBGzLOWnZqGsSbQ717SObQo5rCoRDZB7z4bXNWDEvuH+rqzcs5liu
+af4gmBHNOLGh+Ta5HJ0XeoqU5ANOWlUi95/n2dJufwKBgQDNaMlT1937SHPv/eBq
+Be3MobllTx4vMYh6CtfP8QozTE+sTcmCyvaWVfXwLnQTl//+siefoWsvzW43LaNU
+3A18nCxVFSSbWosBN+0Zo4K9bSpEFGgUrJM5O98zv4+/SzCKFe2562usDzaRiEUW
+iSJkzIUnSlcNc+XCY1rhG9HLXwKBgQDLpS6ATtMDSP9p+XYVMEN2CF8M3xvL	roOT
+6wPYfp9fuagMgzNv9GB9SRyM/dM6mN+fkBqLp3EbDZT0UorHsg+YChoyBmctNqpW
+j5/SrVyYe2xoRRgOzUbDstN44/LAhJLQnOXB7S2amo35zZ4FY6sw2w3QfkCildkB
+mY3VhvESpwKBgErLtUPKfxJZN55UG7t/nS++U/wH6z3UE5YdDKizZLt5NinPyWjO
+7yue8Ycb4zifSKA9zx/Zb2Zgr5l4DNmBp4eQdrQklsfbGHLBIp0LZTgE4DcaFyww
+Cwv0OTpmrrlBb9NYWNAyYWqtv3kO3dlu5g8+Sd4cu8YyRZ+a/iSqNKKRAoGBAIPf
+QICYCq8a60Lt5xiLe3QIsbx9EdvQ86Wqz3+3Z28uo3MO1xVNc9pNqO5oRAuzCUSj
+pXz//g9duTKJ7RKp7M0w5Yu1d8TgnGeXdBCScN7RNf9DlvOm3IdH2wdy3TTr5MKw
+h1wQQbLXGM9F5mlpBGeLwqNbznE6hh8yF5XJX30LAoGBAJqaa+yeZskti5ickNTF
+vBBIXyYYBymdxfkf9vDSW1XcZEIVqo3+AGV+qHyTjURaty3QuEhSJEXem/obH5uE
+y37+bnx8Se1IyJ/phYBLwOmtgZoBJALFhvjkFiGTF6naI8E/i4sbi5j/OEyShfWr
+YFZuEKQJhiiMQznfNgthHU6H
+-----END PRIVATE KEY-----
+""",
+    password=None,
+)
+
+ATT_KEY = serialization.load_pem_private_key(
+    b"""
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDT449phHltY2aV
+QIvaT4PUgNS7wDybnnjVO88NGB5PjfUaWY99oDQgOLJlejyVVqRO2wHxLaUMsbuc
+oe0XbgSFJgrnGvG6yPbjSXeIfV5k2dJG60S4Fg2mZ1ieSabuPVKLA03frhbATmIf
+Q+VTMlWLgLVxcT04iqph6VpjehnYke0VPMuN7OM6RsIOEhLcje0bvL4YjTYXH5j4
+mPquc/ZEj/n6WJ6VsS27QygOBbaiGqHs54QnQi4gcgIgUmkR/bl2wL5s+729RBzS
+v1FZfA5gdM9uEG3ogLHOC2uk+1Nuqcdk/tQxd30/2ulXubqDku/nNY2RSJrwD	att
+qcyCliANAgMBAAECggEAK3Z7HVbKHZENIsJZrY8v6HAAsv5ssDMicALTpsjytrjU
+tPH4B/nLl2xp03zuXmemTnKIBHOrbl4qsKdaXbr4fGNgSyVwvjKoydhxB3NH4IH5
+qwhpUSVc6Ww7dkR/VFEJ1G/6Ek7AZfPuFqGzsYwalgHxtfJXb3iqGGloXA1Yrd5p
+W2cTEhtSFZP/PQIEK773wYd3aYMw8OCqG2V5bw9N3xwY6KTC0Px8zyBlmAcUBPAj
+QZL/DTGlMdD9+PJ3Ft3Zl2uS7ORn7xfXftvxv5IQdD+JBxV5zUIympKK/7KIVUfH
+dfi93R7rqjL9EOP6bVQkg/WzYRLeVf/8km8HRGqtIQKBgQDxFxQ77t4EQqLPFofN
+oRV7P3lvFqlJDTzAGBnjIT/ujT3SgoFUjRtfG27nWd1lycxv3tE0GTIw0LjJwvmg
+VSFbQbPsmdp+f0jnNIiJayiG591j9Afmw06mnDodaQuSTp7K9idgpnFRGDQzwJHK
+0DwSQzlzEXsPhGnXxpv+2Q+ANQKBgQDg/itVFBw3e5wC8boffi1AgnM97Qa/Y+5B
+I2J9+cZD9iBkvE7kTwVUOI2Rr+XkQmSf+pT6L0yFXhQjIed004rpKVqzTvGL9VXJ
+nBeADS4bxl1jsfkfvq9e6eNUK8vzyLoYQpS5/LK1oG5MPq3+30yzGIHM8JxxaOQ9
+VdKQrUdLeQKBgAh35RAN3eKMbKeVhQOmCtkfa6aJRzz3qBCfSBmAS3yXnXpNdzl/
+E10N26FouKwgoHu1eee4ktjAHB2KKbaGBvvrnORMqy4STn9AiyM4jl3euxoNslFa
+vuJ/TlNGI0/qTw2WA+ATOJu+m+bNdtGG6vVBQz1VedsbrZQUt9oFydOZAoGAMlCk
+4CHfLYk3GnF0bhaJiCOkIfUfzS1L2sVPAV0aOZiRJfX2rpf9WRhMkIgFoUY3uo8P
+QePR+QFQ/4pVeIrWRc45ul+tJN94j92YY8qOxSdXOzRRwgeisFcdv3UL5zi8ZTB+
+khkw3e1CvUpHHvhQ7rxMSsiEM9iBMjY/IJuflgECgYEAqiN3eg8cZjVrYEMcPLGx
+wXknCG0KPc8EpDi1moNwS3z/TcUbfP8vnmT2lFHTAbvVBn+4fcLffkQBoGG3AaSH
+3kc0HXLdy+rFcsXpX7hk9BM/Uey9dqBOAusLS6XxYhcAJ1xOI0kYWoeOhO8fcjNa
+tf26cJGzfbbwf8kfisbv4Uk=
+-----END PRIVATE KEY-----
+""",
+    password=None,
+)
+
+
+def _begin_cert() -> x509.CertificateBuilder:
+    return (
+        x509.CertificateBuilder()
+        .serial_number(1)
+        .not_valid_before(datetime(2023, 1, 1))
+        .not_valid_after(datetime(9999, 12, 31, 23, 59, 59))
+    )  # no well-defined expiration date, see RFC5280 4.1.2.5.
+
+
+def generate_chain(ou_fields: List[str]) -> bytes:
+    # First, create the root CA
+    root_name = x509.Name(
+        [
+            x509.NameAttribute(NameOID.COMMON_NAME, "qtestsign Root CA - NOT SECURE"),
+        ]
+    )
+    # only key_cert_sign=True
+    root_usage = x509.KeyUsage(
+        False, False, False, False, False, True, False, False, False
+    )
+    root_ski = x509.SubjectKeyIdentifier.from_public_key(ROOT_KEY.public_key())
+    root_cert_der = (
+        _begin_cert()
+        .subject_name(root_name)
+        .issuer_name(root_name)
+        .public_key(ROOT_KEY.public_key())
+        .add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
+        .add_extension(root_usage, critical=True)
+        .add_extension(root_ski, critical=False)
+        .sign(ROOT_KEY, hashes.SHA256())
+        .public_bytes(serialization.Encoding.DER)
+    )
+
+    # Now, create the attestation certificate
+    att_name = x509.Name(
+        [
+            x509.NameAttribute(
+                NameOID.COMMON_NAME, "qtestsign Attestation CA - NOT SECURE"
+            ),
+            *[
+                x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, ou)
+                for ou in ou_fields
+            ],
+        ]
+    )
+    # only digital_signature=True
+    att_usage = x509.KeyUsage(
+        True, False, False, False, False, False, False, False, False
+    )
+    att_cert_der = (
+        _begin_cert()
+        .subject_name(att_name)
+        .issuer_name(root_name)
+        .public_key(ATT_KEY.public_key())
+        .add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
+        .add_extension(att_usage, critical=True)
+        .add_extension(
+            x509.SubjectKeyIdentifier.from_public_key(ATT_KEY.public_key()),
+            critical=False,
+        )
+        .add_extension(
+            x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(root_ski),
+            critical=False,
+        )
+        .sign(ROOT_KEY, hashes.SHA256())
+        .public_bytes(serialization.Encoding.DER)
+    )
+
+    # The certificate chain is the attestation and root certificate concatenated
+    # in DER format. Note: The order (first attestation, then root) is important!
+    return att_cert_der + root_cert_der
diff --git a/tools/qcom/mkmbn/elf.py b/tools/qcom/mkmbn/elf.py
new file mode 100644
index 0000000000000000000000000000000000000000..38e145b6f62bf482bcd8161c90dab569c525b288
--- /dev/null
+++ b/tools/qcom/mkmbn/elf.py
@@ -0,0 +1,243 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2021 Stephan Gerhold
+# Data classes are based on the header definitions in the ELF(5) man page.
+# Also see: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
+from __future__ import annotations
+
+import dataclasses
+from dataclasses import dataclass
+from struct import Struct
+from typing import List, BinaryIO
+
+
+ at dataclass
+class Ehdr:
+    ei_magic: bytes
+    ei_class: int
+    ei_data: int
+    ei_version: int
+    ei_os_abi: int
+    ei_abi_version: int
+    e_type: int
+    e_machine: int
+    e_version: int
+    # Address size specific part
+    e_entry: int = 0
+    e_phoff: int = 0
+    e_shoff: int = 0
+    # End part
+    e_flags: int = 0
+    e_ehsize: int = 0
+    e_phentsize: int = 0
+    e_phnum: int = 0
+    e_shentsize: int = 0
+    e_shnum: int = 0
+    e_shstrndx: int = 0
+
+    START_FORMAT = Struct("<4s5B7xHHL")
+    START_COUNT = 9
+    MEM_FORMAT32 = Struct("<LLL")
+    MEM_FORMAT64 = Struct("<QQQ")
+    MEM_COUNT = 3
+    END_FORMAT = Struct("<L6H")
+    END_COUNT = 7
+
+    CLASS32 = 1
+    CLASS64 = 2
+
+    # Init a qcom XBL style ELF header
+    def __init__(self):
+        self.ei_magic = b"\x7fELF"
+        self.ei_class = 2
+        self.ei_data = 1
+        self.ei_version = 1
+        self.ei_os_abi = 0
+        self.ei_abi_version = 0
+        self.e_type = 2
+        self.e_machine = 183
+        self.e_version = 1
+
+        self.e_ehsize = 64
+        self.e_phoff = 64
+        self.e_phentsize = 56
+
+    @staticmethod
+    def parse(b: bytes) -> Ehdr:
+        hdr_unpack = Ehdr.START_FORMAT.unpack_from(b)
+        hdr = Ehdr(*hdr_unpack)
+        assert hdr.ei_magic == b"\x7fELF", f"Invalid ELF header magic: {hdr.ei_magic}"
+        assert hdr.ei_data == 1, "Only little endian supported at the moment"
+        assert hdr.ei_version == 1, f"Unexpected ei_version: {hdr.ei_version}"
+        assert hdr.e_version == 1, f"Unexpected e_version: {hdr.e_version}"
+
+        if hdr.ei_class == Ehdr.CLASS32:
+            mem_format = Ehdr.MEM_FORMAT32
+        else:
+            assert hdr.ei_class == Ehdr.CLASS64, f"Unexpected ei_class: {hdr.ei_class}"
+            mem_format = Ehdr.MEM_FORMAT64
+
+        mem_unpack = mem_format.unpack_from(b, Ehdr.START_FORMAT.size)
+        end_unpack = Ehdr.END_FORMAT.unpack_from(
+            b, Ehdr.START_FORMAT.size + mem_format.size
+        )
+        return Ehdr(*hdr_unpack, *mem_unpack, *end_unpack)
+
+    def save(self, f: BinaryIO) -> int:
+        unpack = dataclasses.astuple(self)
+        written = f.write(Ehdr.START_FORMAT.pack(*unpack[: Ehdr.START_COUNT]))
+
+        if self.ei_class == Ehdr.CLASS32:
+            mem_format = Ehdr.MEM_FORMAT32
+        else:
+            mem_format = Ehdr.MEM_FORMAT64
+        written += f.write(
+            mem_format.pack(
+                *unpack[Ehdr.START_COUNT : Ehdr.START_COUNT + Ehdr.MEM_COUNT]
+            )
+        )
+        written += f.write(Ehdr.END_FORMAT.pack(*unpack[-Ehdr.END_COUNT :]))
+        return written
+
+
+ at dataclass
+class Phdr:
+    p_type: int
+    p_offset: int
+    p_vaddr: int
+    p_paddr: int
+    p_filesz: int
+    p_memsz: int
+    p_flags: int
+    p_align: int
+
+    data = None
+
+    FORMAT32 = Struct("<8L")
+    FORMAT64 = Struct("<LL6Q")
+
+    @staticmethod
+    def parse(b: bytes, offset: int, ei_class: int) -> Phdr:
+        if ei_class == Ehdr.CLASS32:
+            unpack = Phdr.FORMAT32.unpack_from(b, offset)
+        else:
+            unpack = list(Phdr.FORMAT64.unpack_from(b, offset))
+
+            # ELFCLASS64 has flags directly before offset for alignment
+            flags = unpack.pop(1)
+            unpack.insert(-1, flags)
+
+        return Phdr(*unpack)
+
+    @staticmethod
+    def from_bin(b: bytes, loadaddr: int) -> Phdr:
+        # p_offset is fixed later
+        phdr = Phdr(
+            p_type=1,
+            p_offset=0xFFFFFFFF,
+            p_vaddr=loadaddr,
+            p_paddr=loadaddr,
+            p_filesz=len(b),
+            p_memsz=len(b),
+            p_flags=7,
+            p_align=0x1000,
+        )
+        phdr.data = memoryview(b)
+        return phdr
+
+    def save(self, f: BinaryIO, ei_class: int) -> int:
+        unpack = dataclasses.astuple(self)
+
+        if ei_class == Ehdr.CLASS32:
+            return f.write(Phdr.FORMAT32.pack(*unpack))
+        else:
+            unpack = list(unpack)
+
+            # ELFCLASS64 has flags directly before offset for alignment
+            flags = unpack.pop(-2)
+            unpack.insert(1, flags)
+
+            return f.write(Phdr.FORMAT64.pack(*unpack))
+
+
+def _pad(f: BinaryIO, offset: int, pos: int) -> int:
+    assert offset >= pos, f"{offset} >= {pos}"
+    pad = offset - pos
+    if pad:
+        assert f.write(b"\0" * pad) == pad
+    return offset
+
+
+def _align(i: int, alignment: int) -> int:
+    mask = max(alignment - 1, 0)
+    return (i + mask) & ~mask
+
+
+ at dataclass
+class Elf:
+    ehdr: Ehdr
+    phdrs: List[Phdr]
+
+    def __init__(self):
+        self.ehdr = Ehdr()
+        self.phdrs: List[Phdr] = []
+
+    def total_header_size(self):
+        return self.ehdr.e_phoff + len(self.phdrs) * self.ehdr.e_phentsize
+
+    @staticmethod
+    def parse(b: bytes) -> Elf:
+        ehdr = Ehdr.parse(b)
+        view = memoryview(b)
+
+        # Parse program headers
+        phdrs = []
+        offset = ehdr.e_phoff
+        for i in range(ehdr.e_phnum):
+            phdr = Phdr.parse(b, offset, ehdr.ei_class)
+            phdrs.append(phdr)
+
+            # Store data if necessary
+            if phdr.p_filesz and phdr.p_offset:
+                phdr.data = view[phdr.p_offset : phdr.p_offset + phdr.p_filesz]
+
+            offset += ehdr.e_phentsize
+
+        return Elf(ehdr, phdrs)
+
+    def update(self):
+        # Rearrange all segments according to their alignment
+        pos = self.total_header_size()
+        for phdr in sorted(self.phdrs, key=lambda phdr: phdr.p_offset):
+            if phdr.p_offset and phdr.p_filesz:
+                phdr.p_offset = _align(pos, phdr.p_align)
+                pos = phdr.p_offset + phdr.p_filesz
+
+        # Ensure program header count is correct
+        self.ehdr.e_phnum = len(self.phdrs)
+
+        # TODO: Clear out sections for now. Those are not read at the moment.
+        # Also, I don't think the Qualcomm firmware loader has any use for these.
+        self.ehdr.e_shoff = 0
+        self.ehdr.e_shnum = 0
+        self.ehdr.e_shstrndx = 0
+
+    def save_header(self, f: BinaryIO) -> int:
+        pos = self.ehdr.save(f)
+        pos = _pad(f, self.ehdr.e_phoff, pos)
+
+        # Write program headers
+        for phdr in self.phdrs:
+            pos += phdr.save(f, self.ehdr.ei_class)
+
+        return pos
+
+    def save(self, f: BinaryIO) -> int:
+        pos = self.save_header(f)
+
+        # Write segment data
+        for phdr in sorted(self.phdrs, key=lambda phdr: phdr.p_offset):
+            if phdr.data:
+                pos = _pad(f, phdr.p_offset, pos)
+                pos += f.write(phdr.data)
+
+        return pos
diff --git a/tools/qcom/mkmbn/hashseg.py b/tools/qcom/mkmbn/hashseg.py
new file mode 100644
index 0000000000000000000000000000000000000000..f85504655a8c6396951f778a36a3ce384ca6f217
--- /dev/null
+++ b/tools/qcom/mkmbn/hashseg.py
@@ -0,0 +1,308 @@
+# SPDX-License-Identifier: GPL-2.0-only AND BSD-3-Clause
+# Copyright (C) 2021-2023 Stephan Gerhold (GPL-2.0-only)
+# MBN header format adapted from:
+#   - signlk: https://git.linaro.org/landing-teams/working/qualcomm/signlk.git
+#   - coreboot (util/qualcomm/mbn_tools.py)
+# Copyright (c) 2016, 2018, The Linux Foundation. All rights reserved. (BSD-3-Clause)
+# See also:
+#   - https://www.qualcomm.com/media/documents/files/secure-boot-and-image-authentication-technical-overview-v1-0.pdf
+#   - https://www.qualcomm.com/media/documents/files/secure-boot-and-image-authentication-technical-overview-v2-0.pdf
+from __future__ import annotations
+
+import dataclasses
+import hashlib
+from dataclasses import dataclass
+from io import BytesIO
+from struct import Struct
+
+import cert
+import elf
+
+# A typical Qualcomm firmware might have the following program headers:
+#     LOAD off    0x00000800 vaddr 0x86400000 paddr 0x86400000 align 2**11
+#          filesz 0x00001000 memsz 0x00001000 flags rwx
+#
+# The signed version will then look like:
+#     NULL off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**0
+#          filesz 0x000000e8 memsz 0x00000000 flags --- 7000000
+#     NULL off    0x00001000 vaddr 0x86401000 paddr 0x86401000 align 2**12
+#          filesz 0x00000988 memsz 0x00001000 flags --- 2200000
+#     LOAD off    0x00002000 vaddr 0x86400000 paddr 0x86400000 align 2**11
+#          filesz 0x00001000 memsz 0x00001000 flags rwx
+#
+# The second NULL program header with off 0x1000 and filesz 0x988 is the actual
+# "hash table segment" or shortly "hash segment" (see Figure 2 on page 6 in the PDF).
+# It contains the MBN header specified below, then a couple of hashes (e.g. SHA256):
+#   1. Hash of ELF header and program headers
+#   2. Empty hash for hash segment
+#   3. Hashes for data of each memory segment (described by program header)
+# Finally, it contains an RSA signature and the concatenated certificate chain.
+#
+# The first NULL program header is never loaded anywhere, because
+# vaddr = paddr = memsz = 0. However, the "off" and "filesz" cover exactly
+# the ELF header (including all program headers). It is a placeholder so that
+# each hash covers the data of exactly one program header.
+
+PHDR_FLAGS_HDR_PLACEHOLDER = 0x7000000  # placeholder for hash over ELF header
+PHDR_FLAGS_HASH_SEGMENT = 0x2200000  # hash table segment
+
+EXTRA_PHDRS = 2  # header placeholder + hash segment
+
+# Note: None of the alignments seem to be truly required,
+# this could probably be reduced to get smaller file sizes.
+HASH_SEG_ALIGN = 0x1000
+CERT_CHAIN_ALIGN = 16
+
+# According to the v2.0 PDF the metadata is 128 bytes long, but this does not
+# seem to work. All official firmware seems to use 120 bytes instead.
+METADATA_SIZE = 120
+
+
+def _align(i: int, alignment: int) -> int:
+    mask = max(alignment - 1, 0)
+    return (i + mask) & ~mask
+
+
+ at dataclass
+class _HashSegment:
+    image_id: int = 0  # Type of image (unused?)
+    version: int = 0  # Header version number
+
+    hash_size = 0
+    signature_size = 0
+    cert_chain_size = 0
+    total_size = 0
+
+    hashes = []
+    signature = b""
+    cert_chain = b""
+
+    FORMAT = Struct("<10L")
+    Hash = hashlib.sha256
+
+    @property
+    def size_with_header(self):
+        return self.FORMAT.size + self.total_size
+
+    def update(self, dest_addr: int):
+        self.hash_size = len(self.hashes) * self.Hash().digest_size
+        self.signature_size = len(self.signature)
+        self.cert_chain_size = len(self.cert_chain)
+        self.total_size = self.hash_size + self.signature_size + self.cert_chain_size
+
+    def check(self):
+        assert len(self.hashes) * self.Hash().digest_size == self.hash_size
+        assert len(self.signature) == self.signature_size
+        assert len(self.cert_chain) == self.cert_chain_size
+
+    def pack_header(self):
+        self.check()
+        return self.FORMAT.pack(*dataclasses.astuple(self))
+
+    def pack(self):
+        return (
+            self.pack_header()
+            + b"".join(self.hashes)
+            + self.signature
+            + self.cert_chain
+        )
+
+
+ at dataclass
+class HashSegmentV3(_HashSegment):
+    version: int = 3  # Header version number
+
+    flash_addr: int = 0  # Location of image in flash (historical)
+    dest_addr: int = 0  # Physical address of loaded hash segment data
+    total_size: int = 0  # = hash_size + signature_size + cert_chain_size
+    hash_size: int = 0  # Size of hashes for all program segments
+    signature_addr: int = 0  # Physical address of loaded attestation signature
+    signature_size: int = 0  # Size of attestation signature
+    cert_chain_addr: int = 0  # Physical address of loaded certificate chain
+    cert_chain_size: int = 0  # Size of certificate chain
+
+    def update(self, dest_addr: int):
+        super().update(dest_addr)
+        self.dest_addr = dest_addr + self.FORMAT.size
+        self.signature_addr = self.dest_addr + self.hash_size
+        self.cert_chain_addr = self.signature_addr + self.signature_size
+
+
+ at dataclass
+class HashSegmentV5(_HashSegment):
+    version: int = 5  # Header version number
+
+    signature_size_qcom: int = 0  # Size of signature from Qualcomm
+    cert_chain_size_qcom: int = 0  # Size of certificate chain from Qualcomm
+    total_size: int = 0  # = hash_size + signature_size + cert_chain_size
+    hash_size: int = 0  # Size of hashes for all program segments
+    signature_addr: int = 0xFFFFFFFF  # unused?
+    signature_size: int = 0  # Size of attestation signature
+    cert_chain_addr: int = 0xFFFFFFFF  # unused?
+    cert_chain_size: int = 0  # Size of certificate chain
+
+    signature_qcom = b""
+    cert_chain_qcom = b""
+
+    def update(self, dest_addr: int):
+        super().update(dest_addr)
+        self.signature_size_qcom = len(self.signature_qcom)
+        self.cert_chain_size_qcom = len(self.cert_chain_qcom)
+        self.total_size += self.signature_size_qcom + self.cert_chain_size_qcom
+
+    def check(self):
+        super().check()
+        assert len(self.signature_qcom) == self.signature_size_qcom
+        assert len(self.cert_chain_qcom) == self.cert_chain_size_qcom
+
+    def pack(self):
+        return (
+            self.pack_header()
+            + b"".join(self.hashes)
+            + self.signature_qcom
+            + self.cert_chain_qcom
+            + self.signature
+            + self.cert_chain
+        )
+
+
+ at dataclass
+class HashSegmentV6(HashSegmentV5):
+    version: int = 6  # Header version number
+
+    metadata_size_qcom: int = 0  # Size of metadata from Qualcomm
+    metadata_size: int = 0  # Size of metadata
+
+    metadata_qcom = b""
+    metadata = b""
+
+    FORMAT = Struct("<12L")
+    Hash = hashlib.sha384
+
+    def update(self, dest_addr: int):
+        super().update(dest_addr)
+        self.metadata_size_qcom = len(self.metadata_qcom)
+        self.metadata_size = len(self.metadata)
+        self.total_size += self.metadata_size_qcom + self.metadata_size
+
+    def check(self):
+        super().check()
+        assert len(self.metadata_qcom) == self.metadata_size_qcom
+        assert len(self.metadata) == self.metadata_size
+
+    def pack(self):
+        return (
+            self.pack_header()
+            + self.metadata_qcom
+            + self.metadata
+            + b"".join(self.hashes)
+            + self.signature_qcom
+            + self.cert_chain_qcom
+            + self.signature
+            + self.cert_chain
+        )
+
+
+HashSegment = {
+    3: HashSegmentV3,
+    5: HashSegmentV5,
+    6: HashSegmentV6,
+}
+
+
+def drop(elff: elf.Elf):
+    # Drop existing hash segments
+    elff.phdrs = [
+        phdr
+        for phdr in elff.phdrs
+        if phdr.p_type != 0
+        or phdr.p_flags not in [PHDR_FLAGS_HASH_SEGMENT, PHDR_FLAGS_HDR_PLACEHOLDER]
+    ]
+
+
+def generate(elff: elf.Elf, version: int, sw_id: int):
+    drop(elff)
+    assert elff.phdrs, "Need at least one program header"
+
+    hash_seg = HashSegment[version]()
+
+    if version >= 6:
+        # TODO: Figure out metadata format and fill this with useful data
+        hash_seg.metadata = b"\0" * METADATA_SIZE
+
+    # Generate hash for all existing segments with data
+    digest_size = hash_seg.Hash().digest_size
+    hash_seg.hashes = [b"\0" * digest_size] * (len(elff.phdrs) + EXTRA_PHDRS)
+    for i, phdr in enumerate(elff.phdrs, start=EXTRA_PHDRS):
+        if phdr.data:
+            hash_seg.hashes[i] = hash_seg.Hash(phdr.data).digest()
+    total_hashes_size = len(hash_seg.hashes) * digest_size
+
+    # Generate certificate chain with specified OU fields (for < v6)
+    # on >= v6 this is part of the metadata instead
+    ou_fields = []
+    if version < 6:
+        ou_fields = [
+            # Note: The SW_ID is checked by the firmware on some platforms (even if secure boot
+            # is disabled), so it must match the firmware type being signed. Everything else seems
+            # to be mostly ignored when secure boot is off and is just added here to match the
+            # documentation and better mimic the official firmware.
+            "01 %016X SW_ID" % sw_id,
+            "02 %016X HW_ID" % 0,
+            "03 %016X DEBUG" % 2,  # DISABLED
+            "04 %04X OEM_ID" % 0,
+            "05 %08X SW_SIZE" % (hash_seg.FORMAT.size + total_hashes_size),
+            "06 %04X MODEL_ID" % 0,
+            "07 %04X SHA256" % 1,
+        ]
+    hash_seg.cert_chain = cert.generate_chain(ou_fields)
+    hash_seg.cert_chain = hash_seg.cert_chain.ljust(
+        _align(len(hash_seg.cert_chain), CERT_CHAIN_ALIGN), b"\xff"
+    )
+    # hash_seg.cert_chain = b''  # uncomment this to omit the certificate chain in the signed image
+
+    # TODO: Generate actual signature with our generated attestation certificate!
+    # There are different signature schemes that could be implemented (RSASSA-PKCS#1 v1.5
+    # RSASSA-PSS, ECDSA over P-384) but it's not entirely clear yet which chipsets supports/
+    # uses which. The signature does not seem to be checked on devices without secure boot,
+    # so just use a dummy value for now.
+    hash_seg.signature = b"\xff" * (cert.ATT_KEY.key_size // 8)
+    # hash_seg.signature = b''  # uncomment this to omit the signature in the signed image
+
+    # Align maximum end address to get address for hash table header, then update header
+    hash_addr = _align(
+        max(phdr.p_paddr + phdr.p_memsz for phdr in elff.phdrs), HASH_SEG_ALIGN
+    )
+    hash_seg.update(hash_addr)
+    # print(hash_seg)
+
+    # Insert new hash NULL segment
+    hash_phdr = elf.Phdr(
+        0,
+        HASH_SEG_ALIGN,
+        hash_addr,
+        hash_addr,
+        hash_seg.size_with_header,
+        _align(hash_seg.size_with_header, HASH_SEG_ALIGN),
+        PHDR_FLAGS_HASH_SEGMENT,
+        HASH_SEG_ALIGN,
+    )
+    elff.phdrs.insert(0, hash_phdr)
+
+    # Insert new ELF header placeholder program header
+    hdr_hash_phdr = elf.Phdr(0, 0, 0, 0, 0, 0, PHDR_FLAGS_HDR_PLACEHOLDER, 0)
+    elff.phdrs.insert(0, hdr_hash_phdr)
+
+    # Now determine size of ELF header (including program headers)
+    hdr_hash_phdr.p_filesz = elff.total_header_size()
+
+    # Recompute attributes to match final output (e.g. adjust e_phnum)
+    elff.update()
+
+    # Compute the hash for the ELF header
+    with BytesIO() as hdr_io:
+        elff.save_header(hdr_io)
+        hash_seg.hashes[0] = hash_seg.Hash(hdr_io.getbuffer()).digest()
+
+    # And finally, assemble the hash segment
+    hash_phdr.data = hash_seg.pack()
diff --git a/tools/qcom/mkmbn/mkmbn.py b/tools/qcom/mkmbn/mkmbn.py
new file mode 100755
index 0000000000000000000000000000000000000000..94d85275c19c01c1e0aa64a61b615d14c10c0468
--- /dev/null
+++ b/tools/qcom/mkmbn/mkmbn.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (C) 2024 Stephan Gerhold
+# Copyright (C) 2025 Casey Connolly
+from __future__ import annotations
+
+import argparse
+from pathlib import Path
+
+from elf import Elf, Phdr
+import hashseg
+import sys
+from enum import Enum
+
+verbose = False
+
+def log(*args, **kwargs):
+    if verbose:
+        print(args, kwargs, file=sys.stderr)
+
+def error(*args, **kwargs):
+    print("mkmbn: ", file=sys.stderr, end='')
+    print(*args, *kwargs, file=sys.stderr)
+
+
+
+class SwId(Enum):
+    sbl1 = 0x00
+    mba = 0x01
+    modem = 0x02
+    prog = 0x03
+    adsp = 0x04
+    devcfg = 0x05
+    tz = 0x07
+    aboot = 0x09
+    rpm = 0x0A
+    tz_app = 0x0C
+    wcnss = 0x0D
+    venus = 0x0E
+    wlanmdsp = 0x12
+    gpu = 0x14
+    hyp = 0x15
+    cdsp = 0x17
+    slpi = 0x18
+    abl = 0x1C
+    cmnlib = 0x1F
+    aop = 0x21
+    qup = 0x24
+    xbl_config = 0x25
+
+class MbnData:
+
+    # sw_id 0x9 is aboot/uefi, the most common
+    def __init__(self, loadaddr: int, version: int, sw_id: SwId = SwId.aboot):
+        self.loadaddr = loadaddr
+        self.version = version
+        self.sw_id = sw_id
+
+
+"""
+This dictionary is used to map a board or platform to the appropriate load address and
+other MBN metadata. When adding support for a new platform to U-Boot, the appropriate
+data should be filled out here. The load address can typically be determined by looking
+at the uefi.elf or xbl.elf for the platform. For the uefi.elf it is the load address, and
+for xbl.elf it is typically the RWX section in the middle, just BEFORE the section loaded
+at 0x1495xxxx or similar. Looking at similar platforms in the table below may help.
+"""
+boards: dict[bytes, MbnData] = {
+    # Exact matches for boards, these are preferred
+    b"qcom,qcs6490-rb3gen2\0": MbnData(0x9FC00000, 6, SwId.aboot),
+    b"qcom,qcs9100-ride-r3\0": MbnData(0xAF000000, 6, SwId.aboot),  # Dragonwing IQ9
+    b"qcom,qcs8300-ride\0": MbnData(0xAF000000, 6, SwId.aboot),  # Dragonwing IQ8
+    b"qcom,qcs615-ride\0": MbnData(0x9FC00000, 6, SwId.aboot),  # Dragonwing IQ6
+    # Fallback/generic matches since most boards for a platform will
+    # use the same load address
+    b"qcom,qcm6490\0": MbnData(0x9FC00000, 6, SwId.aboot),  # rb3gen2, rubikpi3
+    b"qcom,qcs9100\0": MbnData(0xAF000000, 6, SwId.aboot),  # Dragonwing IQ9
+    b"qcom,qcs8300\0": MbnData(0xAF000000, 6, SwId.aboot),  # Dragonwing IQ8
+    b"qcom,qcs615\0": MbnData(0x9FC00000, 6, SwId.aboot),  # Dragonwing IQ6
+    b"qcom,ipq9574\0": MbnData(0x4A240000, 6, SwId.aboot),
+
+    # msm8916/apq8016 has an "aboot" partition but the process is the same
+    # They use header version 3.
+    b"qcom,apq8016\0": MbnData(0x8f600000, 3, SwId.aboot),
+    b"qcom,msm8916\0": MbnData(0x8f600000, 3, SwId.aboot),
+}
+
+parser = argparse.ArgumentParser(
+    description="""
+	Create a signed Qualcomm "uefi" ELF image
+"""
+)
+parser.register("type", "hex", lambda s: int(s, 16))
+parser.add_argument(
+    "-o", "--output", type=Path, default="u-boot.mbn", help="Output file"
+)
+parser.add_argument(
+    "-v", dest="verbose", action="store_true", default=False, help="Verbose"
+)
+parser.add_argument(
+    "bin", type=argparse.FileType("rb"), help="Binary to embed (e.g. u-boot.bin)"
+)
+args = parser.parse_args()
+
+elf = Elf()
+
+data: bytes = args.bin.read()
+
+# dtb is at the end, so find the last match
+dtb_off = 0
+off = 0
+while True:
+    off = data.find(b"\xd0\x0d\xfe\xed", dtb_off + 1)
+    if off == -1:
+        break
+    dtb_off = off
+
+if not dtb_off:
+    print("Couldn't find DTB in provided binary!")
+    exit(1)
+
+log(f"Found FDT at {dtb_off:#x}")
+
+mbn: MbnData|None = None
+
+for match, mbndata in boards.items():
+    if data.find(match, dtb_off) != -1:
+        mbn = mbndata
+        break
+
+if not mbn:
+    error(
+        "Not building an MBN file for this board, see tools/qcom/mkmbn/mkmbn.py for details"
+    )
+    # Bailing out would fail the build, and it's not possible to know if an MBN
+    # is actually needed for the board we're building for. Minimise confusion by removing
+    # any file that might exist from a previous build and exit with a known code.
+    args.output.unlink(missing_ok=True)
+    exit(61)
+
+log(f"Detected board {match.decode('UTF-8')} with load address {mbn.loadaddr:#x}")
+
+elf.phdrs.append(Phdr.from_bin(data, mbn.loadaddr))
+elf.ehdr.e_entry = mbn.loadaddr
+elf.update()
+
+# QLI boards use v6 sw_id is "aboot"
+hashseg.generate(elf, mbn.version, mbn.sw_id.value)
+# print(f"after: {elf}")
+
+with open(args.output, "wb") as f:
+    elf.save(f)
+
+log(f"Built signed MBN: {args.output.resolve()}")

-- 
2.49.0



More information about the U-Boot mailing list