Skip to content

AES Image Encryption

The EncryptImageWithAES operation encrypts an arbitrary image payload with one of eight supported AES modes. It covers the CBC variants and AES-GCM (authenticated encryption) plus optional Additional Authenticated Data (AAD) binding.

The operation is exposed through the standard imagesigning flow:

  1. The reference client uploads the plaintext payload to the platform's S3 staging bucket via a presigned URL.
  2. It then issues an imagesigning add EncryptImageWithAES request that references the upload, the target product operation, the chosen aesMode, and (optionally) aad.
  3. After approval, the platform encrypts the payload inside the HSM-backed crypto service. The IV used by CBC modes is the product's KEK-IV — a 16-byte value generated by the platform at product creation time and stored as a product config item. Callers do not supply it.
  4. The client polls and downloads the resulting ciphertext, plus any unwrapping material the chosen mode produces in service_provided_parameters (sealed key, sealed-key IV, IV, AAD).

Prerequisites

  • A product that has been created with an operation of type EncryptImageWithAES. The operation is keyed by an AES symmetric key generated and held inside the HSM at product creation. See the Product Configuration Guide for how to add the operation to a product.
  • The product / operation IDs.
  • A user token and, when the operation is configured to require approval, an approver token. See the Authentication chapter.
  • For CBC modes, the device or downstream pipeline that decrypts the result must know the product's KEK-IV. It is retrieved with a registered ProductionPC client through the product secrets endpoint (signing-tool secrets get) — the value is returned alongside SRKHash / SRKTable as KEKIV (also surfaced in the product config items as KEK-IVCustomEncryption). See Clients for the registration and retrieval flow. The platform reuses the same KEK-IV across every EncryptImageWithAES request for the product, so this fetch only needs to happen once per provisioned device class.

Supported AES modes

The aesMode field in the request determines which algorithm the platform uses, the key length, and whether the data key is wrapped (PSKEK) or used directly. If the field is omitted, the platform falls back to AES-CBC-256-PSKEK.

aesMode Algorithm Key length Authenticated Per-Session KEK? When to use
AES-CBC-128 AES-128-CBC 128 bit No No Compatibility with legacy bootloaders that decrypt directly with the product key.
AES-CBC-256 AES-256-CBC 256 bit No No Same as above with a 256-bit key.
AES-CBC-128-PSKEK AES-128-CBC + KW 128 bit No Yes Each request uses a fresh data key; the data key is wrapped with the product KEK and returned as sealed_key.
AES-CBC-256-PSKEK AES-256-CBC + KW 256 bit No Yes Default. Unique-per-payload key reduces blast radius of a leaked ciphertext.
AES-GCM-128 AES-128-GCM 128 bit Yes No Authenticated encryption with a long-lived product key (e.g. firmware sealed to a single device class).
AES-GCM-256 AES-256-GCM 256 bit Yes No Same as above with a 256-bit key.
AES-GCM-128-PSKEK AES-128-GCM + KW 128 bit Yes Yes Authenticated encryption with a unique-per-request data key. AAD covers the outer payload only — see AAD and PSKEK below.
AES-GCM-256-PSKEK AES-256-GCM + KW 256 bit Yes Yes Recommended default for new firmware encryption flows.

PSKEK explained

A *-PSKEK mode generates a fresh AES data key per request, encrypts the payload with that data key, then wraps the data key with the product's KEK (the long-lived key in the HSM). For CBC-PSKEK the wrap is AES-CBC under the product's KEK-IV; for GCM-PSKEK the wrap is AES-GCM with no AAD on the inner wrap (caller AAD only binds the outer payload — see AAD and PSKEK). The platform returns the wrapped key in sealed_key so the device can recover the data key with its KEK and then decrypt the payload. Non-PSKEK modes encrypt the payload directly with the product key, so no sealed_key is returned and the device must already share that product key.

IV / nonce handling

Callers do not supply IVs or nonces to EncryptImageWithAES. The CBC and GCM modes get them from different sources, and the response fields that surface them mean different things in each case — read on before wiring up a decrypter.

CBC modes (AES-CBC-*, AES-CBC-*-PSKEK)

  • The IV is 16 bytes (the AES block size) and comes from the product's KEK-IV, generated by the platform at product creation and persisted as the KEK-IVCustomEncryption product config item. It is the same value for every EncryptImageWithAES request issued against this product.
  • The KEK-IV is fetched out of the platform with a ProductionPC-typed client via signing-tool secrets get (returned as the KEKIV field in the decrypted product secrets). Devices need it to decrypt the result.
  • For AES-CBC-*-PSKEK the platform additionally generates a random per-request 16-byte payload IV. The KEK-IV is used only to wrap the ephemeral data key under the product KEK; the payload itself is encrypted with the ephemeral data key under the random per-request IV.
    • The top-level IV in service_provided_parameters is the product KEK-IV (used to wrap the data key).
    • The sealed_key_iv field is the per-request payload IV (used to encrypt the payload with the ephemeral data key).
    • Devices need both: IV to unwrap sealed_key against the product KEK, then sealed_key_iv to decrypt the payload with the unwrapped data key.

Same KEK-IV across all CBC encryptions

For the non-PSKEK CBC modes (AES-CBC-128, AES-CBC-256) the payload is encrypted directly with the product key under the product KEK-IV. Because both are constant per product, encrypting the same plaintext twice produces the same ciphertext (and identical-prefix plaintexts produce identical-prefix ciphertexts). This is acceptable for one-shot firmware images, but if your threat model needs unique-per-payload ciphertexts use a *-PSKEK mode (which rotates the data key and the payload IV per request) or a GCM mode.

GCM modes (AES-GCM-*, AES-GCM-*-PSKEK)

  • GCM uses a 12-byte (96-bit) nonce. This is the only nonce length AWS CloudHSM supports.
  • The platform always generates the nonce. The product KEK-IV is not used for GCM modes. When the product key is HSM-backed the nonce is generated inside the HSM (CloudHSM FIPS requirement); when the key is software-protected the nonce is drawn from crypto/rand.
  • The encrypted payload file is laid out as nonce ‖ ciphertext ‖ tag (12 + N + 16 bytes). The nonce is prepended for self-contained recovery; readers must slice it off before passing the rest to their GCM implementation.
  • The top-level IV in service_provided_parameters is the same 12-byte payload nonce, surfaced for visibility.
  • For AES-GCM-*-PSKEK the sealed_key blob is itself a self-contained nonce ‖ ciphertext ‖ tag of the wrapped data key (under the product KEK). sealed_key_iv is therefore empty for GCM-PSKEK — the wrap nonce lives inside sealed_key.

Single-shot GCM size limit

AES-GCM cannot be safely streamed through a block-cipher pipeline the way CBC can, so the platform encrypts the full payload in a single Seal call. The hard cap is 40 MB (≈ 4096 × 1000 × 10 bytes). Requests with a plaintext larger than this are rejected. Use a CBC mode for very large blobs, or split the payload into chunks that the firmware reassembles after decryption.

Additional Authenticated Data (AAD)

AAD lets you bind the ciphertext to a piece of context (a device serial number, a firmware version, an image header) without encrypting it. On decryption the device must supply the exact same AAD or the GCM tag verification fails.

  • --aad <base64> is only honoured when aesMode is one of the AES-GCM-* variants. For any CBC mode the field is ignored.
  • The value must be Base64-encoded raw bytes. There is no length limit imposed by the platform; keep AAD small enough that the device firmware can reproduce it byte-for-byte at decryption time.
  • If --aad is omitted in a GCM mode, the platform encrypts with empty AAD (zero bytes). Decryption then also requires empty AAD.

AAD and PSKEK

For the AES-GCM-*-PSKEK modes the AAD is bound only to the outer payload. The PSKEK-wrapped inner data key is not covered by the AAD — the data key wrap is a separate step performed by the HSM. In practice this means:

  • An attacker who can substitute a different sealed_key cannot do so silently: decrypting the payload with the wrong data key will still cause GCM tag verification to fail.
  • AAD is not a substitute for transport integrity over the sealed_key field — keep delivering both in the same signed manifest the device validates.

Reference client usage

The reference client subcommand is imagesigning add EncryptImageWithAES:

(venv) $ signing-tool imagesigning add EncryptImageWithAES -h
usage: signing-tool imagesigning add EncryptImageWithAES [-h] -P P --operid OPERID -N N -D D -F F -M M [--aad AAD]

options:
  -h, --help     show this help message and exit
  -P P           Product ID
  --operid OPERID  Product operation ID
  -N N           Request name
  -D D           Request description
  -F F           File to encrypt
  -M M           Encryption mode, (AES-CBC-128, AES-CBC-256, AES-CBC-128-PSKEK, AES-CBC-256-PSKEK,
                 AES-GCM-128, AES-GCM-256, AES-GCM-128-PSKEK, AES-GCM-256-PSKEK)
  --aad AAD      Additional Authenticated Data, base64 encoded (GCM modes only)

The reference client does not expose an --iv flag for EncryptImageWithAES. The CBC IV is the product's KEK-IV managed by the platform (see IV / nonce handling); the GCM nonce is generated by the platform on every request. The older EncryptImageWithAESEscrowCustom operation still has --iv, but it should not be used for new flows.

CBC with PSKEK (default)

(venv) $ signing-tool -c -t $TOKEN -a https://app.laavat.io/<CustomerName>/api/v1 \
    imagesigning add EncryptImageWithAES \
    -P b508d600-977a-46f4-9070-5cf56646bae1 \
    --operid 8736e553-d8d3-442a-a56b-4d243cf22585 \
    -N firmware-v1.6.44 -D "encrypt firmware build 1.6.44" \
    -F firmware.bin -M AES-CBC-256-PSKEK

# Approve if the operation requires it
(venv) $ signing-tool -c -t $APPROVERTOKEN -a https://app.laavat.io/<CustomerName>/api/v1 \
    imagesigning approve -I <request-id>

# Download the ciphertext + sealed key material
(venv) $ signing-tool -c -t $TOKEN -a https://app.laavat.io/<CustomerName>/api/v1 \
    imagesigning get -I <request-id> -O firmware.bin.enc

GCM without PSKEK, with AAD

In this example the firmware version string (v1.6.44) is bound to the ciphertext as AAD. The device must supply the exact same bytes when it decrypts, otherwise the GCM tag check fails.

(venv) $ AAD_B64=$(printf 'v1.6.44' | base64)

(venv) $ signing-tool -c -t $TOKEN -a https://app.laavat.io/<CustomerName>/api/v1 \
    imagesigning add EncryptImageWithAES \
    -P b508d600-977a-46f4-9070-5cf56646bae1 \
    --operid 8736e553-d8d3-442a-a56b-4d243cf22585 \
    -N firmware-v1.6.44-gcm -D "GCM-256, AAD=version" \
    -F firmware.bin -M AES-GCM-256 \
    --aad "$AAD_B64"

GCM with PSKEK and AAD

(venv) $ signing-tool -c -t $TOKEN -a https://app.laavat.io/<CustomerName>/api/v1 \
    imagesigning add EncryptImageWithAES \
    -P b508d600-977a-46f4-9070-5cf56646bae1 \
    --operid 8736e553-d8d3-442a-a56b-4d243cf22585 \
    -N firmware-v1.6.44-gcm-pskek -D "GCM-256-PSKEK with header AAD" \
    -F firmware.bin -M AES-GCM-256-PSKEK \
    --aad "$AAD_B64"

Fetching the product KEK-IV (for CBC decryption)

For any of the CBC modes, the device or downstream pipeline that decrypts the result needs the product's KEK-IV. Fetch it once with a registered ProductionPC client:

(venv) $ signing-tool -c -t $TOKEN \
    -a https://app.laavat.io/<CustomerName>/api/v1/ secrets get \
    -P b508d600-977a-46f4-9070-5cf56646bae1 \
    -C client.private -O /tmp/prod.json

(venv) $ jq -r '.productConfigItems[] | select(.name=="KEK-IVCustomEncryption") | .value' < /tmp/prod.json
ZX75HIzTir2AsAst0BbCMw==

This is the same value that arrives back as the top-level IV in the encryption response. See Clients for the full registration flow.

Response payload

The download endpoint returns the ciphertext through a presigned S3 URL plus the unwrapping material in payload.service_provided_parameters. The exact set of fields depends on the mode:

Mode family IV (top level) — meaning sealed_key sealed_key_iv Encrypted file layout
AES-CBC-* Product KEK-IV used to encrypt the payload (16 B; constant per product) -- -- ciphertext (PKCS#7-padded)
AES-CBC-*-PSKEK Product KEK-IV used to wrap the data key with the product KEK (16 B) data key wrapped under KEK (CBC, 16 / 32 B) Per-request 16 B payload IV ciphertext (PKCS#7-padded)
AES-GCM-* 12 B payload nonce (informational; same as the prefix of the file) -- -- nonce ‖ ciphertext ‖ tag (12 + N + 16 B)
AES-GCM-*-PSKEK 12 B payload nonce (informational; same as the prefix of the file) data key wrapped under KEK as nonce ‖ ct ‖ tag empty (the wrap nonce lives inside sealed_key) nonce ‖ ciphertext ‖ tag (12 + N + 16 B)

For the GCM variants the encrypted file is self-contained: the 12-byte nonce is prepended to the ciphertext and the 16-byte authentication tag is appended. Readers should split the file as nonce = file[:12], tag = file[len-16:], ct = file[12:len-16] before calling their GCM implementation.

A truncated example response for an AES-CBC-256-PSKEK request is:

{
    "id": "22e2e069-335c-4e8d-8706-675d06723ef5",
    "id_product": "b508d600-977a-46f4-9070-5cf56646bae1",
    "id_product_operation": "8736e553-d8d3-442a-a56b-4d243cf22585",
    "name": "firmware-v1.6.44",
    "payload": {
        "id": "6ea19a17-284a-4170-84e4-ea0368330424",
        "name": "firmware.bin",
        "s3_url": "<presigned-S3-URL-for-the-ciphertext>",
        "service_provided_parameters": [
            {
                "aes_mode": "AES-CBC-256-PSKEK"
            },
            {
                "sealed_key": "NhgPZDXsZyG89hhPUAOAYw=="
            }
        ]
    },
    "state": 16
}

For an AES-GCM-256-PSKEK request the same response carries a self-contained sealed key blob and an empty sealed_key_iv:

{
    "payload": {
        "service_provided_parameters": [
            {
                "aes_mode": "AES-GCM-256-PSKEK"
            },
            {
                "sealed_key": "<base64 of 12-byte nonce ‖ wrapped-key ciphertext ‖ 16-byte tag>"
            }
        ]
    },
    "state": 16
}

The full set of optional response fields is documented in the Encryption response payload reference page and in the API specification under OptionalParameters.

Audit trail

Each successful EncryptImageWithAES run produces an OperationCompleted audit event tagged with the product ID and the matching productOperationType. See Audit Events for the field schema.

Comparison with EncryptImageWithAESEscrowCustom

EncryptImageWithAESEscrowCustom predates EncryptImageWithAES and is limited to the four AES-CBC-* modes. It is still available for backwards compatibility, but new product operations should use EncryptImageWithAES so that GCM modes and AAD binding are available without re-creating the product.