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

# Signed SWUpdate updates

Sign SWUpdate's `sw-description` with a key held in the HSM, in whichever of
SWUpdate's signature modes your devices are built with, and check the result with
SWUpdate itself.

The Cyber Resilience Act's Annex I requires that security updates can be distributed
securely and that unauthorised changes to the software are detected. With SWUpdate
both rest on one file: `sw-description` lists every artifact of the update with its
SHA-256, and SWUpdate checks its signature before it installs anything.

> **Note: How to read this**
> This is an example configuration, not a design guide. Choose what your devices
> trust, then take the commands for it. Delivering the archive to the device
> (hawkBit, a USB stick, a local web server) and the handlers that write the
> artifacts are your SWUpdate configuration and are not covered here.
> 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).

## Who does what

<div class="grid cards stages" markdown>

-   __LAAVAT__

    ---

    *Signer*

    The HSM holds the signing key. It receives only the SHA-256 of
    `sw-description`, never the update.

    **Returns** — a signature over `sw-description`

-   __Your build host__

    ---

    *Builder*

    Writes `sw-description` with every artifact's hash, asks for the signature
    with `signing-tool` and packs the `.swu`.

    **Never holds** — the private key

-   __SWUpdate on the device__

    ---

    *Verifier and installer*

    Checks the signature against the key or CA certificates in its image, then
    every artifact's hash, and only then installs.

    **Trusts** — the key or CA certificates in its image

</div>

## How SWUpdate checks an update

An update archive (`.swu`) is a `cpio` archive of three kinds of file:

- `sw-description`, which lists every artifact of the update with its SHA-256;
- `sw-description.sig`, the signature over `sw-description`;
- the artifacts themselves, for example `rootfs.ext4`.

Only `sw-description` is signed; the hashes in it cover the artifacts. SWUpdate checks
the signature first, then every artifact's hash, and installs nothing if either check
fails. The build host sends LAAVAT only the SHA-256 of `sw-description`, never the
artifacts.

An example `sw-description`. It lists a single artifact, `rootfs.ext4`; a real update
has one entry per artifact, each with its own `sha256`.

```text
software =
{
  version = "1.4.0";
  hardware-compatibility = [ "1.0" ];
  images: (
    { filename = "rootfs.ext4"; device = "/dev/mmcblk0p3"; type = "raw";
      sha256 = "<sha256 of rootfs.ext4>"; }
  );
}
```

## Choose what the device trusts

SWUpdate checks the signature against what its image gives it with `-k`. The signature
mode SWUpdate is built with decides which of two kinds of trust that is:

- **A plain key** (raw RSA or RSA-PSS): the device trusts one public key, the
  operation's.
- **A certificate chain** (CMS): the device trusts your CA hierarchy. The signature
  carries a certificate for the operation's key, and SWUpdate follows the chain from it
  up to the root. Built with OpenSSL, SWUpdate also takes the issuing CA from the
  signature, so the device needs only the root. Built with mbedTLS, it reads only one
  certificate from the signature, so the device holds the issuing CA as well.

| Build option | The device holds (`-k`) | You also need | Replacing the signing key | Fits |
| --- | --- | --- | --- | --- |
| `CONFIG_SIGALG_RAWRSA` (RSA PKCS #1 v1.5) or `CONFIG_SIGALG_RSAPSS` (RSA-PSS) | The operation's public key | Nothing besides LAAVAT | SWUpdate trusts one key, so the new key reaches devices in an update signed with the old one; from then on only the new key works | Short-lived or small fleets, no PKI |
| `CONFIG_SIGALG_CMS`, OpenSSL | Your root CA certificate | A CA hierarchy that issues a certificate for the signing key | A new key with a certificate under the same root needs no change on the device | Long-lived fleets, or a PKI you already run |
| `CONFIG_SIGALG_CMS`, mbedTLS | Your root CA and issuing CA certificates | As for OpenSSL | As for OpenSSL | As for OpenSSL |

The mbedTLS CMS mode needs SWUpdate 2026.05 or later, built with mbedTLS 3.4 or later;
this page was checked with SWUpdate 2026.05.1 and mbedTLS 3.6.

The mode is built into SWUpdate on the device, so choose it while planning the product.

Which library SWUpdate uses is also a build option: OpenSSL is the default, mbedTLS
(`CONFIG_SSL_IMPL_MBEDTLS`) is the alternative this page covers. Your device's SWUpdate
configuration shows both settings, `CONFIG_SIGALG_*` and `CONFIG_SSL_IMPL_*`.

## The LAAVAT product

The product has one `DigestSigning` operation. An RSA key serves every mode; an EC P-256
key also works in the CMS modes.

```json
{
  "name": "SWUpdate gateway",
  "description": "SWUpdate: sw-description signed by digest signing",
  "productType": "Production",
  "enabled": true,
  "caInfo": [], "rndKeys": [],
  "productOperations": [
    { "name": "sw-description signing",
      "description": "Signs the SHA-256 of sw-description",
      "operationType": "DigestSigning",
      "token": { "name": "SWUpdate signing key", "description": "RSA-3072", "keyType": "RSA3072" },
      "approvalRule": { "name": "Release", "description": "Release group requests, approver group approves",
                        "allowedGroups": ["<release-group-uuid>"], "approvalGroups": ["<approver-group-uuid>"], "blanketGroups": [] } }
  ]
}
```

```bash
signing-tool -n test.ini product add -T swupdate-gateway.json
# the new product is approved in the LAAVAT console before use; then export the operation's public key
signing-tool -n test.ini product getpubkey -P <product-uuid> --operid <operation-uuid> -O public.pem
```

**Approval rule.** The rule decides who may sign. A member of `allowedGroups` submits
the signing request; it then waits until a member of `approvalGroups` approves it, in
the LAAVAT console or with `signing-tool`, and only then does the HSM sign. If the
requester is also a member of one of the `blanketGroups`, no approval is needed and the
request is signed straight away; that is how an unattended build pipeline, such as a
Jenkins job, signs. It skips the human check, so keep those groups narrow. The group ids
in the rule are the object ids of groups in your identity provider (Microsoft Entra ID
or Google Cloud Identity). See [approval rules](https://docs.laavat.io/usage/productguide/product/) and
[configuration groups](https://docs.laavat.io/usage/adminguide/groups/).

`public.pem` is the operation's public key. Unlike in SWUpdate's own guide, you do not
generate a key pair with OpenSSL: the private key is created in the HSM and never leaves
it, and `product getpubkey` exports the public key. See
[key custody](https://docs.laavat.io/security/#key-custody-and-export).

## Prepare what the device trusts

### A plain key

Put `public.pem` into the device's root filesystem and start SWUpdate with
`-k public.pem`. If the boot chain verifies the root filesystem (for example with
dm-verity or a signed root filesystem image), the key cannot be swapped; see
[Secure boot and CRA](https://docs.laavat.io/cra-compliance/secure-boot-cra/).

### A certificate chain

SWUpdate does not trust the operation's key directly here: the key needs a certificate,
issued from your CA hierarchy, that names `public.pem` as its public key. For a test
setup, OpenSSL can issue one from the exported public key:

```bash
printf '%s\n' 'basicConstraints=CA:FALSE' 'keyUsage=critical,digitalSignature' \
    'extendedKeyUsage=emailProtection' 'subjectKeyIdentifier=hash' 'authorityKeyIdentifier=keyid' > signer.ext
openssl x509 -new -subj "/CN=SWUpdate signing" -force_pubkey public.pem \
    -CA issuing-ca.pem -CAkey issuing-ca.key -days 730 -extfile signer.ext -out signer.pem
cat signer.pem issuing-ca.pem > chain.pem
```

What the commands do:

- **`signer.ext`** lists what the certificate allows. `basicConstraints=CA:FALSE`: it
  is a signing certificate, not a CA. `keyUsage=critical,digitalSignature`: its key may
  make signatures. `extendedKeyUsage=emailProtection`: the purpose SWUpdate checks by
  default; if your CA issues `codeSigning` certificates instead, set
  `cert-purpose = "codeSigning"` in `swupdate.cfg`. `subjectKeyIdentifier` and
  `authorityKeyIdentifier`: identifiers verifiers use to match the certificate to its
  key and to the CA that issued it.
- **`openssl x509 -new`** creates the certificate. `-force_pubkey public.pem` puts the
  operation's public key into it (the private key stays in the HSM), `-CA` and `-CAkey`
  sign it with your issuing CA, and `-days 730` makes it valid for two years.
- **`cat`** writes `chain.pem`: the signing certificate followed by the issuing CA's.
  For the OpenSSL build, pass both to `signing-tool` (`--cms-cert chain.pem`).

**The files.** The hierarchy in this example has two levels: a root CA (`root.pem`) and
an issuing CA under it (`issuing-ca.pem`, with its private key `issuing-ca.key`) that
issues the signing certificate. The device's `-k` file, in its root filesystem like a
plain key, is `root.pem` for the OpenSSL build. For the mbedTLS build it is the root and
the issuing CA in one file:

```bash
cat issuing-ca.pem root.pem > ca-chain.pem
```

> **Note: Test setup or production**
> The commands above issue the certificate straight from the exported public key, so
> the issuing CA's private key has to be on the machine that runs them. That is fine
> for a test CA. A production CA normally issues certificates only from a CSR, which
> the key itself signs, and keeps its own private key in its protected environment.
> For that case, use a certificate the platform holds: the platform creates the key in
> the HSM together with a CSR for your CA. See
> [Certificate-based signing](https://docs.laavat.io/solutions/cra-ready-certificate-signing/).

## Sign `sw-description`

Every mode sends the same thing: `-p` is the SHA-256 of `sw-description`, base64-encoded
on one line, and it is all that reaches LAAVAT. The modes differ only in the options
after it, and `get --wait` downloads the signature as `sw-description.sig`.

### Raw RSA (`CONFIG_SIGALG_RAWRSA`)

```bash
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <operation-uuid> \
    -p "$(openssl dgst -sha256 -binary sw-description | base64 -w0)" -H SHA256 \
    -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
```

### RSA-PSS (`CONFIG_SIGALG_RSAPSS`)

The raw RSA request with `--padding PSS` added.

```bash
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <operation-uuid> \
    -p "$(openssl dgst -sha256 -binary sw-description | base64 -w0)" -H SHA256 --padding PSS \
    -N "swu 1.4.0" -D "sw-description 1.4.0"
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O sw-description.sig
```

### CMS, OpenSSL build (`CONFIG_SIGALG_CMS`)

The signature carries the signing certificate and the issuing CA: `--cms-cert chain.pem`.

```bash
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <operation-uuid> \
    -p "$(openssl dgst -sha256 -binary sw-description | base64 -w0)" -H SHA256 \
    --format CMS --cms-preset swupdate --cms-cert chain.pem -N "swu 1.4.0" -D "sw-description 1.4.0"
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O sw-description.sig
```

Optional: add `--padding PSS` to make the CMS signature RSA-PSS instead of PKCS #1 v1.5.
The OpenSSL build accepts both.

### CMS, mbedTLS build (`CONFIG_SIGALG_CMS`)

The signature carries the signing certificate alone: `--cms-cert signer.pem`.

```bash
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <operation-uuid> \
    -p "$(openssl dgst -sha256 -binary sw-description | base64 -w0)" -H SHA256 \
    --format CMS --cms-preset swupdate-mbedtls --cms-cert signer.pem -N "swu 1.4.0" -D "sw-description 1.4.0"
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O sw-description.sig
```

Two limits of the mbedTLS build:

- the signature must be RSA PKCS #1 v1.5 or ECDSA, so do not add `--padding PSS`;
- pass `signer.pem`, not `chain.pem`, to `--cms-cert`: a signature that carries the
  issuing CA as well is refused.

## Pack the update

The same for every mode. With `sw-description.sig` next to `sw-description` and the
artifacts:

```bash
printf 'sw-description\nsw-description.sig\nrootfs.ext4\n' | cpio -ov -H crc > update.swu
```

SWUpdate reads `sw-description` first and its signature second, so they lead the list;
the artifacts follow, one per line, the same ones `sw-description` lists. `-H crc` is one
of the two `cpio` formats SWUpdate accepts (`newc` is the other).

## Verify the update

SWUpdate's check mode (`-c`) runs the same checks as the device: the signature, then every
artifact's hash. It installs nothing. Use a `swupdate` built with the same signature mode
as the device's; distribution packages are usually built for CMS.

```text
swupdate -c -k public.pem -i update.swu     # raw RSA or RSA-PSS
swupdate -c -k root.pem -i update.swu       # CMS, OpenSSL build
swupdate -c -k ca-chain.pem -i update.swu   # CMS, mbedTLS build
SWUPDATE successful !
```

Every refused update ends with `Image invalid or corrupted. Not installing ...`, and
`swupdate` exits with a non-zero status; the lines before it say why. In the CMS modes a
changed `sw-description` is reported as `Signature verification failed`, and the OpenSSL
build reports the same for another CA's root or a `codeSigning` certificate without
`cert-purpose`. Add `-H <board>:<revision>` when your build checks hardware compatibility.

## Where off-hardware verification stops

| Claim | Off-hardware | What would close the gap |
| --- | --- | --- |
| Each mode's signature is accepted by SWUpdate, with RSA-2048 and RSA-3072 keys, and EC P-256 keys in both CMS builds | Checked on the build host with `swupdate -c` built for each mode, plus a distribution-packaged SWUpdate for CMS | Not needed |
| A changed description, a changed artifact, a foreign key or another CA's root is refused | Checked on the build host with `swupdate -c` | Not needed |
| mbedTLS: RSA and ECDSA accepted, RSA-PSS and a full chain refused | Checked on the build host with SWUpdate 2026.05.1 and mbedTLS 3.6 | Not needed |
| The device holds the right key or CA certificates, protected by secure boot | Not testable off-device | The device's image build and secure boot chain |
| The handlers install the artifacts | Not tested | An update on the target |

## 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 verifies the signature over `sw-description` and every artifact's hash before it installs |
| Part I (2)(c): vulnerabilities can be addressed through security updates. Part II (7): mechanisms to securely distribute updates | Signed `.swu` archives over any channel; a tampered or foreign archive is refused on the device |
| Supporting evidence, not an Annex I product requirement: custody of the signing key | The signing key stays in the HSM. Only members of the release group can request a signature, and each request is signed only after a member of the approver group approves it. Every request is in the platform's audit trail |

## Related

- [Digest signing](https://docs.laavat.io/usage/signing-encryption/digest-signing/) — every option, preset
  and error message
- [Signatures with digest signing](https://docs.laavat.io/solutions/cra-ready-digest-signing/) —
  kernel modules, tokens, attestations and post-quantum keys
- [Certificate-based signing](https://docs.laavat.io/solutions/cra-ready-certificate-signing/) — CMS
  signatures with a certificate your CA issues from a CSR and the platform holds
- [Signed Mender OTA updates](https://docs.laavat.io/solutions/cra-ready-mender-ota/) — the same idea with Mender
- [Secure boot and CRA](https://docs.laavat.io/cra-compliance/secure-boot-cra/) — the boot chain that
  protects the key on the device

## References

- [SWUpdate: signed images](https://sbabic.github.io/swupdate/signed_images.html),
  including
  [generating the private and public key](https://sbabic.github.io/swupdate/signed_images.html#generating-private-and-public-key),
  which the HSM replaces here
- [SWUpdate: the sw-description syntax](https://sbabic.github.io/swupdate/sw-description.html)
