> Markdown version of https://docs.laavat.io/solutions/encrypted-rauc-per-device-keys/ from the LAAVAT PKI and Signing Platform documentation. All pages: https://docs.laavat.io/llms.txt

# Encrypted RAUC updates with per-device keys

An update bundle can be built so that only the devices of one rollout can read
it. Each device keeps its own decryption key inside OP-TEE, where it is
generated and stays. LAAVAT issues each device a certificate for that key and
builds each RAUC bundle signed and encrypted for the devices it is addressed
to.

This guide is an example of such a setup. Take the parts that match your own
design. For why a single shared update key is not enough, see
[Encrypted firmware updates](https://www.laavat.io/post/encrypted-firmware-updates)
on the LAAVAT blog.

> **Note: How to read this**
> This is an example configuration, not a design guide. It shows which
> platform calls issue the device certificates and build an encrypted
> bundle, and what the device needs to open it. The device registry, update
> delivery and the rollout plan stay with the manufacturer and are not
> covered. The flow was tested on the LAAVAT platform with SoftHSM standing
> in for OP-TEE's PKCS#11 interface.

## RAUC crypt bundles and the device key

A RAUC bundle in the `crypt` format carries its payload encrypted with a random
key. That key is in turn encrypted to each recipient certificate listed when
the bundle is built, so every listed device can recover it with its own private
key, and no other device can. The bundle is also signed, as every RAUC bundle
is.

In this example every device has its own key, generated in OP-TEE, and each
bundle lists the devices of one rollout as its recipients.

| Piece | Used by | On the device | LAAVAT role |
| --- | --- | --- | --- |
| Device encryption key (EC P-256) | RAUC, to decrypt the bundle | Generated inside OP-TEE, never extractable | None: the key never leaves the device |
| Device encryption certificate | The build, as a recipient; RAUC, to find its entry | Stored next to the key, named in `system.conf` | Issues it from the Device Encryption CA, from the device's CSR |
| Bundle signature | RAUC, before anything else | RAUC keyring: the CA chain of the signing key | Signs each bundle with a one-time key under the product's RAUC CA |
| Bundle encryption | RAUC, with the device key | Nothing extra | Encrypts the payload to the recipient certificates in the request |

Nothing on the device talks to LAAVAT at update time.

> **Abstract: Core property — no shared decryption key**
> There is no key that opens every device's updates. Each device's private
> key is generated inside OP-TEE and never leaves it; the platform sees only
> its CSR and certificate. Extracting the key from one device opens only the
> bundles addressed to that device. The CA and bundle-signing keys stay in
> the HSM — see [key custody](https://docs.laavat.io/security/#key-custody-and-export).

## The certificate hierarchy

The device certificates come from their own Sub CA, separate from the CA that
issues device identities (IDevID). An encryption certificate then can never be
used as an identity certificate, and the two can be managed apart.

```mermaid
graph TD
    R["<b>Root CA</b><br/>your existing root"]
    S["<b>Device Encryption CA</b><br/>Sub CA · ECDSA P-256<br/>issues device certificates only"]
    L["<b>Device encryption certificate</b><br/>one per device · P-256 key from the CSR<br/>Key Agreement only"]
    R -->|signs once| S
    S -->|issues, per device| L
```

The bundle signature uses the product's existing RAUC CA chain, not this
hierarchy.

### Profiles

Two profiles define it. The one that matters for decryption is the leaf
profile: the key comes from the device's CSR, and the certificate may be used
for key agreement and nothing else.

```yaml
# device-encryption-leaf-profile.yaml
Name: Device Encryption Leaf
DN:
  Organization: Manufacturer
  Country: FI
  # CommonName comes from the CSR, one per device
ProfileType: 1                 # end entity
SignatureAlgorithm: 10         # ECDSAWithSHA256, by the EC Sub CA
KeyAlgorithm: 2                # ECDSA, the device's key from the CSR
ValidityPeriod: "175200h"      # 20 years, example
BasicConstraints:
  BasicConstraintsValid: true
  IsCA: false
AddSKI: true
AddAKI: true
KeyUsage:
  - 16                         # KeyAgreement only
ExtraExtensions: false
SANUsage: false
EnforceUniqueDN: true          # one certificate per device CN
```

The Sub CA profile is a standard one: ECDSA P-256, CertSign and CRLSign, path
length 0. Both are created once, here with `signing-tool`. Its config file
`test.ini` is set up once, as described in
[providing the token](https://docs.laavat.io/democlient/usage/#providing-the-token).

```bash
signing-tool -n test.ini profile add \
    -F device-encryption-subca-profile.yaml \
    -N "Device Encryption CA Profile" -T SUB
signing-tool -n test.ini profile add \
    -F device-encryption-leaf-profile.yaml \
    -N "Device Encryption Leaf" -T END
```

### Sub CA and its issuing operation

The Sub CA **Device Encryption CA** (Sub CA profile, ECDSA P-256) and its
operation **Issue device encryption certificate** (leaf profile) are created
the same way as the IDevID Issuing CA in
[Trusted device identities](https://docs.laavat.io/solutions/trusted-device-identities/#creating-the-pki-in-the-console).
The operation is blanket-approved for the factory group, so the factory station
gets certificates without waiting for an approver.

## At the factory: key in OP-TEE, certificate from LAAVAT

For each device, the factory station has OP-TEE generate the key through its
PKCS#11 library (`libckteec`), creates a CSR with that key, and sends the CSR
to the Sub CA. The private key never leaves OP-TEE; only the CSR does. The key
type used here is EC P-256.

```bash
# on the device: EC P-256 key generated inside OP-TEE
pkcs11-tool --module /usr/lib/libckteec.so.0 \
    --token-label <token> --login --pin <user-pin> \
    --keypairgen --key-type EC:prime256v1 --id 01 --label enc

# CSR signed by that key, through the OpenSSL PKCS#11 engine
openssl req -new -engine pkcs11 -keyform engine \
    -key "pkcs11:token=<token>;object=enc;type=private" \
    -subj "/CN=<device-serial>" -outform DER -out device.csr.der
```

The station submits the CSR to the Sub CA (`<ca-uuid>`) and its issuing
operation (`<op-uuid>`) and polls the request until it is ready (state 16). The
response carries the certificate and its chain, base64-encoded. The REST calls
are described in more detail in
[Trusted device identities](https://docs.laavat.io/solutions/trusted-device-identities/#submission-over-rest).

```bash
curl -sS -X POST \
    "https://app.laavat.io/<tenant>/api/v1/cas/<ca-uuid>/certificaterequests" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d "{\"csr\":\"$(base64 -w0 device.csr.der)\",\"operationId\":\"<op-uuid>\"}"
{"id": "<request-uuid>", ...}

curl -sS \
    "https://app.laavat.io/<tenant>/api/v1/certificaterequests/<request-uuid>" \
    -H "Authorization: Bearer $TOKEN" \
    | jq -r .certificate | base64 -d > device-enc.crt
```

The certificate goes onto the device, for example as
`/etc/rauc/device-enc.crt`, and into the manufacturer's device registry: the
registry is where the recipient list of each rollout comes from.

## Building an encrypted bundle for a rollout

A bundle is requested from the product's `RaucBundleSigningWithOneTimeKey`
operation with the bundle content, a manifest with `format=crypt`, and a
`certs.pem` with the certificates of the devices in the rollout, concatenated.
The platform signs the bundle and encrypts it to those certificates. The
request format is documented in
[RAUC signing](https://docs.laavat.io/usage/signing-encryption/raucsigning/#crypt-bundling).

```ini
# request.json
{ "format": "crypt", "directory": "data", "encryptionCertsFile": "certs.pem" }

# data/manifest.raucm
[bundle]
format=crypt

[update]
compatible=gateway-v1
version=1.4.0

[image.rootfs]
filename=rootfs.img
```

```bash
# the certificates of the devices in this rollout, from the device registry
cat device-0001.crt device-0002.crt device-0003.crt > certs.pem
tar -czf crypt-request.tar.gz request.json certs.pem data

signing-tool -n test.ini imagesigning add RaucBundleSigningWithOneTimeKey \
    -P <product-uuid> --operid <rauc-operation-uuid> \
    -N "gateway-1.4.0-wave1" -D "rollout wave 1, 3 devices" \
    -F crypt-request.tar.gz
Request sent. Request ID: <request-uuid>, state: Created

signing-tool -n test.ini imagesigning get -I <request-uuid> \
    --wait --skipBase64 -O update.raucb
```

The image needs a file extension RAUC knows (`.img`, `.ext4`, `.squashfs`, …)
or a `type=` entry; `type=` needs RAUC 1.15 or later on the device.

## On the device

RAUC finds the key and the certificate in `system.conf`. The certificate tells
OpenSSL which recipient entry belongs to this device.

```ini
# /etc/rauc/system.conf (excerpt)
[keyring]
path=/etc/rauc/rauc-ca-chain.pem

[encryption]
key=pkcs11:token=<token>;object=enc;type=private
cert=/etc/rauc/device-enc.crt
```

```bash
# the PKCS#11 library and PIN for RAUC, e.g. in the service's environment
RAUC_PKCS11_MODULE=/usr/lib/libckteec.so.0
RAUC_PKCS11_PIN=<user-pin>
```

`rauc install update.raucb` then verifies the signature against the keyring,
unwraps the bundle's payload key with the key in OP-TEE, and decrypts the
payload. A device whose certificate is not in the bundle stops with
`Failed to decrypt CMS EnvelopedData`.

Before a release, the recipient list can be checked on a lab device that is
one of the recipients:

```text
rauc info --dump-recipients update.raucb
  0   Issuer:    C = FI, O = Manufacturer, CN = Device Encryption CA
  1   Issuer:    C = FI, O = Manufacturer, CN = Device Encryption CA
  2   Issuer:    C = FI, O = Manufacturer, CN = Device Encryption CA
```

## Limits

- **OpenSSL on the device.** Decrypting with an EC key held in a PKCS#11 token
  needs OpenSSL 3.0.15 or later (3.1.7, 3.2.3 and 3.3.2 on those branches).
  Older releases fail with `kdf parameter error`
  ([openssl/openssl#24698](https://github.com/openssl/openssl/issues/24698)).
  With an RSA key, any OpenSSL 3 works; the RSA variant costs about twice the
  space per recipient.
- **Recipients per bundle.** The recipient list sits in the bundle's signature
  block, which RAUC caps at 64 KiB by default. That fits about 300 P-256
  recipients. RAUC 1.14 or later on the device raises the cap with
  `[system] max-bundle-signature-size`; otherwise a large fleet is served in
  batches, one bundle per batch.
- **One bundle per recipient list.** A crypt bundle cannot be re-encrypted for
  other devices; a changed list means a new bundle. Leaving a device out
  affects only the bundles built after that; it can still read the ones it
  already received.
- **A crypt request needs its recipients.** The platform does not build a
  signed but unencrypted crypt bundle for later encryption.
- **The platform sees the image.** It builds the bundle from the uploaded
  content. If the image must never leave the build host, encrypt it there and
  have the platform wrap only its key, as in the
  [Raspberry Pi design](https://docs.laavat.io/solutions/cra-ready-raspberry-pi/).

## Mapping to the CRA

| Annex I requirement | Covered by |
| --- | --- |
| Confidentiality of data, including data at rest | Update payloads encrypted; only the devices of a rollout can decrypt them, each with a key that never leaves OP-TEE |
| Integrity of the software is protected; unauthorised modification is detected | Every bundle signed under the product's RAUC CA and verified on the device before installation |
| Security updates are provided and can be installed securely | Signed, encrypted bundles addressed to the devices that should receive them |
| Keys are protected; access is controlled and logged | CA and signing keys in the HSM; device keys in OP-TEE; certificate issuance limited to the factory group; audit trail on the platform |

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.

## Related

- [RAUC signing](https://docs.laavat.io/usage/signing-encryption/raucsigning/) — the request
  formats, including `crypt` bundles
- [RAUC signing example flow](https://docs.laavat.io/exampleflows/raucsigning/) — a signed
  bundle, from product creation to verification
- [Trusted device identities](https://docs.laavat.io/solutions/trusted-device-identities/) — the identity CA
  this hierarchy is kept apart from, and the console stages for a Sub CA
- [CRA-ready i.MX8M gateways](https://docs.laavat.io/solutions/cra-ready-imx8m/) — the RAUC signing PKI and
  the device keyring
- [CRA-ready Raspberry Pi gateways](https://docs.laavat.io/solutions/cra-ready-raspberry-pi/) — encrypted
  updates where the image never leaves the build host

## References

- RAUC:
  [bundle encryption](https://rauc.readthedocs.io/en/latest/advanced.html#bundle-encryption)
  and the
  [system configuration file](https://rauc.readthedocs.io/en/latest/reference.html#system-configuration-file)
- OP-TEE:
  [PKCS#11 (libckteec) integration](https://optee.readthedocs.io/en/latest/building/userland_integration.html)
- LAAVAT blog:
  [Encrypted firmware updates](https://www.laavat.io/post/encrypted-firmware-updates)
  — why a key per device
- [Regulation (EU) 2024/2847, Cyber Resilience Act](https://eur-lex.europa.eu/eli/reg/2024/2847/oj),
  Annex I
