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.
How to read this
The 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 sets out which
requirements the platform supports and which remain yours.
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.
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.
Find your use case¶
| Use case | The device or verifier checks | What the platform returns | Certificate needed |
|---|---|---|---|
| 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 | A signature in the image header against a key in fuses or flash | A raw signature | No |
| Linux kernel modules | The kernel checks a module's appended signature against its trusted keys | The signed module | Yes |
| 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 | The firmware checks a db update against the KEK |
A PKCS #7 for db.auth |
Yes |
| Signed tokens and configuration | A device or service checks a signed file against your public key | A JWS | No |
| Supply-chain attestations | A customer or auditor checks an in-toto statement | A DSSE envelope | No |
| Post-quantum signatures | 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 | — |
| Over-the-air updates | SWUpdate or Mender checks the update before it installs | See SWUpdate and Mender | — |
Formats with an operation of their own are signed there: RAUC bundles, MCUboot images, NXP HAB and AHAB.
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.
{
"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": [] } }
]
}
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 and
configuration 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:
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).
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. IMA has no such route yet, because its envelope needs this operation's key.
For example, with OpenSSL:
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.extlists 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). TheextendedKeyUsageline matters only for SWUpdate, which checks it; these verifiers do not.openssl x509 -new -force_pubkey op.pubcreates 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.catwritesca-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_BINARIESturned 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; 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.
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) 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.
espsecure digest-sbv2-public-key --keyfile op.pub --output key-digest.bin
To verify:
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, orRAW_LEif your verifier expects little-endian.
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
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:
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:
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.
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; --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:
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.
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. 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:
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), from that operation's public key.
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-listwraps the new certificatedb.pemin an EFI signature list,db.esl. The owner GUID is a fixed value you keep, which marks your entries indb.sign-efi-sig-list -owritesdb.tbs: the exact bytes the KEK must sign.signing-toolsigns them with the KEK operation, as a PKCS #7 (--cms-preset uefi).sign-efi-sig-list -iputs the PKCS #7 and the list intodb.auth, the file you write to thedbvariable, for example withefi-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:
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.
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 useRS256(PKCS #1 v1.5) orPS256(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 whenconfig.jsontravels separately; the verifier then supplies it.
To verify:
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.
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:
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.
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.
# 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:
# 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 — every option, preset and error message, and the recipes this page builds on
- Signed SWUpdate updates and Signed Mender OTA updates — digest signing for over-the-air updates
- OCI signing — container images with cosign
- Certificate-based signing — kernel modules, UEFI and SWUpdate CMS with a certificate your CA issues from a CSR
- MCUboot signing — when the platform signs the whole MCUboot image
- Supported keys
- Security and architecture — where the keys live and who can reach them