Code signing is fairly ubiquitous and is a cryptographic technique for verifying the authenticity of a binary. It is often used as part of the secure boot process of an embedded device where software components of the bootchain are verified by the previous component. For example, the on-chip ROM in a SoC will verify the bootloader binary (e.g. u-boot) before loading u-boot, and u-boot will verify a kernel binary image before loading the kernel and so on.
In essence, secure boot prevents the SoC from loading software that has not been signed with a private key. The verification process involves using the corresponding public key, and in SoCs that implement secure boot, a hash of the public key (or some derivation of the public key) is stored in one or more fuse registers which are effectively one-time programmable registers that cannot be reset once written.
The i.MX family of processors from NXP provide this secure boot capability through a component in the on-chip ROM called High Assurance Boot (HAB). HAB allows the on-chip ROM to verify the initial bootloader and in doing so provides a mechanism to establish a root of trust for the remaining software components in the boot chain.
An important aspect of secure boot is key management, as with any form of public-key cryptography, the private keys need to remain secret. It’s normally good organisational processes and practices that ensure the protection of keys, combined with the use of appropriate tools and technology. One useful tool is a Hardware Security Module (HSM), this is a physical device (such as the Yubico YubiHSM 2 device) that can securely store secrets. For example, you can store a private key (which generally cannot be retrieved) and then ask the HSM (often via it’s PKCS #11 interface) to use that key to perform an action (for example code signing). It’s significantly harder (perhaps impossible) to copy a HSM than it is to copy a private key.
Another useful tool is a signing server – for example digsigserver. Signing servers allow you to perform code signing as a service on your network, thereby preventing the need for distribution of private keys to those that need to sign images. Clients can send a signing request to the server along with the binary to be signed and receive back a signed binary (or a binary blob containing the signature and other required data). The digsigserver signing server includes support for signing software for i.MX, Nvidia Jetson and Rockchip SoCs amongst others. When using a signing server, access to the signing service can be limited via existing organisational means. As the private keys are kept local to the signing server, it’s no longer possible for employees to terminate their employment and take copies of private keys with them.
The tool used to actually sign images for i.MX chips comes from NXP and rather imaginatively it is called CST (Code Signing Tool). Fairly recently, CST was split into a front end consisting of NXP proprietary operations and a choice of two backends for cryptographic operations, one using OpenSSL with key material directly in the filesystem, and one using OpenSSL in conjunction with a PKCS#11 interface for performing certain cryptographic on a HSM. Note that these two backends are referred to as reference backends which would imply that it may be possible to write your own.
We recently implemented a secure boot solution for an i.MX8 based customer and they asked us if we could also provide a solution for protecting their private keys. Part of this solution was to make use of the digsigserver but rather than store private keys local to the server they would instead be stored in a YubiKey HSM 2. Unfortunately digsigserver didn’t support using HSM’s so our job was to add this support and upstream our changes back to the project. The remainder of this blog post describes how to make use of this new support.
Setting up the code signing server
There are three main steps in setting up the code signing server to sign i.MX images using the YubiHSM 2:
- Build the docker image containing YubiHSM support software and the code signing server
- Generate the HAB4 PKI tree
- Run the docker image to set up the YubiHSM key
After everything is set up, the code signing server can be used to service signing requests.
Build the docker image
The dockerfile used to build the docker image containing the digsigserver and support for the YubiHSM 2 is here.
To build the image from the dockerfile, and assuming that you have cloned the digsigserver repo and are in the top directory of the repo, you need first to copy a tar.gz archive of the NXP CST release 3.3.2 or later into the ./docker/nxp_tools directory as there is no easy way to fetch this automatically using curl or similar. The best way to get it is to login to the NXP website and search for IMX_CST_TOOL_NEW. Finally, build the docker image as follows :
docker build . -f docker/Dockerfile.nxp-hsm -t digsigserver-nxp-hsm:latest
Generate the HAB4 PKI tree
This is actually outside the scope of this article, but you should be familiar with this if you have ever implemented secure boot on the i.MX family of processors. In essence it involves running an NXP-supplied script called hab4_pki_tree.sh which can generate up to 4 SRKs (Super Root Keys), and under each SRK are two subordinate keys, the CSF key (for verifying the signature across CSF commands) and the IMG key (for verifying signatures across product software).
After creating the PKI tree, it is then necessary to create an SRK table along with corresponding fuse information which is done by another NXP-supplied tool called srktool.
Assuming that we are only using SRK1 for secure boot (the use of more than one SRK allows SRKs to be revoked if compromised) the only files in the PKI tree that we are interested in are as follows :
.
├── ca
├── crts
│ ├── CSF1_1_sha256_4096_65537_v3_usr_crt.der
│ ├── IMG1_1_sha256_4096_65537_v3_usr_crt.der
│ ├── SRK_1_2_3_4_table.bin
└── keys
├── CSF1_1_sha256_4096_65537_v3_usr_key.der
├── IMG1_1_sha256_4096_65537_v3_usr_key.der
├── key_pass.txt
When running the signing server, the keys and certificates are all stored on the YubiHSM 2, and the SRK table is the only file that needs to be remain in the filesystem.
Prepare the YubiHSM 2
The YubiHSM 2 needs to be prepared before using with digsigserver :
- Set the password for accessing the YubiHSM 2
- Store keys and certificates on the YubiHSM 2
The easiest way to do this is from the docker container as it contains all the necessary support programs and libraries for the YubiHSM 2.
Run the docker container
The required keys and certificates must be made available to the container, so assuming they are in a directory imx_keys inside your home directory, then the container would be started as follows :
docker run \
--rm \
-it \
--privileged -v /dev/bus/usb:/dev/bus/usb \
-v ~/imx_keys:/opt/imx_keys \
digsigserver-nxp-hsm:latest /bin/bash
The line ‘-v ~/imx_keys:/opt/imx_keys’ shares your directory ~/imx_keys with the container and it will be at /opt/imx_keys from within the container.
The –rm option means that the container is removed after exiting. Without this option, an image of the running container is saved every time you exit from it and so if you run the docker image many times this can use up a great deal of disk space. With the –rm option, any changes that you make to the container’s filesystem (except for directories shared with the host) are lost when you exit.
The line ‘–privileged -v /dev/bus/usb:/dev/bus/usb’ gives the docker container access to the host USB bus which is required in order for the container to be able to use the YubiHSM.
Set the YubiHSM 2 password
The YubiHSM 2 stores everything as an Object which has an Object ID. Objects may also be assigned text labels by which to refer to them. Objects can be things like private keys and certificates. There are also authentication objects which are used to grant access to other objects. When new, or after a factory reset, the YubiHSM contains a single authentication object with Object ID 0001 which allows you to access the YubiHSM, and this is set to a default password of “password”. So to access sensitive objects on the token such as private keys, you need to authenticate with object 0001 first using the password.
When the container is started with the command /bin/bash, the “yubihsm connector” software needs to be started manually to enable the yubihsm utilities to access the YubiHSM 2, this is done by running “yubihsm-connector start” from the command line.
The yubihsm-shell utility can be used to change the password. Say you want to change the password from the default “password” to “newpassword” :
root@acb68063238f:/opt/imx_keys# yubihsm-connector start
root@acb68063238f:/opt/imx_keys# yubihsm-shell
Using default connector URL: http://localhost:12345
yubihsm> connect
Session keepalive set up to run every 15 seconds
yubihsm> session open 0001 password
Created session 0
yubihsm> change authkey 0 0001 newpassword
Changed Authentication key 0x0001
yubihsm> session close 0
yubihsm> exit
root@acb68063238f:/opt/imx_keys#
In the above sequence, “connect” opens a connection to the YubiHSM via the yubihsm-connector. In the “session open” command, 0001 (or you use just 1 instead) is the object id of the authentication object to use and password is the password for that object, and the session id 0 is returned which is used in subsequent commands.
Store certificates and keys on the YubiHSM 2
As well as the yubihsm utilities provided by Yubico, the docker image also contains various OpenSC utilities such as pkcs11-tool which can be used to access the YubiHSM 2. OpenSC is an open source smart card framework, see OpenSC. In this case we will use pkcs11-tool in order to store the required certificates and keys on the YubiHSM 2.
Decrypt the private keys
In the container, the private keys are in the directory /opt/imx_keys/keys which is shared with the host (see the ‘docker run’ command above), and since the keys are stored encrypted, we need to decrypt them first before writing them to the YubiHSM 2. The password for the encryption is in the keys/key_pass.txt file (two copies of the password on two lines). Run the following commands from the keys directory to decrypt the CSF1_1 and IMG1_1 private keys:
openssl rsa -in IMG1_1_sha256_4096_65537_v3_usr_key.der -out IMG1_1_key.der
openssl rsa -in CSF1_1_sha256_4096_65537_v3_usr_key.der -out CSF1_1_key.der
Write keys and certificates to the YubiHSM 2
From the /opt/imx_keys directory, export the following environment variables for use with the pkcs11-tool commands that will be used afterwards:
export USR_PIN=0001password
export IMG1_KEY=IMG1_1_sha256_4096_usr
export CSF1_KEY=CSF1_1_sha256_4096_usr
For USR_PIN, “0001password” should be replaced with “0001” followed immediately by the password you set to access the YubiHSM 2. The “0001” prefix is the object id of the authenication object used, and this prefix is required by pkcs11-tool.
IMG1_KEY and CSF1_KEY are labels that are used to access the keys or certificates rather than using explicit object ids. Note that whatever labels you actually use need to be specified in the CSF files used in the signing process.
Note here that you should have set the environment variable PKCS11_MODULE (after ‘sudo apt install p11-kit’):
export PKCS11_MODULE=/usr/lib/x86_64-linux-gnu/p11-kit-proxy.so
Now write the keys and certificates to the YubiHSM from the /opt/imx_keys directory as followss :
pkcs11-tool --module $PKCS11_MODULE -l --write-object keys/CSF1_1_key.der --type privkey --usage-sign --label $CSF1_KEY --id 1002 --pin $USR_PIN
pkcs11-tool --module $PKCS11_MODULE -l --write-object keys/IMG1_1_key.der --type privkey --usage-sign --label $IMG1_KEY --id 1003 --pin $USR_PIN
pkcs11-tool --module $PKCS11_MODULE -l --write-object crts/CSF1_1_sha256_4096_65537_v3_usr_crt.der --type cert --label $CSF1_KEY --id 1002 --pin $USR_PIN
pkcs11-tool --module $PKCS11_MODULE -l --write-object crts/IMG1_1_sha256_4096_65537_v3_usr_crt.der --type cert --label $IMG1_KEY --id 1003 --pin $USR_PIN
Note that the CSF1_1 key and certificate are given the same label and object id which is ok as they are different object types. Similarly for the IMG1_1 key and certificate.
Note that when a private key is written to the YubiHSM, it automatically extracts the public key and creates a public key object with the same object id and label.
Quick check
Check that everything looks good by running the pkcs11-tool –list-objects command:
pkcs11-tool --module $PKCS11_MODULE -l --pin $USR_PIN --list-objects
The output should look similar to this :
Using slot 0 with a present token (0x10)
Private Key Object; RSA
label: IMG1_1_sha256_4096_usr
ID: 1003
Usage: sign
Public Key Object; RSA 4096 bits
label: IMG1_1_sha256_4096_usr
ID: 1003
Usage: verify
Private Key Object; RSA
label: CSF1_1_sha256_4096_usr
ID: 1002
Usage: sign
Public Key Object; RSA 4096 bits
label: CSF1_1_sha256_4096_usr
ID: 1002
Usage: verify
Certificate Object; type = X.509 cert
label: IMG1_1_sha256_4096_usr
ID: 1003
Certificate Object; type = X.509 cert
label: CSF1_1_sha256_4096_usr
ID: 1002
Exit the container
Now that the YubiHSM 2 has been prepared, exit from the container with ‘exit’. The container is run using a different ‘docker run’ command when it is used to run the signing server.
Prepare the imx-cst-keys.tar.gz file
As mentioned previously, this imx-cst-keys.tar.gz file, located in the user’s home directory is used by the signing server. When the signing server is using the YubiHSM 2, only the crts/SRK_1_2_3_4_table.bin is required to be present in the file. One way to create this file in your home directory might be as follows, assuming that your HAB4 PKI files are in directory ~/imx_keys :
cd ~
mkdir -p ~/tmp/imx_keys/crts
cp ./imx_keys/crts/SRK_1_2_3_4_table.bin ./imx_keys/crts/SRK_1_2_3_4_fuse.bin ./tmp/imx_keys/crts/
tar czf imx-cst-keys.tar.gz -C tmp/imx_keys .
Running the code signing server
The signing server can be run using the following command :
docker run \
--restart unless-stopped \
--privileged -v /dev/bus/usb:/dev/bus/usb \
--env "YUBIHSM_PASSWORD=0001password" \
-p 9999:9999 \
--mount type=bind,source=$HOME/imx-cst-keys.tar.gz,target=/digsigserver/{machine}/imxsign/imx-cst-keys.tar.gz,readonly \
digsigserver-nxp-hsm:latest
Note the {machine} placeholder identifies your hardware, for example it would normally be set to the MACHINE variable in a yocto build, for instance imx8mp.
The environment variable YUBIHSM_PASSWORD is used by the signing server to login to the YubiHSM. As mentioned before, the 0001 prefix is required by pkcs11, and this is immediately followed by the actual password.
Also, when there is no command at the end of the ‘docker run’ command, the default command given in the dockerfile is run, in this case ‘yubihsm-connector start && digsigserver –debug’.
Using the code signing server
REST API
When the docker container starts, it runs the Sanic http server which is set up to provide a REST API on the port specified in the ‘docker run’ command. On receiving a request at the /sign/imx endpoint, internally the server code will run the CST using the passed in parameters to create the output data.
Request type: POST
Endpoint: /sign/imx
Expected parameters:
machine=<machine-name>
– a name for the device, used to locate the signing keyssoctype=<soctype>
– currently onlymx8m
recognized (but not currently used)cstversion=<cst-version>
– the version of CST (e.g.,3.3.
2)csf=<body>
– plain/text CSF description fileartifact=<body>
– binary associated with the CSF description file
Optional parameters:
backend=<backend-type>
– backend can be pkcs11 or ssl, defaults to ssl if ommitted – note that cstversion must be at least 3.3.2 if pkcs11 is selected.
Response: binary CSF blob containing the signing commands, signature hashes, and certificates
The client is responsible for inserting/appending the CSF blob (and an IVT) at the correct location in the binary for use on the target device.
Required changes to CSF files
CSF (command sequence file) files are generated by the build and basically contain instructions for the HAB firmware and provide paths to key files etc. for CST. CST then converts these to a binary form which are then placed in a firmware image at appropriate header locations.
When using the YubiHSM 2, the Code Signing Tool running on the server will use the pkcs11 backend to use the keys which are kept on the token and the CSF files should be modified to indicate that the keys are on the token rather than on the filesystem. So replace the following :
File = "crts/CSF1_1_sha256_4096_65537_v3_usr_crt.pem"
with
File = "pkcs11:token=YubiHSM;object=CSF1_1_sha256_4096_usr;type=cert;pin-value=password"
And similarly, replace the following :
File = "crts/IMG1_1_sha256_4096_65537_v3_usr_crt.pem"
with
File = "pkcs11:token=YubiHSM;object=IMG1_1_sha256_4096_usr;type=cert;pin-value=password"
Note that “password” in the “pin-value=password” field is just a placeholder as this will be replaced with the actual password on the server, the server searches for this string so setting this field to anything else will fail.
The names CSF1_1_sha256_4096_usr and IMG1_1_sha256_4096_usr are the labels used to access the CSF1_1 and IMG1_1 keys on the token.
Example request using curl
The build system for the i.MX8 software will need to be modified such that the generated artifacts are signed. This can easily be achieved with a build system such as Yocto by updating the relevant recipes to interact with the signing server via curl (or similar) rather than directly with the CST tools. Following is an example curl command that may be invoked to the signing server’s /sign/imx endpoint.
curl --silent --fail \
-X POST -F machine=imx8mp -F soctype=mx8m -F cstversion=3.3.2 \
-F backend=pkcs11 \
-F "csf=@${HOME}/imx-cst-keys/flash_evk-csf-fit.csf;type=text/plain" \
-F "artifact=@${HOME}/imx-cst-keys/imx-boot-${MACHINE}-sd.bin-flash_evk" \
--output ${HOME}/imx-cst-keys/flash_evk-csf-fit.bin \
http://172.17.0.1:9999/sign/imx
The returned blob then needs to dd’d to the correct offset in the image which is specified in the CSF file.
Typically for i.MX, an image will contain an SPL image and a FIT image and the build will create a separate CSF file for each, so you would send two requests, one with the SPL CSF file and the other with the FIT CSF file, but sending the same actual image file with each request.
It’s not a trivial task to protect cryptographic material such as private keys used for secure boot, yet there are potentially serious implications for when this isn’t done correctly. In this blog post we’ve seen how both HSMs and signing servers can be use to significantly limit the distribution of keys and allow for better controls (and auditing) surrounding signing code. Thanks to our work, it’s now possible for anyone to use the digsigserver along with the YubiHSM 2 to sign images for platforms including the i.MX8.
Find out More
Yubico, the inventor of the YubiKey, offers the gold standard for phishing-resistant multi-factor authentication (MFA), stopping account takeovers in their tracks and making secure login easy and available for everyone. Since the company was founded in 2007, it has been a leader in setting global standards for secure access to computers, mobile devices, servers, browsers, and internet accounts. Yubico is a creator and core contributor to the FIDO2, WebAuthn, and FIDO Universal 2nd Factor (U2F) open authentication standards, and is a pioneer in delivering hardware-based passwordless authentication using the highest assurance passkeys to customers in 160+ countries.
Yubico’s solutions enable passwordless logins using the most secure form of passkey technology. YubiKeys work out-of-the-box across hundreds of consumer and enterprise applications and services, delivering strong security with a fast and easy experience.