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.
Related¶
- RAUC signing — the request
formats, including
cryptbundles - RAUC signing example flow — a signed bundle, from product creation to verification
- 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 — the RAUC signing PKI and the device keyring
- CRA-ready Raspberry Pi gateways — encrypted updates where the image never leaves the build host
References¶
- RAUC: bundle encryption and the system configuration file
- OP-TEE: PKCS#11 (libckteec) integration
- LAAVAT blog: Encrypted firmware updates — why a key per device
- Regulation (EU) 2024/2847, Cyber Resilience Act, Annex I