> Markdown version of https://docs.laavat.io/solutions/cra-ready-certificate-signing/ from the LAAVAT PKI and Signing Platform documentation. All pages: https://docs.laavat.io/llms.txt

# 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`.

> **Note: 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](https://docs.laavat.io/exampleflows/detachedsigning/); 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](https://docs.laavat.io/solutions/cra-ready-digest-signing/) and
> [SWUpdate](https://docs.laavat.io/solutions/cra-ready-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](https://docs.laavat.io/cra-compliance/annex-i-mapping/) sets out which requirements
> the platform supports and which remain yours.

> **Note: 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](https://docs.laavat.io/democlient/usage/#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](https://docs.laavat.io/usage/productguide/product/).

## 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](https://docs.laavat.io/exampleflows/detachedsigning/)).
- 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](https://docs.laavat.io/solutions/cra-ready-certificate-signing/#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](https://docs.laavat.io/usage/clientguide/clients/)), as on the
  [OCI signing page](https://docs.laavat.io/exampleflows/ocisigning/#retrieve-signing-certificate-and-public-key):

```bash
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](https://docs.laavat.io/exampleflows/detachedsigning/) 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

```bash
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](https://docs.laavat.io/solutions/cra-ready-swupdate/#pack-the-update).

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

To verify:

```text
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

```bash
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:

```text
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

```bash
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:

```text
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 |

## Related

- [Detached signing example](https://docs.laavat.io/exampleflows/detachedsigning/) — profiles, the
  product, the CSR and the chain
- [OCI signing](https://docs.laavat.io/exampleflows/ocisigning/#retrieve-signing-certificate-and-public-key)
  — reading the signing certificate and the chain from the product
- [Signed SWUpdate updates](https://docs.laavat.io/solutions/cra-ready-swupdate/) — packing the `.swu`,
  and SWUpdate's other modes
- [Signatures with digest signing](https://docs.laavat.io/solutions/cra-ready-digest-signing/) — use
  cases that need no certificate
- [Digest signing](https://docs.laavat.io/usage/signing-encryption/digest-signing/) — every CMS option and
  preset

## References

- [SWUpdate: signed images](https://sbabic.github.io/swupdate/signed_images.html)
- [Linux kernel module signing](https://docs.kernel.org/admin-guide/module-signing.html)
- [sign-efi-sig-list(1)](https://manpages.debian.org/bookworm/efitools/sign-efi-sig-list.1.en.html)
