Skip to content

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 on the LAAVAT blog.

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.

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.

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.

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.

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

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

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

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.

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

# /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
# 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:

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). 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.

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 sets out which requirements the platform supports and which remain yours.

References