LAN969x Secure and Verified Boot
1. Introduction
This section describes the methods to control which firmware is allowed to execute on a LAN969X-based platform.
First and foremost, LAN969X supports Secure Boot by using Trusted Firmware for ARM (TFA), which implements authenticated boot, allowing only software explicitly signed for use on the system.
This will control all elements executed up to and including the final Non-Secure boot stage, the so-called BL33. (See Booting LAN969X for more details on boot stages).
BL33 is normally implemented by U-Boot, but TFA can also boot other software entities such as Linux or RTOS-based applications. This is controlled during preparation of boot media images.
After BL33 is authenticated and started, it is the responsibility of the BL33 to protect the Non-Secure realm against execution of any non-authenticated Non-Secure software. Note that the Secure World is inherently protected by hardware mechanisms, and by definition the Non-Secure World needs no special precautions for maintaining the security of the Secure World. But by ensuring that no undesired (Non-Secure) code is allowed, the total attack surface is further reduced.
When the BL33 used supports starting other firmware entities it will be desirable to authenticate those firmware entities. Methods to do so is presented for U-Boot and Linux.
1.1. Secure Boot using Trusted Firmware for ARM
Secure Boot is an integral feature of of TFA. Refer to LAN969x Secure Boot for information of how to produce (signed) boot media and securing the platform with the ROT (root of trust) in the OTP.
1.2. Verified boot in U-Boot
Using U-Boot as BL33 is inherently insecure. More measures than described here are required to achieve other than a casual level of security. If you are serious about security you should be using either (secured, hardened) Linux or an RTOS-based embedded application. |
This section describes how to enable the verified boot in u-boot and
how the fit images need to be signed to be able to boot
them. Although U-Boot may support other image types, only fit
images can be signed.
1.2.1. Generate keys
First it is required to generate a private and public key which will be used to sign the fit images:
openssl genrsa -F4 -out /tmp/keys/dev.key 2048 openssl req -batch -new -x509 -key /tmp/keys/dev.key -out /tmp/keys/dev.crt
1.2.2. Configure U-boot
After this it is required to enable these configuration options in u-boot:
-
CONFIG_FIT_SIGNATURE=y
-
CONFIG_DEVICE_TREE_INCLUDES="lan969x_signed.dtsi"
Next step is to get the public key inside the dtbs. The key is to be
used when authenticating any fit image. Since it is a public key, it
is not required to be secured/secret, and it is included directly in
the U-Boot binary image by the u-boot.dtsi
device tree included in
U-Boot.
It is required to get the public key into a format that the
u-boot.dtsi
file can understand. This is possible with the following
tool:
And run the command like this
./ubpubkey.py /tmp/keys/dev.crt
The output will be printed on the screen:
rsa,exponent = <0x00 0x10001>; rsa,modulus = <0xcfbfd0fa 0x1dd472f3 0xaa6bfa57 0xa5e8fba8 0x617a0f2a 0x4554cde 0x3569ba8b 0xbb948b19 0x39ad4777 0x80c42017 0xe0568d97 0x2a80a1fc 0x299dab32 0x3c1e78f2 0x837a24ce 0x3f0b7394 0x8739f31d 0xcaba9ead 0x49917502 0x9443ddfb 0x97d8de50 0xf9c1a873 0x24b8312f 0x5d0d97ba 0x9d92b21f 0x44120622 0x228dfc54 0x3bef2d6f 0x1fb391c6 0x782e1b93 0x7677a18b 0x2678af0f 0x138df5bd 0x64b80087 0x6ea046e7 0xc9922bd2 0xb928c077 0xcda29d2e 0xa5713525 0x759465b5 0x852577bd 0xc40a9233 0x516f9f6e 0x45653ed1 0x1f62bd9 0x478b7ac9 0xddff297c 0x438501e3 0xbd3e4a02 0x4f851b57 0x81842570 0x507ce1ce 0x3af6ff00 0xc0519a5b 0x655ab6ae 0x4a073221 0xfc719ac8 0xf2360513 0x60c57a5c 0x2c9578f7 0x5b9ca51a 0xf46b67a8 0x21dbfdf4 0x46f0c511>; rsa,n0-inverse = <0xe0d340f>; rsa,num-bits = <0x800>; rsa,r-squared = <0x8c7b3c00 0x2808e8b6 0x25b3e495 0x88cb17a0 0x8dc80f3b 0x2931470f 0x6186d1c5 0xf300592e 0x9e1706ae 0xf18bcfb7 0x3c1f223a 0xe3a8aca3 0xdd5d14d6 0x4dd59f67 0xbce4fb1 0x3bebe47 0xdc716260 0x3b5bc3ec 0x4891b7c7 0x88cb6aad 0xc5ad6144 0x8253b5be 0x26ea6578 0x7bcabc6e 0xa9c391c9 0x360be02e 0x64574b32 0x7445fa80 0x433ee11a 0xf5cd7ec7 0x6472c089 0xa6d0769c 0xde0987a5 0xc32a3b1f 0xaece0ae3 0x4951ede8 0x9df3b025 0x7326a93d 0x4c0e31c1 0x7c57d24a 0x47fa4053 0xf10ba5e4 0xd0655b 0xa6c5a3ae 0x2c653e10 0xae6590a6 0xf6d5f56 0xfab661d9 0xfa253682 0x3baa5bd1 0x1d1e53f2 0xd40efada 0x789c912c 0x52a5adba 0x4c1cb1cc 0x4ad22c1a 0x935893f0 0xadbcad80 0x112d184f 0x90eda7af 0xf9a7c5e7 0x3d2f192b 0x9039c371 0x16dce8ff>; k-gen = "ubpubkey/1.0a/1707912564"; k-sha256-fp = "6b84d0da71c8fe705ad46a9c76ce23211f2c5ac835e8823f62db13be5b676e62"; k-src = "cert/dev.crt"; Done
The it is required to take the following fields:
-
rsa,exponent
-
rsa,modulus
-
rsa,n0-inverse
-
rsa,num-bits
-
rsa,r-squared
And replace the existing one inside the u-boot file 'lan969x_signed.dtsi':
signature { key-dev { required = "conf"; key-name-hint = "dev"; algo = "sha1,rsa2048"; rsa,exponent = <0x00 0x10001>; rsa,modulus = <0xcfbfd0fa 0x1dd472f3 0xaa6bfa57 ...>; rsa,n0-inverse = <0xe0d340f>; rsa,num-bits = <0x800>; rsa,r-squared = <0x8c7b3c00 0x2808e8b6 0x25b3e495 ...>; }; };
After this, it is required to compile u-boot.
The values in this snippet are just for demo purpose, don’t use them in production. |
The BSP contains a pre-compiled U-Boot with sample demonstration keys. The pre-compiled binary version should not be used directly in a production environment, rather build your own with your own, private key. |
1.2.3. How to sign FIT images with private key
Inside the its
file that is used to generate the itb
file using
mkimage, it is required to add a signature-1
node under the
configuration which will be signed with the private key and a hash-1
for each of the images that will be signed.
For example:
images { kernel { ... hash-1 { algo = "sha1"; }; }; ... }; configurations { lan9698_ev23x71a_0_at_lan969x { description = "lan9698 ev23x71a"; kernel = "kernel"; ramdisk = "ramdisk"; fdt = "fdt_lan9698_ev23x71a_0_at_lan969x"; signature-1 { algo = "sha1,rsa2048"; key-name-hint = "dev"; sign-images = "fdt", "kernel", "ramdisk"; }; hash-1 { algo = "sha1"; }; }; };
Then simply running mkimage command like this:
mkimage -f vmlinux.its -k /tmp/keys/ fit.itb
— will sign the FIT image with the key in the given directory.
1.3. Verified boot in Linux
Using Linux in a secure configuration is outside the scope of this document. Secure Linux is a vast topic, and this section only propose a possible, partial method. |
As Linux can be started by both TFA and U-Boot, this method applies to both using a Linux (fit) image as BL33 and to using U-Boot as BL33.
The Linux kernel itself should be authenticated by the previous boot stage - either TFA or U-Boot depending on which BL33 is used. This was described in the previous sections.
But as Linux (typically) supports starting separate firmware entities from a file system, such as Linux kernel modules and/or user-mode applications, it may be required to control the origin of these entities.
If your Linux image FIT contain an initial root file system (rootfs), then it should already been authenticated by the prior boot stage. But if it is mounting a file system from a NOR or eMMC partition, then it is necessary to ensure that the data in the file system is not tampered with or entirely overwritten with a rogue file system image.
The following describes how to enable file system integrity checking.
1.3.1. File system integrity
File system integrity cannot stand alone. It can easily be circumvented if the Linux system allows either root-level command line access, using U-Boot or if the base system (TFA) is not properly tied to a ROT in the OTP. |
Several methods for protecting a file system is available under
Linux. The method presented here is using a tool called dm-verity
.
Detailed info can be seen here: dm-verity introduction.
Dm-verity works by creating a so-called Merkle Tree data structure, which is a hierarchical set of hashes, the bottom-most leaves representing a hash of the individual data blocks in the raw file system its supposed to protect. The top node is the root hash and is used to identify the total sum of all hashes.
This hash data is contained in a separate disk partition, alongside the (unchanged) root file system.
When the Linux system mounts the file system, it should be given two extra parameters:
-
The Root Hash
-
The partition name where the hash data is kept
When creating the file system, it is required to use a block size of
4k
(4096). You can use other file systems than ext4
, and it makes
good sense to use read-only file systems.
When you have the file system image ready (in a file of appropriate size), then you can create the hash data.
The command to generate the hash data file is (input file
rootfs.squashfs
, hash algorithm sha256
):
$ veritysetup format -v --debug \ --hash=sha256 --root-hash-file roothash.txt rootfs.squashfs rootfs.hash # cryptsetup 2.4.3 processing "veritysetup -v --debug format --hash=sha256 --root-hash-file roothash.txt rootfs.squashfs rootfs.hash" # Running command format. # Allocating context for crypt device rootfs.hash. # Trying to open and read device rootfs.hash with direct-io. # Initialising device-mapper backend library. # Formatting device rootfs.hash as type VERITY. # Crypto backend (OpenSSL 3.0.2 15 Mar 2022 [default][legacy]) initialized in cryptsetup library version 2.4.3. # Detected kernel Linux 6.5.0-18-generic x86_64. # Setting ciphertext data device to rootfs.squashfs. # Trying to open and read device rootfs.squashfs with direct-io. # Hash creation sha256, data device rootfs.squashfs, data blocks 1476, hash_device rootfs.hash, offset 1. # Data device size required: 6045696 bytes. # Hash device size required: 57344 bytes. # Using 2 hash levels. # Updating VERITY header of size 512 on device rootfs.hash, offset 0. VERITY header information for rootfs.hash UUID: 7cbcdd9d-2c7f-4615-84b3-b2c8fd7ff0af Hash type: 1 Data blocks: 1476 Data block size: 4096 Hash block size: 4096 Hash algorithm: sha256 Salt: b61705a90f00c79348629adf0c777a8d9c923636f7f9ece854902f3a609233c5 Root hash: e3796aecd5f734716fc0f2569d54bf60c585dd61fa0e346586cb017b6f1ec27f # Created root hash file roothash.txt. # Releasing crypt device rootfs.hash context. # Releasing device-mapper backend. # Closing read write fd for rootfs.hash. Command successful.
After running this command, the root hash is in the roothash.txt
file, and the hash data is in rootfs.hash
. The former needs to be
read prior to mounting the verified file system, and the latter needs
to be put in a disk partition (or loop mounted as a file).
Mounting the disk can be done like this:
$ veritysetup open \ /dev/mmcblk0p5 \ dmv_root \ /dev/mmcblk0p6 \ e3796aecd5f734716fc0f2569d54bf60c585dd61fa0e346586cb017b6f1ec27f $ mkdir mnt $ mount /dev/mapper/dmv_root mnt/
The dmv_root
is an arbitrary name used to identify the verified file
system, and /dev/mmcblk0p5
is assumed to hold the file system image,
whereas /dev/mmcblk0p6
holds the hash data generated.
To support a dm-verity
in the Linux kernel, make sure you have the
following kernel options enabled:
CONFIG_MD=y CONFIG_BLK_DEV_DM=y CONFIG_DM_VERITY=y CONFIG_CRYPTO_SHA256=y CONFIG_CRYPTO_USER_API_HASH=y CONFIG_CRYPTO_USER_API_SKCIPHER=y