Skip to content

Certificate-based signing

Some verifiers trust a certificate, not a bare key. Let your CA certify a key that lives in the HSM, and let the platform hold that certificate for you.

Verifiers such as SWUpdate in CMS mode, the Linux kernel and UEFI trust a certificate, not a bare key, so the signing key needs a certificate from your CA. Your CA issues certificates from a CSR, which the key itself must sign. A DetachedSignature operation gives you exactly that: the platform creates the key in the HSM with a CSR for your CA and keeps the issued certificate. SWUpdate and UEFI signatures carry that certificate from the platform, so those pipelines need no certificate file; kernel modules need signer.pem for sign-file.

How to read this

This page covers what each verifier needs. Creating the product, getting the CSR signed and uploading the chain are in the detached signing example; LAAVAT can also host the PKI hierarchy that signs the CSR. If your verifier trusts a bare public key instead (Mender, SWUpdate's raw modes, ESP32), the digest signing and SWUpdate pages are simpler. IMA is not covered here: its envelope works only with DigestSigning operations; see the digest signing page. Building a device this way does not by itself make a product CRA compliant; the Annex I mapping sets out which requirements the platform supports and which remain yours.

Client version

The commands on this page need signing-tool 3.11.0 or later. The config file test.ini is set up once, as described in providing the token.

Terms used here. A product holds the keys for one device line. Each operation in it is one key: its type says what the key is (here DetachedSignature, a key with a certificate), and its approval rule says who may ask for a signature and who approves. A request is one signing call. Its type is DigestSigning whatever the operation type: you always send a digest, and the operation id decides which key signs it. See the product guide.

The setup, in short

  • A product with one DetachedSignature operation per purpose, for example one for SWUpdate updates, one for kernel modules and one for the UEFI KEK, each with an end-entity certificate of its own. Each certificate slot is marked externalRoot: the platform creates the key in the HSM and a CSR for it, and waits for the chain (detached signing example).
  • Your CA, or a hierarchy hosted in LAAVAT, signs the CSR, and the chain is uploaded to the product with ca patch.
  • Download the certificates. The devices need them (see Which certificate goes where), and so does the kernel module command below. The product keeps the issued chain; read it with a ProductionPC client registered for the product (client.key is that client's private key; see clients), as on the OCI signing page:
signing-tool -n test.ini secrets get -P <product-uuid> -C client.key -O secrets.json
jq -r '.caInfo[] | select(.cn=="<certificate CN>") | .pkcs7chain' secrets.json | base64 -d > chain.p7
openssl pkcs7 -in chain.p7 -print_certs -out chain.pem
openssl x509 -in chain.pem -out signer.pem
awk '/BEGIN CERT/{n++} n>1' chain.pem > ca-chain.pem

chain.pem lists the signing certificate first, then the CA certificates above it: signer.pem is the first, ca-chain.pem the rest, the issuing CA and the root. Which of them a device holds decides what a renewal touches:

  • SWUpdate holds the CA chain, so a renewed signing certificate under the same CA needs no change on the device; a new CA does.
  • The kernel and UEFI hold the signing certificate itself, so renewing it means an update to the device's trusted keys or KEK.

Requests use imagesigning add DigestSigning, the request type that sends a digest, with the DetachedSignature operation's id: the operation decides which key signs, here the key of its certificate. LAAVAT's detached signing example signs the same way. A CMS preset then shapes the signature for the verifier.

Which certificate goes where

Verifier The device trusts Also needed
SWUpdate in CMS mode (OpenSSL or mbedTLS build) ca-chain.pem, the issuing CA and the root, as the -k file. The CMS carries only the signing certificate, so the device holds the issuing CA as well, with either build cert-purpose = "codeSigning" in swupdate.cfg: the certificate is for code signing, and SWUpdate expects emailProtection by default
Linux kernel modules signer.pem, built in with CONFIG_SYSTEM_TRUSTED_KEYS or loaded into the secondary keyring Not needed
UEFI db updates The KEK operation's signer.pem, enrolled as the KEK Not needed

SWUpdate in CMS mode

signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <swupdate-operation-uuid> \
    -p "$(openssl dgst -sha256 -binary sw-description | base64 -w0)" -H SHA256 \
    --format CMS --cms-preset swupdate --cms-embed SIGNER -N "swu 1.4.0" -D "sw-description 1.4.0"
Image signing request sent. Request ID: <request-uuid> state: Created
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O sw-description.sig

--cms-embed SIGNER asks the platform to put its copy of the signing certificate into the CMS, so no certificate file is needed on the build host. The same signature works with SWUpdate built with OpenSSL and with mbedTLS. Pack the .swu as on the SWUpdate page.

# swupdate.cfg
globals :
{
  cert-purpose = "codeSigning";
};

To verify:

swupdate -c -k ca-chain.pem -f swupdate.cfg -i update.swu
SWUPDATE successful !

Without cert-purpose SWUpdate refuses the update with Signature verification failed. With another CA's certificates in -k it is refused too, ending in Image invalid or corrupted. Not installing ...; the OpenSSL build also logs unable to get local issuer certificate.

Linux kernel modules

signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <kmod-operation-uuid> \
    -p "$(openssl dgst -sha512 -binary my_module.ko | base64 -w0)" -H SHA512 \
    --format CMS --cms-preset kernel-module -N "my_module 1.4.0" -D "kernel module"
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O my_module.p7s
scripts/sign-file -s my_module.p7s sha512 signer.pem my_module.ko

The kernel-module preset makes a CMS without signed attributes, as the kernel requires for module signatures. No --cms-cert is needed: the platform names its own certificate in the signature, by issuer and serial number, which is how the kernel finds the key in its keyring. sign-file comes with the kernel source and appends the signature in the kernel's format; the module body is unchanged. The envelopes on the digest signing page work only with DigestSigning operations, so with this operation the module is signed in these three steps.

To verify:

perl scripts/extract-module-sig.pl -0 my_module.ko > my_module.unsigned.ko
perl scripts/extract-module-sig.pl -s my_module.ko > my_module.sig.p7s
openssl cms -verify -binary -inform DER -in my_module.sig.p7s -content my_module.unsigned.ko \
    -certfile signer.pem -CAfile ca-chain.pem -purpose any -out /dev/null
CMS Verification successful

UEFI db updates

cert-to-efi-sig-list -g <your-owner-guid> db.pem db.esl
sign-efi-sig-list -a -o -t "<YYYY-MM-DD HH:MM:SS>" db db.esl db.tbs
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <kek-operation-uuid> \
    -p "$(openssl dgst -sha256 -binary db.tbs | base64 -w0)" -H SHA256 \
    --format CMS --cms-preset uefi --cms-embed SIGNER -N db -D "db update"
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O db.p7
sign-efi-sig-list -a -i db.p7 -t "<YYYY-MM-DD HH:MM:SS>" db db.esl db.auth

The owner GUID is a fixed value you keep, which marks your entries in db. --cms-embed SIGNER puts the KEK's certificate into the PKCS #7 from the platform, so no certificate file is needed here either. -a appends the certificate to db; without it the update replaces every entry, which can leave machines unable to boot. Both calls need it, with the same -t timestamp, and -i takes the PKCS #7 as downloaded, in DER.

To verify:

openssl cms -verify -binary -inform DER -in db.p7 -content db.tbs \
    -CAfile ca-chain.pem -purpose any -out /dev/null
CMS Verification successful

The firmware itself checks db.auth against the KEK when the variable is written.

Where off-hardware verification stops

Claim Off-hardware What would close the gap
SWUpdate accepts the signature with the CA chain as -k and cert-purpose set; refuses another CA, or the default purpose Checked on the build host with swupdate -c built with OpenSSL and with mbedTLS, and a distribution-packaged SWUpdate Not needed
A kernel module signature verifies under the chain; a changed module is refused Checked on the build host with the kernel's scripts and openssl cms The kernel loading the module
db.auth carries the platform's PKCS #7, which verifies under the chain Checked on the build host with efitools and openssl cms The firmware writing the variable

Mapping to the CRA

Annex I requirement Covered by
Part I (2)(f): protect the integrity of programs, commands and configuration against manipulation or modification not authorised by the user SWUpdate, the kernel and UEFI check each update, module and variable against a certificate chain you control
Supporting evidence, not an Annex I product requirement: custody of the signing key The private key stays in the HSM and is certified through your CA's normal CSR process; every request is in the platform's audit trail

References