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

# Signatures with digest signing

One kind of operation, `DigestSigning`, covers most of what a product signs:
ESP32 and other firmware, kernel modules, tokens, attestations and file
signatures. The key stays in the HSM, and each result is checked with the
verifier the device or service uses.

The Cyber Resilience Act's Annex I requires that a product protects the
integrity of its software and that unauthorised changes are detected. Much of
what a device checks is not a format the platform builds itself, and for all of
it the pattern is the same: the build hashes what it ships, the platform signs
the hash under a recorded, group-restricted operation, and the device's own
verifier checks it.

> **Note: How to read this**
> The [digest signing](https://docs.laavat.io/usage/signing-encryption/digest-signing/) page is
> the reference: every option, preset and error message. This page starts
> from the use case instead. Each one says what the device checks, gives the
> signing command, and shows how to verify the result the way the device
> will. Where `signing-tool` has an envelope for a format (kernel modules,
> JWS, DSSE, IMA), the page uses it; elsewhere it sends the digest with
> `imagesigning add` and the format's own tool assembles the result. SWUpdate
> and Mender have pages of their own.
> 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).

**Terms used here.** A *product* holds the keys for one device line. Each
*operation* in it is one key: its type says what the key may do (here mostly
`DigestSigning`, which signs a digest you send), and its *approval rule* says
who may ask for a signature and who approves. A *request* is one signing call;
`signing-tool` submits it and downloads the result. See the
[product guide](https://docs.laavat.io/usage/productguide/product/).

## Find your use case

| Use case | The device or verifier checks | What the platform returns | Certificate needed |
| --- | --- | --- | --- |
| [ESP32 Secure Boot V2](https://docs.laavat.io/solutions/cra-ready-digest-signing/#esp32-secure-boot-v2) | The ROM bootloader checks the bootloader, the bootloader checks the app, against a key digest in eFuse | A raw signature; `espsecure` attaches it | No |
| [Firmware for your own bootloader](https://docs.laavat.io/solutions/cra-ready-digest-signing/#firmware-for-your-own-bootloader) | A signature in the image header against a key in fuses or flash | A raw signature | No |
| [Linux kernel modules](https://docs.laavat.io/solutions/cra-ready-digest-signing/#linux-kernel-modules) | The kernel checks a module's appended signature against its trusted keys | The signed module | Yes |
| [File integrity at run time](https://docs.laavat.io/solutions/cra-ready-digest-signing/#file-integrity-at-run-time) | Linux IMA checks each file's signature before it runs or is read | One signature per file | Yes |
| [UEFI Secure Boot variables](https://docs.laavat.io/solutions/cra-ready-digest-signing/#uefi-secure-boot-variables) | The firmware checks a `db` update against the KEK | A PKCS #7 for `db.auth` | Yes |
| [Signed tokens and configuration](https://docs.laavat.io/solutions/cra-ready-digest-signing/#signed-tokens-and-configuration) | A device or service checks a signed file against your public key | A JWS | No |
| [Supply-chain attestations](https://docs.laavat.io/solutions/cra-ready-digest-signing/#supply-chain-attestations) | A customer or auditor checks an in-toto statement | A DSSE envelope | No |
| [Post-quantum signatures](https://docs.laavat.io/solutions/cra-ready-digest-signing/#post-quantum-signatures-with-ml-dsa) | An ML-DSA signature, raw or in a CMS | A raw signature or a CMS | For kernel modules |
| Container images | cosign or skopeo against the operation's public key | See [OCI signing](https://docs.laavat.io/exampleflows/ocisigning/) | — |
| Over-the-air updates | SWUpdate or Mender checks the update before it installs | See [SWUpdate](https://docs.laavat.io/solutions/cra-ready-swupdate/) and [Mender](https://docs.laavat.io/solutions/cra-ready-mender-ota/) | — |

Formats with an operation of their own are signed there:
[RAUC bundles](https://docs.laavat.io/usage/signing-encryption/raucsigning/),
[MCUboot images](https://docs.laavat.io/usage/signing-encryption/mcuboot-signing/),
[NXP HAB](https://docs.laavat.io/exampleflows/habsigning/) and
[AHAB](https://docs.laavat.io/exampleflows/ahabsigning/).

## The LAAVAT product

Use one `DigestSigning` operation per key, and separate operations for keys that
guard different things, so each can have its own approval rule: an image key
every device trusts should need an approval for every request, while a key for
build attestations can sign unattended.

```json
{
  "name": "Gateway release signing",
  "description": "Digest signing: firmware, Linux, tokens, attestations, UEFI and post-quantum keys",
  "productType": "Production",
  "enabled": true,
  "caInfo": [], "rndKeys": [],
  "productOperations": [
    { "name": "Firmware RSA", "description": "ESP32 RSA chips, RSA bootloaders", "operationType": "DigestSigning",
      "token": { "name": "firmware RSA 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": [] } },
    { "name": "Firmware EC", "description": "ECDSA-only ESP32 chips, raw r and s bootloaders", "operationType": "DigestSigning",
      "token": { "name": "firmware EC key", "description": "EC P-256", "keyType": "ECDSAP256" },
      "approvalRule": { "name": "Release", "description": "Release group requests, approver group approves",
                        "allowedGroups": ["<release-group-uuid>"], "approvalGroups": ["<approver-group-uuid>"], "blanketGroups": [] } },
    { "name": "Linux runtime", "description": "Kernel modules and IMA", "operationType": "DigestSigning",
      "token": { "name": "Linux 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": [] } },
    { "name": "Tokens and configuration", "description": "Licences and configuration devices accept", "operationType": "DigestSigning",
      "token": { "name": "token key", "description": "EC P-256", "keyType": "ECDSAP256" },
      "approvalRule": { "name": "Release", "description": "Release group requests, approver group approves",
                        "allowedGroups": ["<release-group-uuid>"], "approvalGroups": ["<approver-group-uuid>"], "blanketGroups": [] } },
    { "name": "Build attestations", "description": "in-toto statements, signed unattended", "operationType": "DigestSigning",
      "token": { "name": "attestation key", "description": "EC P-256", "keyType": "ECDSAP256" },
      "approvalRule": { "name": "Pipeline", "description": "The build pipeline signs unattended",
                        "allowedGroups": ["<pipeline-group-uuid>"], "approvalGroups": ["<pipeline-group-uuid>"], "blanketGroups": ["<pipeline-group-uuid>"] } },
    { "name": "UEFI KEK", "description": "RSA-2048, the common KEK size; signs db updates", "operationType": "DigestSigning",
      "token": { "name": "KEK", "description": "RSA-2048", "keyType": "RSA2048" },
      "approvalRule": { "name": "Platform keys", "description": "Platform security group requests, approver group approves",
                        "allowedGroups": ["<platform-security-group-uuid>"], "approvalGroups": ["<approver-group-uuid>"], "blanketGroups": [] } },
    { "name": "Post-quantum", "description": "ML-DSA-65", "operationType": "DigestSigning",
      "token": { "name": "ML-DSA key", "description": "ML-DSA-65", "keyType": "MLDSA65" },
      "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 release-signing.json
# the new product is approved in the LAAVAT console before use
```

**Approval rules.** Each operation's rule decides who may sign with its key. 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. In the example, every operation needs an approval for each
request except build attestations, which list the pipeline group as a blanket
group so the build, such as a Jenkins job, can sign unattended; keep such groups
narrow, since they skip the human check. 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/).

## Before you sign

### The operation's public key

Every Verify step, and `espsecure` for ESP32, needs the public key of the
operation that signed. Export it once per operation; on this page it is called
`op.pub`:

```bash
signing-tool -n test.ini product getpubkey -P <product-uuid> --operid <operation-uuid> -O op.pub
```

### What a request sends

In every request, `-p "$(openssl dgst -sha256 -binary FILE | base64 -w0)"` is the
SHA-256 of what you sign, base64-encoded on one line; `-H` names the hash. That
digest is all that reaches LAAVAT; the file stays on the build host. The envelopes
(`imagesigning envelope …`) compute the digests themselves. The one exception is
raw ML-DSA, which signs the message itself (see
[Post-quantum signatures](https://docs.laavat.io/solutions/cra-ready-digest-signing/#post-quantum-signatures-with-ml-dsa)).

### A certificate, for kernel modules, IMA and UEFI

These verifiers trust a certificate for the signing key, not the bare key. On this
page `signer.pem` is that certificate, issued by your CA for the operation's public
key `op.pub`, and `ca-chain.pem` holds the CA certificates above it: the issuing CA
and the root.

If your CA issues certificates only from a CSR, kernel modules and UEFI can use a
certificate the platform holds instead: see
[Certificate-based signing](https://docs.laavat.io/solutions/cra-ready-certificate-signing/).
IMA has no such route yet, because its envelope needs this operation's key.

For example, with OpenSSL:

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

What the commands do:

- `signer.ext` lists what the certificate allows: it is a signing certificate, not
  a CA, its key may make signatures, and it carries the identifiers verifiers use
  to match it to its key and to its CA (`subjectKeyIdentifier`,
  `authorityKeyIdentifier`). The `extendedKeyUsage` line matters only for SWUpdate,
  which checks it; these verifiers do not.
- `openssl x509 -new -force_pubkey op.pub` creates the certificate for the
  operation's public key (the private key stays in the HSM) and signs it with your
  issuing CA (`-CA`, `-CAkey`), valid for two years.
- `cat` writes `ca-chain.pem`: the issuing CA and the root.

## ESP32 Secure Boot V2

Use case: the ROM bootloader checks the second-stage bootloader, and the bootloader
checks the application, each against a public key whose digest is burned into
eFuse. An image without a valid signature block does not run.

The key type depends on the chip (see the table below). Two images are signed, the
second-stage bootloader and the application, each in the same way and with the same
operation:

- Build them unsigned, with `CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES` turned off.
  ESP-IDF then pads each image the way Secure Boot V2 requires, and that padded
  image is what gets hashed and signed.
- Sign its hash with LAAVAT, and attach the signature with `espsecure`, as below.

| Chip | Secure Boot V2 accepts | Operation key and settings |
| --- | --- | --- |
| ESP32 (v3.0 and later), ESP32-S2, ESP32-S3, ESP32-C3 | RSA-3072 | `RSA3072`, `-H SHA256 --padding PSS` |
| ESP32-C6 | RSA-3072 or ECDSA P-256 | As above, or `ECDSAP256`, `-H SHA256` |
| ESP32-H2, ESP32-C5, ESP32-P4 | RSA-3072. ECDSA is supported, but Espressif does not recommend it on these chips | `RSA3072`, `-H SHA256 --padding PSS` |
| ESP32-H4 | ECDSA P-256 or ECDSA P-384 | `ECDSAP256`, `-H SHA256`, or `ECDSAP384`, `-H SHA384` |
| ESP32-C2, ESP32-C61 | ECDSA P-256 | `ECDSAP256`, `-H SHA256` |

These are from
[Espressif's ESP-IDF documentation](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/security/secure-boot-v2.html);
select your chip at the top of that page and check its revision. On the ESP32-H2,
C5 and P4, Espressif marks ECDSA Secure Boot V2 as "not functional for certain input
vectors" and recommends RSA.

The commands below sign the application, `app.bin`. The bootloader,
`bootloader.bin`, is signed the same way.

```bash
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <firmware-rsa-operation-uuid> \
    -p "$(openssl dgst -sha256 -binary app.bin | base64 -w0)" -H SHA256 --padding PSS \
    -N "app 1.4.0" -D "ESP32 application 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 app.sig
espsecure sign-data --version 2 --pub-key op.pub --signature app.sig --output app-signed.bin app.bin
```

`espsecure` attaches the signature to the image, using `op.pub` (see
[Before you sign](https://docs.laavat.io/solutions/cra-ready-digest-signing/#before-you-sign)) to build the signature block. It checks the
signature before it writes `app-signed.bin`, so a signature over other bytes fails
here and not on the device. For ECDSA use the Firmware EC operation and leave out
`--padding PSS`. For P-384 (ESP32-H4) add an operation with an `ECDSAP384` key and
use `-H SHA384` and `openssl dgst -sha384`. These are the command names of esptool
v5; esptool v4 spells them `espsecure.py sign_data` and `verify_signature`.

The eFuse holds a digest of the public key, not the key. `espsecure` computes it
from `op.pub`; ESP-IDF's documentation gives the `espefuse` command that burns it on
each chip.

```bash
espsecure digest-sbv2-public-key --keyfile op.pub --output key-digest.bin
```

To verify:

```text
espsecure verify-signature --version 2 --keyfile op.pub app-signed.bin
Signature block 0 verification successful using the supplied key (RSA).
```

This is the check the ROM bootloader and the bootloader make. A changed byte in the
image gives `Signature block image digest does not match the actual image digest`.

## Firmware for your own bootloader

Use case: a bootloader checks a signature in the firmware image header before it
jumps to the firmware image. The public key is in fuses or in protected flash.

Your bootloader decides the signature format. Two common cases:

- **RSA**, for a bootloader that checks RSA-PSS: the Firmware RSA operation, first
  command below.
- **ECDSA P-256 on a small microcontroller:** many verifiers expect the signature as
  64 bytes, the two 32-byte values r and s one after the other, instead of the DER
  encoding OpenSSL produces. Use the Firmware EC operation with `--encoding RAW`, or
  `RAW_LE` if your verifier expects little-endian.

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

```bash
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <firmware-ec-operation-uuid> \
    -p "$(openssl dgst -sha256 -binary app.bin | base64 -w0)" -H SHA256 --encoding RAW \
    -N "app 1.4.0" -D "controller application 1.4.0"
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O app.sig
```

To verify:

```text
openssl dgst -sha256 -binary app.bin > app.dgst
openssl pkeyutl -verify -pubin -inkey op.pub -in app.dgst -sigfile app.sig \
    -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pss -pkeyopt rsa_pss_saltlen:-1
Signature Verified Successfully
```

This is the check an RSA-PSS bootloader makes. For the raw ECDSA signature, turn r
and s into DER first; OpenSSL then checks it the same way:

```text
python3 -c 'from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature as e; b = open("app.sig", "rb").read();
open("app.der.sig", "wb").write(e(int.from_bytes(b[:32], "big"), int.from_bytes(b[32:], "big")))'
openssl pkeyutl -verify -pubin -inkey op.pub -in app.dgst -sigfile app.der.sig -pkeyopt digest:sha256
Signature Verified Successfully
```

Then put `app.sig` where your image format expects it.

## Linux kernel modules

Use case: out-of-tree drivers, built outside the kernel tree. With module signature
enforcement, the kernel loads a module only if its appended signature verifies
against a key in its trusted keyring.

```bash
signing-tool -n test.ini imagesigning envelope kernel-module -P <product-uuid> --operid <linux-operation-uuid> \
    --state-dir .sign -F drivers/a.ko -F drivers/net/b.ko -O signed/ --cms-cert signer.pem
```

All modules go into one request with one approval. `signing-tool` hashes each
module, appends the signature in the kernel's format and writes `signed/a.ko` and
`signed/net/b.ko`; the module bodies are unchanged. `signer.pem` is the certificate
from [Before you sign](https://docs.laavat.io/solutions/cra-ready-digest-signing/#before-you-sign); `--cms-cert signer.pem` tells
`signing-tool` which certificate the kernel holds for this key: the signature names
that certificate by issuer and serial number, which is how the kernel finds the key
in its keyring. The certificate itself is not embedded. With several `-F`, `-O`
names a directory, as here; with one, it names the output file.

To verify:

```text
perl scripts/extract-module-sig.pl -0 signed/a.ko > a.unsigned.ko
perl scripts/extract-module-sig.pl -s signed/a.ko > a.p7s
openssl cms -verify -binary -inform DER -in a.p7s -content a.unsigned.ko \
    -certfile signer.pem -CAfile ca-chain.pem -purpose any -out /dev/null
CMS Verification successful
```

Both scripts come with the kernel source. `-CAfile ca-chain.pem` holds the issuing
CA as well as the root, because OpenSSL 3.0 does not use `-certfile` to build the
chain. On the target, the kernel trusts `signer.pem` when it is built in with
`CONFIG_SYSTEM_TRUSTED_KEYS` or loaded into the secondary keyring.

## File integrity at run time

Use case: refuse to run a program or library that was changed after release. Linux
IMA (Integrity Measurement Architecture) appraisal does this: before the kernel
executes or reads a file, it checks the file's signature, stored in the file's
`security.ima` extended attribute, against a certificate in the kernel's `.ima`
keyring.

```bash
signing-tool -n test.ini imagesigning envelope ima -P <product-uuid> --operid <linux-operation-uuid> \
    --state-dir .sign -F rootfs/usr/bin/app -F rootfs/usr/lib/libapp.so -O ima/
```

`signing-tool` signs every file given with `-F` in one request, with one approval,
and writes into `ima/` one signature file per input, plus `ima/ima.xattrs`. That
file sets all the `security.ima` attributes at once with `setfattr --restore` while
the root filesystem image is assembled, which needs root. The `.ima` keyring holds
`signer.pem`, the certificate from [Before you sign](https://docs.laavat.io/solutions/cra-ready-digest-signing/#before-you-sign). The kernel
accepts a certificate into `.ima` only if a key in its system trusted keyring signed
it (`CONFIG_INTEGRITY_TRUSTED_KEYRING`, on by default), so your issuing CA's
certificate is built into the kernel, for example with `CONFIG_SYSTEM_TRUSTED_KEYS`.

To verify:

```text
cp ima/bin/app.sig rootfs/usr/bin/app.sig
openssl x509 -in signer.pem -outform DER -out signer.der
evmctl ima_verify --key signer.der --sigfile rootfs/usr/bin/app
rootfs/usr/bin/app: verification is OK
```

`evmctl` runs the check the kernel makes, against the same certificate.

## UEFI Secure Boot variables

Use case: add a certificate to the UEFI `db` of machines in the field. `db` is the
list of certificates the firmware accepts boot loaders from, and it accepts a change
only if the change is signed with the KEK (key exchange key), whose certificate is
enrolled in the firmware. Here the KEK is the UEFI KEK operation's key. `KEK.pem` is
that operation's certificate, made the same way as `signer.pem` (see
[Before you sign](https://docs.laavat.io/solutions/cra-ready-digest-signing/#before-you-sign)), from that operation's public key.

```bash
cert-to-efi-sig-list -g <your-owner-guid> db.pem db.esl
sign-efi-sig-list -a -o -t "<YYYY-MM-DD HH:MM:SS>" db db.esl db.tbs
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <kek-operation-uuid> \
    -p "$(openssl dgst -sha256 -binary db.tbs | base64 -w0)" -H SHA256 \
    --format CMS --cms-preset uefi --cms-cert KEK.pem -N db -D "db update"
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O db.p7
sign-efi-sig-list -a -i db.p7 -t "<YYYY-MM-DD HH:MM:SS>" db db.esl db.auth
```

- `cert-to-efi-sig-list` wraps the new certificate `db.pem` in an EFI signature list,
  `db.esl`. The owner GUID is a fixed value you keep, which marks your entries in
  `db`.
- `sign-efi-sig-list -o` writes `db.tbs`: the exact bytes the KEK must sign.
- `signing-tool` signs them with the KEK operation, as a PKCS #7
  (`--cms-preset uefi`).
- `sign-efi-sig-list -i` puts the PKCS #7 and the list into `db.auth`, the file you
  write to the `db` variable, for example with `efi-updatevar`.

`-a` appends the certificate to `db`; without it the update replaces every entry in
`db`, such as the Microsoft UEFI CA, which can leave machines unable to boot. It is
part of the signed data, so both `sign-efi-sig-list` calls need it, with the same
`-t` timestamp. `-i` takes the PKCS #7 as downloaded, in DER: the tool copies the
file into `db.auth` byte for byte. Changing `db` on a fleet is rare and sensitive,
which is why the KEK has its own operation with an approval group.

To verify:

```text
openssl cms -verify -binary -inform DER -in db.p7 -content db.tbs \
    -certfile KEK.pem -CAfile ca-chain.pem -purpose any -out /dev/null
CMS Verification successful
```

The firmware itself checks `db.auth` against the KEK when the variable is written.

## Signed tokens and configuration

Use case: a device or a cloud service should accept a file, such as a licence, a
provisioning token or a configuration, only if you signed it. JSON Web Signature
(JWS, RFC 7515) is the common format for that: one string holding a small header
(algorithm and key name), the file's content and the signature.

```bash
signing-tool -n test.ini imagesigning envelope jws -P <product-uuid> --operid <tokens-operation-uuid> \
    --state-dir .sign -F config.json -O config.jws --alg ES256 --kid release-2026
```

`signing-tool` reads `config.json`, has the *Tokens and configuration* operation sign
it, and writes `config.jws`: one line of three parts separated by dots, a header that
names the algorithm and the key, the content of `config.json`, and the signature, each
base64url-encoded.

The options:

- `--alg ES256`: the signature algorithm, ECDSA P-256 with SHA-256, matching the
  operation's EC P-256 key. For an RSA key use `RS256` (PKCS #1 v1.5) or `PS256`
  (RSA-PSS).
- `--kid release-2026`: a name for the key, written into the header. A verifier that
  trusts several keys uses it to pick the right one.
- `--omit-payload` (optional): leaves the content out of the JWS, for when
  `config.json` travels separately; the verifier then supplies it.

To verify:

```bash
python3 -c 'from jwcrypto import jwk, jws; t = jws.JWS(); t.deserialize(open("config.jws").read());
t.verify(jwk.JWK.from_pem(open("op.pub", "rb").read()), alg="ES256"); print("JWS verified")'
```

Any JWS or JWT library does the same with `op.pub`. Pin the algorithm in the verifier
(`ES256` here), so a token cannot choose a weaker one.

## Supply-chain attestations

Use case: an in-toto statement that says what was built from what, signed so that a
customer or an auditor can check it came from your pipeline. DSSE is the envelope
format in-toto uses for the signed statement.

```bash
signing-tool -n test.ini imagesigning envelope dsse -P <product-uuid> --operid <attestation-operation-uuid> \
    --state-dir .sign -F statement.json -O statement.dsse.json \
    --payload-type application/vnd.in-toto+json --keyid release-2026
```

On an RSA key, `--padding PSS` matches in-toto and the default PKCS #1 v1.5 matches
sigstore.

To verify:

```bash
python3 -c 'import json; from cryptography.hazmat.primitives.serialization import load_pem_public_key
from securesystemslib.dsse import Envelope; from securesystemslib.signer import SSlibKey
key = SSlibKey.from_crypto(load_pem_public_key(open("op.pub", "rb").read()), keyid="release-2026")
Envelope.from_dict(json.load(open("statement.dsse.json"))).verify([key], 1); print("DSSE verified")'
```

Signing container images and verifying them with cosign is covered in
[OCI signing](https://docs.laavat.io/exampleflows/ocisigning/).

## Post-quantum signatures with ML-DSA

ML-DSA is the post-quantum signature standard (NIST FIPS 204). The platform holds
ML-DSA keys in all three parameter sets, so a product can start signing with them now,
next to its RSA or EC keys, for verifiers that are ready.

| Key type | Signature | Public key | Strength |
| --- | --- | --- | --- |
| `MLDSA44` | 2,420 bytes | 1,312 bytes | NIST category 2 |
| `MLDSA65` | 3,309 bytes | 1,952 bytes | NIST category 3 |
| `MLDSA87` | 4,627 bytes | 2,592 bytes | NIST category 5 |

Plan header and keyring space for them: an ML-DSA-65 signature is almost nine times an
RSA-3072 one.

### Two ways to sign, and what leaves the build host

| Form | The platform receives | Use it for |
| --- | --- | --- |
| Raw ML-DSA, `-H NONE` | The message itself: ML-DSA signs the message, not a hash | Short messages: a manifest, a token, a hash list your verifier reads |
| ML-DSA in a CMS, `--format CMS -H SHA512` | Only a SHA-512 digest; the key signs the CMS signed attributes | Large artifacts, and kernel modules (`envelope kernel-module`) |

The raw form sends the message itself, so it is not digest signing in the usual sense:
what you sign leaves the build host. The HSM signs it in one call, and accepts at most
13,504 bytes. For anything larger, or anything that should stay on the build host, use
the CMS form, or sign a small manifest that lists the image's hash.

In the first example below, the message is a small `manifest.json` that lists the
image's SHA-512: the manifest leaves the build host, the image does not.

```bash
# raw ML-DSA over a manifest
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <mldsa-operation-uuid> \
    -p "$(base64 -w0 manifest.json)" -H NONE -N "manifest 1.4.0" -D "ML-DSA manifest"
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O manifest.sig

# ML-DSA kernel module: only the module's SHA-512 is sent
signing-tool -n test.ini imagesigning envelope kernel-module -P <product-uuid> --operid <mldsa-operation-uuid> \
    --state-dir .sign -F my_module.ko -O my_module.signed.ko -N "my_module 1.4.0" -D "ML-DSA kernel module"
```

The signature names its key by subject key identifier, so the certificate the kernel
holds for it needs `subjectKeyIdentifier=hash`, and `--cms-cert` is not needed. The
kernel accepts ML-DSA module signatures from Linux 7.0, and only when built with
`CONFIG_PKCS7_WAIVE_AUTHATTRS_REJECTION_FOR_MLDSA=y`, which is not set unless you
enable it. The OpenSSL check below cannot show whether your kernel is built that way.

To verify:

```bash
# OpenSSL 3.5 or later knows ML-DSA
openssl pkeyutl -verify -pubin -inkey op.pub -rawin -in manifest.json -sigfile manifest.sig
perl scripts/extract-module-sig.pl -0 my_module.signed.ko > my_module.unsigned.ko
perl scripts/extract-module-sig.pl -s my_module.signed.ko > my_module.p7s
openssl cms -verify -binary -inform DER -in my_module.p7s -content my_module.unsigned.ko \
    -certfile signer-mldsa.pem -CAfile ca-chain.pem -purpose any -out /dev/null
```

`signer-mldsa.pem` is the certificate for the post-quantum operation's public key,
issued like `signer.pem` (with OpenSSL, version 3.5 or later). The device-side verifier
needs ML-DSA support of its own: check your bootloader, kernel or library before
switching a fleet.

## Where off-hardware verification stops

| Use case | Checked on the build host | Not tested: needs the target |
| --- | --- | --- |
| ESP32 | RSA-3072 PSS, ECDSA P-256 and P-384 attached and verified with `espsecure`; a changed byte refused. A test image, not an ESP-IDF build | The ROM bootloader and the bootloader, with the key digest in eFuse |
| Other firmware | Every key type, hash and encoding with `openssl pkeyutl` | Your bootloader reading it from the image header |
| Kernel modules | Two modules in one request, extracted with the kernel's script, CMS verified | The kernel loading the module; for ML-DSA, a Linux 7.0 kernel built with the option above |
| Tokens | ES256, ES384, RS256, PS256 and a detached JWS with jwcrypto | Nothing: the verifier is software |
| Attestations | EC, RSA-PSS and RSA PKCS #1 v1.5 with securesystemslib and cosign | Nothing: the verifier is software |
| IMA | Two files with `evmctl ima_verify` | `setfattr --restore` (needs root) and kernel appraisal |
| UEFI | `sign-efi-sig-list -a -i` took the platform's DER PKCS #7 and wrote it into `db.auth` byte for byte; the PKCS #7 verifies over `db.tbs` | The firmware writing the variable |
| Post-quantum | ML-DSA-44, 65 and 87 over a message; ML-DSA CMS; an ML-DSA kernel module from `envelope kernel-module`, all verified with OpenSSL 3.6 | A device-side ML-DSA verifier |

## 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 | Boot ROMs, bootloaders, the kernel, IMA and UEFI check every image, module, file and variable against a key you control |
| Supporting evidence, not an Annex I product requirement: custody of the signing keys | The private keys stay in the HSM; one operation per key with its own approval rule; every request in the platform's audit trail |

## Related

- [Digest signing](https://docs.laavat.io/usage/signing-encryption/digest-signing/) — every option,
  preset and error message, and the recipes this page builds on
- [Signed SWUpdate updates](https://docs.laavat.io/solutions/cra-ready-swupdate/) and
  [Signed Mender OTA updates](https://docs.laavat.io/solutions/cra-ready-mender-ota/) — digest signing for
  over-the-air updates
- [OCI signing](https://docs.laavat.io/exampleflows/ocisigning/) — container images with cosign
- [Certificate-based signing](https://docs.laavat.io/solutions/cra-ready-certificate-signing/) —
  kernel modules, UEFI and SWUpdate CMS with a certificate your CA issues from a CSR
- [MCUboot signing](https://docs.laavat.io/usage/signing-encryption/mcuboot-signing/) — when the platform
  signs the whole MCUboot image
- [Supported keys](https://docs.laavat.io/appendixes/supported-keys/)
- [Security and architecture](https://docs.laavat.io/security/) — where the keys live and who can
  reach them

## References

- [ESP-IDF: Secure Boot V2](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/security/secure-boot-v2.html),
  including remote signing of images
- [NIST FIPS 204: Module-Lattice-Based Digital Signature Standard (ML-DSA)](https://csrc.nist.gov/pubs/fips/204/final)
- [Linux kernel module signing](https://docs.kernel.org/admin-guide/module-signing.html)
  and [IMA documentation](https://ima-doc.readthedocs.io/)
- [RFC 7515: JSON Web Signature](https://www.rfc-editor.org/rfc/rfc7515),
  [DSSE](https://github.com/secure-systems-lab/dsse) and
  [in-toto](https://in-toto.io/)
- [RFC 5652: Cryptographic Message Syntax](https://www.rfc-editor.org/rfc/rfc5652)
