Hi Sughosh, On Sat, 5 Aug 2023 at 05:35, Sughosh Ganu <sughosh.g...@linaro.org> wrote: > > Add support in binman for generating EFI capsules. The capsule > parameters can be specified through the capsule binman entry. Also add > test cases in binman for testing capsule generation. > > Signed-off-by: Sughosh Ganu <sughosh.g...@linaro.org> > --- > Changes since V6: > * Add macros for the GUID strings in sandbox_efi_capsule.h > * Highlight that the private and public keys are mandatory for capsule > signing. > * Add a URL link to the UEFI spec, as used in the rst files. > * Use local vars for private and public keys in BuildSectionData() > * Use local vars for input payload and capsule filenames in > BuildSectionData(). > * Drop the ProcessContents() and SetImagePos() as the superclass > functions suffice. > * Use GUID macro names in the capsule test dts files. > * Rename efi_capsule_payload.bin to capsule_input.bin. > > > include/sandbox_efi_capsule.h | 14 ++
Please move this file to a later patch - see below. Could we have a single header file with all the GUIDs, i.e. sandbox, ARM, etc. > tools/binman/entries.rst | 62 ++++++++ > tools/binman/etype/efi_capsule.py | 143 ++++++++++++++++++ > tools/binman/ftest.py | 121 +++++++++++++++ > tools/binman/test/307_capsule.dts | 23 +++ > tools/binman/test/308_capsule_signed.dts | 25 +++ > tools/binman/test/309_capsule_version.dts | 24 +++ > tools/binman/test/310_capsule_signed_ver.dts | 26 ++++ > tools/binman/test/311_capsule_oemflags.dts | 24 +++ > tools/binman/test/312_capsule_missing_key.dts | 24 +++ > .../binman/test/313_capsule_missing_index.dts | 22 +++ > .../binman/test/314_capsule_missing_guid.dts | 19 +++ > .../test/315_capsule_missing_payload.dts | 19 +++ > 13 files changed, 546 insertions(+) > create mode 100644 include/sandbox_efi_capsule.h > create mode 100644 tools/binman/etype/efi_capsule.py > create mode 100644 tools/binman/test/307_capsule.dts > create mode 100644 tools/binman/test/308_capsule_signed.dts > create mode 100644 tools/binman/test/309_capsule_version.dts > create mode 100644 tools/binman/test/310_capsule_signed_ver.dts > create mode 100644 tools/binman/test/311_capsule_oemflags.dts > create mode 100644 tools/binman/test/312_capsule_missing_key.dts > create mode 100644 tools/binman/test/313_capsule_missing_index.dts > create mode 100644 tools/binman/test/314_capsule_missing_guid.dts > create mode 100644 tools/binman/test/315_capsule_missing_payload.dts > > diff --git a/include/sandbox_efi_capsule.h b/include/sandbox_efi_capsule.h > new file mode 100644 > index 0000000000..da71b87a18 > --- /dev/null > +++ b/include/sandbox_efi_capsule.h > @@ -0,0 +1,14 @@ > +// SPDX-License-Identifier: GPL-2.0+ > +/* > + * Copyright (c) 2023, Linaro Limited > + */ > + > +#if !defined(_SANDBOX_EFI_CAPSULE_H_) > +#define _SANDBOX_EFI_CAPSULE_H_ > + > +#define SANDBOX_UBOOT_IMAGE_GUID "09d7cf52-0720-4710-91d1-08469b7fe9c8" > +#define SANDBOX_UBOOT_ENV_IMAGE_GUID "5a7021f5-fef2-48b4-aaba-832e777418c0" > +#define SANDBOX_FIT_IMAGE_GUID "3673b45d-6a7c-46f3-9e60-adabb03f7937" > +#define SANDBOX_INCORRECT_GUID "058b7d83-50d5-4c47-a195-60d86ad341c4" These should not be needed in binman tests. > + > +#endif /* _SANDBOX_EFI_CAPSULE_H_ */ > diff --git a/tools/binman/entries.rst b/tools/binman/entries.rst > index f2376932be..fc094b9c08 100644 > --- a/tools/binman/entries.rst > +++ b/tools/binman/entries.rst > @@ -468,6 +468,68 @@ updating the EC on startup via software sync. > > > > +.. _etype_efi_capsule: > + > +Entry: capsule: Entry for generating EFI Capsule files > +------------------------------------------------------ > + > +The parameters needed for generation of the capsules can > +be provided as properties in the entry. > + > +Properties / Entry arguments: > + - image-index: Unique number for identifying corresponding > + payload image. Number between 1 and descriptor count, i.e. > + the total number of firmware images that can be updated. > + - image-type-id: Image GUID which will be used for identifying the > + updatable image on the board. > + - hardware-instance: Optional number for identifying unique > + hardware instance of a device in the system. Default value of 0 > + for images where value is not to be used. > + - fw-version: Optional value of image version that can be put on > + the capsule through the Firmware Management Protocol(FMP) header. > + - monotonic-count: Count used when signing an image. > + - private-key: Path to PEM formatted .key private key file. This property > + is mandatory for generating signed capsules. > + - public-key-cert: Path to PEM formatted .crt public key certificate > + file. This property is mandatory for generating signed capsules. > + - oem-flags - Optional OEM flags to be passed through capsule header. > + > + Since this is a subclass of Entry_section, all properties of the parent > + class also apply here. > + > +For more details on the description of the capsule format, and the capsule > +update functionality, refer Section 8.5 and Chapter 23 in the `UEFI > +specification`_. > + > +The capsule parameters like image index and image GUID are passed as > +properties in the entry. The payload to be used in the capsule is to be > +provided as a subnode of the capsule entry. > + > +A typical capsule entry node would then look something like this:: > + > + capsule { > + type = "efi-capsule"; > + image-index = <0x1>; > + /* Image GUID for testing capsule update */ > + image-type-id = SANDBOX_UBOOT_IMAGE_GUID; > + hardware-instance = <0x0>; > + private-key = "path/to/the/private/key"; > + public-key-cert = "path/to/the/public-key-cert"; > + oem-flags = <0x8000>; > + > + u-boot { > + }; > + }; > + > +In the above example, the capsule payload is the U-Boot image. The > +capsule entry would read the contents of the payload and put them > +into the capsule. Any external file can also be specified as the > +payload using the blob-ext subnode. > + > +.. _`UEFI specification`: > https://uefi.org/sites/default/files/resources/UEFI_Spec_2_10_Aug29.pdf > + > + > + > .. _etype_encrypted: > > Entry: encrypted: Externally built encrypted binary blob > diff --git a/tools/binman/etype/efi_capsule.py > b/tools/binman/etype/efi_capsule.py > new file mode 100644 > index 0000000000..bfdca94e26 > --- /dev/null > +++ b/tools/binman/etype/efi_capsule.py > @@ -0,0 +1,143 @@ > +# SPDX-License-Identifier: GPL-2.0+ > +# Copyright (c) 2023 Linaro Limited > +# > +# Entry-type module for producing a EFI capsule > +# > + > +import os > + > +from binman.entry import Entry > +from binman.etype.section import Entry_section > +from dtoc import fdt_util > +from u_boot_pylib import tools > + > +class Entry_efi_capsule(Entry_section): > + """Generate EFI capsules > + > + The parameters needed for generation of the capsules can > + be provided as properties in the entry. > + > + Properties / Entry arguments: > + - image-index: Unique number for identifying corresponding > + payload image. Number between 1 and descriptor count, i.e. > + the total number of firmware images that can be updated. > + - image-type-id: Image GUID which will be used for identifying the > + updatable image on the board. > + - hardware-instance: Optional number for identifying unique > + hardware instance of a device in the system. Default value of 0 > + for images where value is not to be used. > + - fw-version: Optional value of image version that can be put on > + the capsule through the Firmware Management Protocol(FMP) header. > + - monotonic-count: Count used when signing an image. > + - private-key: Path to PEM formatted .key private key file. This property > + is mandatory for generating signed capsules. > + - public-key-cert: Path to PEM formatted .crt public key certificate > + file. This property is mandatory for generating signed capsules. > + - oem-flags - Optional OEM flags to be passed through capsule header. > + > + Since this is a subclass of Entry_section, all properties of the parent > + class also apply here. > + > + For more details on the description of the capsule format, and the > capsule > + update functionality, refer Section 8.5 and Chapter 23 in the `UEFI > + specification`_. > + > + The capsule parameters like image index and image GUID are passed as > + properties in the entry. The payload to be used in the capsule is to be > + provided as a subnode of the capsule entry. > + > + A typical capsule entry node would then look something like this > + > + capsule { > + type = "efi-capsule"; > + image-index = <0x1>; > + /* Image GUID for testing capsule update */ > + image-type-id = SANDBOX_UBOOT_IMAGE_GUID; > + hardware-instance = <0x0>; > + private-key = "path/to/the/private/key"; > + public-key-cert = "path/to/the/public-key-cert"; > + oem-flags = <0x8000>; > + > + u-boot { > + }; > + }; > + > + In the above example, the capsule payload is the U-Boot image. The > + capsule entry would read the contents of the payload and put them > + into the capsule. Any external file can also be specified as the > + payload using the blob-ext subnode. > + > + .. _`UEFI specification`: > https://uefi.org/sites/default/files/resources/UEFI_Spec_2_10_Aug29.pdf > + """ > + def __init__(self, section, etype, node): > + super().__init__(section, etype, node) > + self.required_props = ['image-index', 'image-type-id'] > + self.image_index = 0 > + self.image_guid = '' > + self.hardware_instance = 0 > + self.monotonic_count = 0 > + self.fw_version = 0 > + self.oem_flags = 0 > + self.private_key = '' > + self.public_key_cert = '' > + self.auth = 0 > + > + def ReadNode(self): > + self.ReadEntries() > + super().ReadNode() I believe those two lines should be swapped. > + > + self.image_index = fdt_util.GetInt(self._node, 'image-index') > + self.image_guid = fdt_util.GetString(self._node, 'image-type-id') > + self.fw_version = fdt_util.GetInt(self._node, 'fw-version') > + self.hardware_instance = fdt_util.GetInt(self._node, > 'hardware-instance') > + self.monotonic_count = fdt_util.GetInt(self._node, 'monotonic-count') > + self.oem_flags = fdt_util.GetInt(self._node, 'oem-flags') > + > + self.private_key = fdt_util.GetString(self._node, 'private-key') > + self.public_key_cert = fdt_util.GetString(self._node, > 'public-key-cert') > + if ((self.private_key and not self.public_key_cert) or > (self.public_key_cert and not self.private_key)): > + self.Raise('Both private key and public key certificate need to > be provided') > + elif not (self.private_key and self.public_key_cert): > + self.auth = 0 > + else: > + self.auth = 1 > + > + def ReadEntries(self): > + """Read the subnode to get the payload for this capsule""" > + # We can have a single payload per capsule > + if len(self._node.subnodes) == 0: > + self.Raise('The capsule entry expects at least one subnode for > payload') Still need to drop this > + > + for node in self._node.subnodes: > + entry = Entry.Create(self, node) > + entry.ReadNode() > + self._entries[entry.name] = entry I think you can drop this method, since it should be the same as entry_Sectoin ? > + > + def BuildSectionData(self, required): > + private_key = '' > + public_key_cert = '' > + if self.auth: > + if not os.path.isabs(self.private_key): > + private_key = tools.get_input_filename(self.private_key) > + if not os.path.isabs(self.public_key_cert): > + public_key_cert = > tools.get_input_filename(self.public_key_cert) > + data, payload, _ = self.collect_contents_to_file( s/_/uniq/ since you need this below > + self._entries.values(), 'capsule_payload') 'capsule_in' is better as it is an input to this entry type > + outfile = self._filename if self._filename else self._node.name You need a unique filename so cannot use self._node.name since it is not guaranteed to be unique. See how mkimage does this, using uniq > + capsule_fname = tools.get_output_filename(outfile) > + ret = self.mkeficapsule.generate_capsule(self.image_index, > + self.image_guid, > + self.hardware_instance, > + payload, > + capsule_fname, > + private_key, > + public_key_cert, > + self.monotonic_count, > + self.fw_version, > + self.oem_flags) > + if ret is not None: > + os.remove(payload) > + return tools.read_file(capsule_fname) > + > + def AddBintools(self, btools): > + self.mkeficapsule = self.AddBintool(btools, 'mkeficapsule') > diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py > index 36428ec343..9bda0157f7 100644 > --- a/tools/binman/ftest.py > +++ b/tools/binman/ftest.py > @@ -48,6 +48,7 @@ U_BOOT_VPL_DATA = b'vpl76543210fedcbazywxyz_' > BLOB_DATA = b'89' > ME_DATA = b'0abcd' > VGA_DATA = b'vga' > +EFI_CAPSULE_DATA = b'efi' > U_BOOT_DTB_DATA = b'udtb' > U_BOOT_SPL_DTB_DATA = b'spldtb' > U_BOOT_TPL_DTB_DATA = b'tpldtb' > @@ -119,6 +120,11 @@ COMP_BINTOOLS = ['bzip2', 'gzip', 'lz4', 'lzma_alone', > 'lzop', 'xz', 'zstd'] > > TEE_ADDR = 0x5678 > > +# Firmware Management Protocol(FMP) GUID > +FW_MGMT_GUID = 'edd5cb6d2de8444cbda17194199ad92a' > +# Image GUID specified in the DTS > +CAPSULE_IMAGE_GUID = '52cfd7092007104791d108469b7fe9c8' > + > class TestFunctional(unittest.TestCase): > """Functional tests for binman > > @@ -215,6 +221,7 @@ class TestFunctional(unittest.TestCase): > TestFunctional._MakeInputFile('scp.bin', SCP_DATA) > TestFunctional._MakeInputFile('rockchip-tpl.bin', ROCKCHIP_TPL_DATA) > TestFunctional._MakeInputFile('ti_unsecure.bin', TI_UNSECURE_DATA) > + TestFunctional._MakeInputFile('capsule_input.bin', EFI_CAPSULE_DATA) > > # Add a few .dtb files for testing > TestFunctional._MakeInputFile('%s/test-fdt1.dtb' % TEST_FDT_SUBDIR, > @@ -7139,5 +7146,119 @@ fdt fdtmap Extract the > devicetree blob from the fdtmap > self.assertEqual(fdt_util.GetString(key_node, "key-name-hint"), > "key") > > + def _CheckCapsule(self, data, signed_capsule=False, version_check=False, > + capoemflags=False): > + fmp_signature = "4d535331" # 'M', 'S', 'S', '1' > + fmp_size = "10" > + fmp_fw_version = "02" > + oemflag = "0080" > + > + payload_data = EFI_CAPSULE_DATA > + > + # Firmware Management Protocol(FMP) GUID - offset(0 - 32) > + self.assertEqual(FW_MGMT_GUID, data.hex()[:32]) > + # Image GUID - offset(96 - 128) > + self.assertEqual(CAPSULE_IMAGE_GUID, data.hex()[96:128]) > + As discussed please add a TODO here, so it is clear that you are planning to write a decoder tool like dump_image. > + if capoemflags: > + # OEM Flags - offset(40 - 44) > + self.assertEqual(oemflag, data.hex()[40:44]) > + if signed_capsule and version_check: > + # FMP header signature - offset(4770 - 4778) > + self.assertEqual(fmp_signature, data.hex()[4770:4778]) > + # FMP header size - offset(4778 - 4780) > + self.assertEqual(fmp_size, data.hex()[4778:4780]) > + # firmware version - offset(4786 - 4788) > + self.assertEqual(fmp_fw_version, data.hex()[4786:4788]) > + # payload offset signed capsule(4802 - 4808) > + self.assertEqual(payload_data.hex(), data.hex()[4802:4808]) > + elif signed_capsule: > + # payload offset signed capsule(4770 - 4776) > + self.assertEqual(payload_data.hex(), data.hex()[4770:4776]) > + elif version_check: > + # FMP header signature - offset(184 - 192) > + self.assertEqual(fmp_signature, data.hex()[184:192]) > + # FMP header size - offset(192 - 194) > + self.assertEqual(fmp_size, data.hex()[192:194]) > + # firmware version - offset(200 - 202) > + self.assertEqual(fmp_fw_version, data.hex()[200:202]) > + # payload offset for non-signed capsule with version header(216 > - 222) > + self.assertEqual(payload_data.hex(), data.hex()[216:222]) > + else: > + # payload offset for non-signed capsule with no version > header(184 - 190) > + self.assertEqual(payload_data.hex(), data.hex()[184:190]) > + > + def testCapsuleGen(self): > + """Test generation of EFI capsule""" > + data = self._DoReadFile('307_capsule.dts') > + > + self._CheckCapsule(data) > + > + def testSignedCapsuleGen(self): > + """Test generation of EFI capsule""" > + data = tools.read_file(self.TestFile("key.key")) > + self._MakeInputFile("key.key", data) > + data = tools.read_file(self.TestFile("key.pem")) > + self._MakeInputFile("key.crt", data) > + > + data = self._DoReadFile('308_capsule_signed.dts') > + > + self._CheckCapsule(data, signed_capsule=True) > + > + def testCapsuleGenVersionSupport(self): > + """Test generation of EFI capsule with version support""" > + data = self._DoReadFile('309_capsule_version.dts') > + > + self._CheckCapsule(data, version_check=True) > + > + def testCapsuleGenSignedVer(self): > + """Test generation of signed EFI capsule with version information""" > + data = tools.read_file(self.TestFile("key.key")) > + self._MakeInputFile("key.key", data) > + data = tools.read_file(self.TestFile("key.pem")) > + self._MakeInputFile("key.crt", data) > + > + data = self._DoReadFile('310_capsule_signed_ver.dts') > + > + self._CheckCapsule(data, signed_capsule=True, version_check=True) > + > + def testCapsuleGenCapOemFlags(self): > + """Test generation of EFI capsule with OEM Flags set""" > + data = self._DoReadFile('311_capsule_oemflags.dts') > + > + self._CheckCapsule(data, capoemflags=True) > + > + def testCapsuleGenKeyMissing(self): > + """Test that binman errors out on missing key""" > + with self.assertRaises(ValueError) as e: > + self._DoReadFile('312_capsule_missing_key.dts') > + > + self.assertIn("Both private key and public key certificate need to > be provided", > + str(e.exception)) > + > + def testCapsuleGenIndexMissing(self): > + """Test that binman errors out on missing image index""" > + with self.assertRaises(ValueError) as e: > + self._DoReadFile('313_capsule_missing_index.dts') > + > + self.assertIn("entry is missing properties: image-index", > + str(e.exception)) > + > + def testCapsuleGenGuidMissing(self): > + """Test that binman errors out on missing image GUID""" > + with self.assertRaises(ValueError) as e: > + self._DoReadFile('314_capsule_missing_guid.dts') > + > + self.assertIn("entry is missing properties: image-type-id", > + str(e.exception)) > + > + def testCapsuleGenPayloadMissing(self): > + """Test that binman errors out on missing input(payload)image""" > + with self.assertRaises(ValueError) as e: > + self._DoReadFile('315_capsule_missing_payload.dts') > + > + self.assertIn("The capsule entry expects at least one subnode for > payload", > + str(e.exception)) > + > if __name__ == "__main__": > unittest.main() > diff --git a/tools/binman/test/307_capsule.dts > b/tools/binman/test/307_capsule.dts > new file mode 100644 > index 0000000000..86552ff83f > --- /dev/null > +++ b/tools/binman/test/307_capsule.dts > @@ -0,0 +1,23 @@ > +// SPDX-License-Identifier: GPL-2.0+ > + > +/dts-v1/; > + > +#include <sandbox_efi_capsule.h> We can't include sandbox files in binman! I Does it actually matter what the GUID value is? Can they all be the same? For now I think it is best to have #define BINMAN_TEST_GUID "xxx" repeated at the top of each .dts file. Hopefully at some point we can figure out a sensible way to provide a decoder ring for this mess, so we can use actually names in the binman definition instead of GUIDs. > + > +/ { > + #address-cells = <1>; > + #size-cells = <1>; > + > + binman { > + efi-capsule { > + image-index = <0x1>; > + /* Image GUID for testing capsule update */ > + image-type-id = SANDBOX_UBOOT_IMAGE_GUID; > + hardware-instance = <0x0>; > + > + blob { > + filename = "capsule_input.bin"; > + }; > + }; > + }; > +}; Regards, Simon