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

# Digest signing

A digest signing request carries a hash you compute, not your data. The platform signs it with the operation's
HSM key, after approval if the operation needs one, and you download the signature or CMS and place it in your
artifact.

1. Hash locally. Only the digest leaves your machine, together with what you pass in `--cms-cert`,
   `--cms-content` or `--cms-extra-attributes`.
2. `imagesigning add <subcommand> ... [--format CMS] [--cms-* settings]`. Settings the platform can check
   without the key are refused when you send the request, before anyone approves it: an unreadable certificate,
   an unknown value, conflicting options, a private key in `--cms-cert`, and on DigestSigning and SignOCI any
   setting that needs a certificate you did not pass.
3. Approval, as for every request.
4. The HSM signs and the platform builds the CMS. A problem that only the key reveals ends the request in state
   Failed, with the reason in `errorStatus` and `errorDetail` (see [Errors](https://docs.laavat.io/usage/signing-encryption/digest-signing/#errors)).
5. `imagesigning get -I <request-uuid> --wait -O <file> [--bundle <file>.json]`.

Use `imagesigning add` when your device reads a CMS or a raw signature. Use
[`imagesigning envelope`](https://docs.laavat.io/usage/signing-encryption/digest-signing/#envelopes) when it reads JWS, COSE, DSSE, IMA, Mender or a signed kernel module.

The digest subcommands are `DigestSigning`, `SignHABCSF`, `SignHABIMG` and `SignAHAB`. They take the same
options, and each accepts any digest operation: what you get depends on the type of the operation `--operid`
names, as the tables below show.

## Algorithms

| Key | Hash (`-H`) | Options | Output |
| --- | --- | --- | --- |
| RSA, all supported key sizes | SHA256, SHA384, SHA512 | `--padding PKCS_v1_5` (default) | PKCS #1 v1.5 signature |
| RSA, all supported key sizes | SHA256, SHA384, SHA512 | `--padding PSS`, optionally `--salt-length` | RSASSA-PSS, MGF1 with the same hash |
| EC P-224, P-256, P-384, P-521 | SHA256, SHA384, SHA512 | `--encoding DER` (default) | ASN.1 DER `ECDSA-Sig-Value` |
| EC P-224, P-256, P-384, P-521 | SHA256, SHA384, SHA512 | `--encoding RAW` | `r‖s`, 56 / 64 / 96 / 132 bytes |
| EC P-224, P-256, P-384, P-521 | SHA256, SHA384, SHA512 | `--encoding RAW_LE` | `r‖s` with each half little-endian |

- `-p` is the digest, base64. `-H` names the hash it was made with and defaults to SHA512, so pass `-H SHA256`
  for a SHA-256 digest. signing-tool refuses a digest longer than the hash it sends, and warns when `-H` does
  not match the digest length.
- RSA keys need a digest of exactly the hash length: 32, 48 or 64 bytes.
- PSS salt: the hash length unless `--salt-length N` gives N bytes (`--salt-length hash` says so explicitly). The salt
  can be at most `key size in bytes - hash length - 2`, so RSA-1024 with SHA-512 needs `--salt-length 62` or less.
  ESP32, MCUboot, JWS `PS*`, AHAB and TF-A expect a salt of the hash length, which is the default.
- `RAW_LE` is for verifiers that read each half of `r‖s` little-endian. The platform returns `r‖s`;
  `imagesigning get -O` converts it, using the payload metadata entry `signingtool.signatureEncoding=RAW_LE`
  that `add` records.
- The REST API takes the same options as one property per `optionalParameters` item, for example
  `{"hashAlgorithm": "SHA384"}`, `{"signaturePadding": "PSS"}`, `{"saltLength": 32}`,
  `{"signatureEncoding": "RAW"}`. An empty value counts as not set, as `{"saltLength": 0}` does.

> **Note: Post-quantum: ML-DSA**
> ML-DSA keys sign the message itself (`-H NONE`, `-p` is the message, base64). In a CMS
> (`--format CMS -H SHA512`) they sign a SHA-512 digest; for kernel modules use
> `--cms-preset kernel-module-mldsa`. See [Supported keys](https://docs.laavat.io/appendixes/supported-keys/).

ESP32 Secure Boot V2 and MCUboot (RSA-3072/2048, PSS, SHA-256, 32-byte salt):

```bash
DIGEST=$(sha256sum image.bin | cut -d' ' -f1 | xxd -r -p | base64 -w0)
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <operation-uuid> \
    -p "$DIGEST" -H SHA256 --padding PSS -N "fw-1.4.0" -D "firmware 1.4.0"
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O image.sig
```

## Which CMS do I get?

`--format CMS` asks for a CMS (RFC 5652) instead of a raw signature. The first matching row applies.

| What you send | Operation | What you get |
| --- | --- | --- |
| a `--cms-*` setting without `--format CMS` | any | refused when the request is sent (signing-tool already refuses a `--cms-preset`) |
| no `--format`, or `--format PKCS1` | any digest operation | raw signature |
| `--format CMS --padding PSS`, no `--cms-*` setting | any digest operation | EC keys: a raw DER ECDSA signature; RSA keys: Failed (`unsupportedSignaturePadding`). Add a `--cms-*` setting for a CMS |
| `--format CMS` and any `--cms-*` setting, a `--cms-preset` included | any digest operation | a CMS built from your settings |
| `--format CMS`, no `--cms-*` setting | SignHABCSF, SignHABIMG, SignAHAB, DetachedSignature | the CMS these operations have always returned |
| `--format CMS`, no `--cms-*` setting | DigestSigning, SignOCI | a CMS that names the signer by key ID and embeds no certificate |

`--format` takes `CMS` exactly; any other value is sent as PKCS1, and another case such as `--format cms` also
prints a warning.

## Where the certificate comes from

A DigestSigning key has no platform certificate. Export its public key, have your CA certify it, and pass the
signer certificate, plus the intermediates where your verifier accepts them, with `--cms-cert`. HAB and
DetachedSignature operations already have a platform certificate chain.

```bash
signing-tool -n test.ini product getpubkey -P <product-uuid> --operid <operation-uuid> -O op.pub
printf '%s\n' 'basicConstraints=CA:FALSE' 'keyUsage=critical,digitalSignature' \
    'extendedKeyUsage=emailProtection' 'subjectKeyIdentifier=hash' 'authorityKeyIdentifier=keyid' > signer.ext
openssl x509 -new -subj "/CN=Firmware signing" -force_pubkey op.pub \
    -CA issuing-ca.pem -CAkey issuing-ca.key -days 730 -extfile signer.ext -out signer.pem
cat signer.pem issuing-ca.pem > chain.pem
```

This needs OpenSSL 3.0 or later; set `-days` by your CA's policy. `issuing-ca.pem` is the CA that signs the
certificate; append further intermediates to `chain.pem` if your root is further up. Do not include the root: the
verifier holds it as its trust anchor. `subjectKeyIdentifier=hash` lets a verifier find this certificate when a
CMS names the signer by key ID.

`--cms-cert` takes PEM `CERTIFICATE` blocks or a `PKCS7` bundle, is repeatable, and refuses a file that holds a
private key. The certificate whose public key is the operation's key is the signer; if none matches, the request
fails with `invalidCMSCertificates`.

## CMS settings

| signing-tool | REST property | Values | Default |
| --- | --- | --- | --- |
| `--cms-cert FILE` | `cmsCertificates` | PEM certificates or a PKCS7 bundle, base64 | none |
| `--cms-signed-attributes` | `cmsSignedAttributes` | `STANDARD` (contentType, signingTime, messageDigest), `NO_SIGNING_TIME`, `NONE` | `STANDARD` |
| `--cms-extra-attributes FILE` | `cmsExtraSignedAttributes` | a DER `SET OF Attribute` added to the signed attributes | none |
| `--cms-signer-id` | `cmsSignerIdentifier` | `ISSUER_AND_SERIAL`, `SUBJECT_KEY_ID` | issuer and serial when a signer certificate is known, else key ID |
| `--cms-embed` | `cmsEmbedCertificates` | `CALLER` (your `--cms-cert` certificates), `ALL` (yours and the operation chain), `SIGNER`, `NONE` | `CALLER` with `--cms-cert`, else `NONE` |
| `--cms-content-type OID` | `cmsContentType` | dotted OID | `1.2.840.113549.1.7.1` (id-data) |
| `--cms-content FILE` | `cmsContent` | content to encapsulate; `-p` must be its digest | detached |

- `NONE` signed attributes: the key signs the digest directly. It cannot be combined with `--cms-content`,
  extra attributes or another content type.
- A key ID is the certificate's SubjectKeyIdentifier extension. Without a certificate it is the SHA-1 of the
  public key bit string (RFC 5280 method 1), which is what `subjectKeyIdentifier=hash` produces.
- Extra attributes are single-valued and may not repeat contentType, signingTime or messageDigest.
- `--encoding RAW` and `RAW_LE` do not combine with `--format CMS`.
- With an RSA key, `--padding PSS` gives an RSASSA-PSS CMS signer; EC keys ignore it.
- ML-DSA keys: `-H SHA512` and signed attributes (`STANDARD` or `NO_SIGNING_TIME`); `--padding PSS` is refused.

A preset fills these settings for a consumer; settings you give yourself win. Every preset sends at least one
setting, so a request with a preset always gets a CMS built from your settings.

| Preset | Settings |
| --- | --- |
| `swupdate` | `--cms-signed-attributes STANDARD` (the default value); with `--cms-cert`, issuer and serial, your certificates embedded |
| `swupdate-mbedtls` | `--cms-embed SIGNER` |
| `rauc` | `--cms-signed-attributes STANDARD` (the default value) |
| `rauc-verity` | as `rauc`; needs `--cms-content <manifest>` |
| `cst-hab4` | `--cms-embed NONE --cms-signer-id ISSUER_AND_SERIAL` (on DigestSigning it needs `--cms-cert`; on a HAB operation the chain's leaf is the signer) |
| `kernel-module` | `--cms-signed-attributes NONE --cms-embed NONE` (RSA, ECDSA) |
| `kernel-module-mldsa` | `--cms-signed-attributes NO_SIGNING_TIME --cms-embed NONE` (ML-DSA; `-H SHA512`) |
| `uefi` | `--cms-signed-attributes NONE --cms-embed CALLER` (needs `--cms-cert`) |

## Recipes

Each recipe uses one DigestSigning operation unless it says otherwise, and the `signer.pem` / `chain.pem` from
[Where the certificate comes from](https://docs.laavat.io/usage/signing-encryption/digest-signing/#where-the-certificate-comes-from). `root.pem` is your root CA.

### SWUpdate with OpenSSL

```bash
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <operation-uuid> \
    -N swu -D "sw-description 1.4.0" \
    -p "$(openssl dgst -sha256 -binary sw-description | base64)" -H SHA256 \
    --format CMS --cms-preset swupdate --cms-cert chain.pem
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O sw-description.sig --bundle swu.json
openssl cms -verify -binary -inform DER -in sw-description.sig -content sw-description \
    -CAfile root.pem -purpose smimesign -out /dev/null
```

`CMS Verification successful` means SWUpdate (built with `CONFIG_SIGALG_CMS`, started with `-k root.pem`)
accepts it. SWUpdate requires `keyUsage=digitalSignature` and `extendedKeyUsage=emailProtection` by default; for
a `codeSigning` certificate set `cert-purpose` in `swupdate.cfg`. `--cms-preset rauc` gives the same CMS for a
RAUC plain bundle.

### SWUpdate with mbedTLS

SWUpdate 2026.05 or later built with mbedTLS 3.4 or later reads at most one certificate from the CMS, so embed
only the signer and give SWUpdate the root and the intermediates:

```bash
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <operation-uuid> \
    -N swu -D "sw-description 1.4.0" \
    -p "$(openssl dgst -sha256 -binary sw-description | base64)" -H SHA256 \
    --format CMS --cms-preset swupdate-mbedtls --cms-cert signer.pem
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O sw-description.sig
cat root.pem issuing-ca.pem > root-and-intermediates.pem   # swupdate -k root-and-intermediates.pem
```

Use RSA PKCS #1 v1.5 or ECDSA; the mbedTLS verifier does not take RSA-PSS.

### RAUC verity and crypt bundles

A verity or crypt bundle's signature is a CMS that encapsulates the manifest. Build the bundle with a throwaway
key, take its manifest, sign that, and put the new signature in:

```bash
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -subj "/CN=throwaway" \
    -keyout throwaway.key -out throwaway.pem -days 1
rauc bundle --cert=throwaway.pem --key=throwaway.key bundle-dir/ unsigned.raucb
rauc extract-signature --keyring=throwaway.pem unsigned.raucb throwaway.sig
openssl cms -verify -noverify -binary -inform DER -in throwaway.sig -out manifest.raucm
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <operation-uuid> \
    -N rauc -D "bundle 1.4.0" \
    -p "$(openssl dgst -sha256 -binary manifest.raucm | base64)" -H SHA256 \
    --format CMS --cms-preset rauc-verity --cms-cert chain.pem --cms-content manifest.raucm
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O bundle.sig
rauc replace-signature --keyring=throwaway.pem --signing-keyring=root.pem unsigned.raucb bundle.sig bundle.raucb
rauc info --keyring=root.pem bundle.raucb
```

`bundle-dir/manifest.raucm` must set `format=verity` (or `crypt`). If the target sets `use-bundle-signing-time`,
keep the default `STANDARD` signed attributes, which carry the signing time.

### NXP HAB4 with your own key

When your SRK CA certified a DigestSigning key, the `cst-hab4` preset gives the CMS layout NXP CST produces:
signed attributes, signer by issuer and serial, no certificates.

```bash
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <operation-uuid> \
    -N csf -D "CSF 1.4.0" -p "$(openssl dgst -sha256 -binary csf-data.bin | base64)" -H SHA256 \
    --format CMS --cms-preset cst-hab4 --cms-cert csf.pem
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O csf-data.sig
```

`csf.pem` is the CSF (or IMG) certificate your SRK CA issued for the operation's key. The device's HAB ROM
verifies the result.

### NXP HAB4 operations with other settings

SignHABCSF and SignHABIMG return the CMS they always have. Any `--cms-*` setting gives a CMS built from your
settings, signed with the operation's chain leaf:

```bash
signing-tool -n test.ini imagesigning add SignHABCSF -P <product-uuid> --operid <operation-uuid> \
    -N csf -D "CSF 1.4.0" -p "$(openssl dgst -sha256 -binary csf-data.bin | base64)" -H SHA256 \
    --format CMS --cms-signed-attributes NO_SIGNING_TIME
```

### Linux kernel modules

[`imagesigning envelope kernel-module`](https://docs.laavat.io/usage/signing-encryption/digest-signing/#kernel-module) signs and appends in one step. For a build that appends
the signature itself, the `kernel-module` preset returns the bare CMS for `scripts/sign-file -s`:

```bash
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <operation-uuid> \
    -N module -D "my_module 1.4.0" -p "$(openssl dgst -sha512 -binary my_module.ko | base64)" -H SHA512 \
    --format CMS --cms-preset kernel-module --cms-cert signer.pem
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O my_module.p7s
scripts/sign-file -s my_module.p7s sha512 signer.pem my_module.ko
```

`signer.pem` is the certificate the kernel trusts for the key (built in with `CONFIG_SYSTEM_TRUSTED_KEYS`, or in
the secondary keyring). Without `--cms-cert` the CMS names the signer by key ID, which the kernel matches only
against a certificate made with `subjectKeyIdentifier=hash`. ML-DSA keys use `--cms-preset kernel-module-mldsa`.

### UEFI authenticated variables

```bash
cert-to-efi-sig-list -g "$(uuidgen)" db.pem db.esl
sign-efi-sig-list -o -t "2026-10-07 12:00:00" db db.esl db.tbs
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <operation-uuid> \
    -N db -D "db update" -p "$(openssl dgst -sha256 -binary db.tbs | base64)" -H SHA256 \
    --format CMS --cms-preset uefi --cms-cert KEK.pem
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O db.p7
openssl pkcs7 -inform DER -in db.p7 -out db.p7.pem
sign-efi-sig-list -i db.p7.pem -t "2026-10-07 12:00:00" db db.esl db.auth
```

`KEK.pem` is the KEK certificate for the operation's key. Use the same `-t` timestamp in both
`sign-efi-sig-list` calls. The firmware verifies `db.auth` when you write the variable.

### CAdES-style signing certificate attribute

Some verifiers want the ESS `signingCertificateV2` attribute. Compute it from the signer certificate and pass it
as an extra signed attribute (needs Python with `asn1crypto`):

```bash
python3 - signer.pem ess.der <<'EOF'
import hashlib
import sys

from asn1crypto import cms, pem, tsp, x509

_, _, der = pem.unarmor(open(sys.argv[1], "rb").read())
cert = x509.Certificate.load(der)
v2 = tsp.SigningCertificateV2({"certs": [{
    "cert_hash": hashlib.sha256(der).digest(),
    "issuer_serial": {"issuer": [x509.GeneralName({"directory_name": cert.issuer})],
                      "serial_number": cert.serial_number}}]})
open(sys.argv[2], "wb").write(cms.CMSAttributes([{"type": "signing_certificate_v2", "values": [v2]}]).dump())
EOF
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <operation-uuid> \
    -N doc -D "document 1.4.0" -p "$(openssl dgst -sha256 -binary document.pdf | base64)" -H SHA256 \
    --format CMS --cms-cert chain.pem --cms-extra-attributes ess.der
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O document.p7s
openssl cms -verify -cades -binary -inform DER -in document.p7s -content document.pdf -CAfile root.pem \
    -purpose any -out /dev/null
```

### Signed by this key

Without any `--cms-*` setting a DigestSigning `--format CMS` request returns a CMS that names the signer by key
ID and embeds nothing. `--bundle` records that signing-tool verified it against the operation's public key:

```bash
signing-tool -n test.ini imagesigning add DigestSigning -P <product-uuid> --operid <operation-uuid> \
    -N data -D "data 1.4.0" -p "$(openssl dgst -sha256 -binary data.bin | base64)" -H SHA256 --format CMS
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait -O data.p7s --bundle data.json
openssl cms -verify -binary -inform DER -in data.p7s -content data.bin -certfile chain.pem -CAfile root.pem \
    -purpose any -out /dev/null
```

The OpenSSL check finds the signer in `chain.pem` by key ID, so the signer certificate must be made with `subjectKeyIdentifier=hash`.

## Envelopes

`imagesigning envelope <kind>` builds the bytes to be signed from your local files, sends only their digests,
puts every `-F` into one request with one approval, and verifies each result against the operation's public key
before it writes anything. Envelopes need a DigestSigning operation.

```text
imagesigning envelope <kind> -P <product-uuid> --operid <operation-uuid> --state-dir DIR
    -F <input> [-F ...] -O <output> [kind options] [--bundle FILE]
```

- With one `-F`, `-O` is the output file. With several (`kernel-module` and `ima`), `-O` is a directory and each
  output keeps its input's path below the inputs' common directory.
- `--state-dir` records a pending request, so running the same command again resumes it instead of asking for
  a second approval. A changed input or option starts a new request. `--approver-token @FILE` approves the
  request in the same run, and `--wait-timeout` sets how long to wait, as in `imagesigning sign`.
- If the request fails, signing-tool lists each input that was not signed and the request's `errorStatus`, and
  writes no output.
- Approvers see `signingtool.envelope=<kind>` and the input file name in each payload's metadata.
- The JWS, COSE_Sign1, DSSE and Mender recipes verify with `op.pub` from `product getpubkey`; the kernel module
  and IMA recipes verify with the certificate the device trusts.

| Kind | Signs | Keys | Output |
| --- | --- | --- | --- |
| `jws` | `BASE64URL(header).BASE64URL(payload)` (RFC 7515) | `--alg` RS256/384/512, PS256/384/512, ES256/384/512 | compact JWS |
| `cose-sign1` | the COSE `Sig_structure` (RFC 9052) | `--alg` ES256/384/512, PS256/384/512 | COSE_Sign1 |
| `dsse` | the DSSE pre-authentication encoding | RSA (`--padding PKCS_v1_5` or `PSS`), EC P-256, P-384, P-521 | DSSE JSON envelope |
| `kernel-module` | the module | RSA PKCS #1 v1.5, ECDSA, ML-DSA | the signed module |
| `ima` | the file's hash | RSA PKCS #1 v1.5, ECDSA | `security.ima` values and `ima.xattrs` |
| `mender` | the Artifact's `manifest` | RSA PKCS #1 v1.5, ECDSA P-256 | `manifest.sig` |

### JWS

```bash
signing-tool -n test.ini imagesigning envelope jws -P <product-uuid> --operid <operation-uuid> --state-dir .sign \
    -F manifest.json -O manifest.jws --alg ES256 --kid release-2026
python3 -c 'from jwcrypto import jwk, jws; t = jws.JWS(); t.deserialize(open("manifest.jws").read()); t.verify(jwk.JWK.from_pem(open("op.pub", "rb").read()), alg="ES256"); print("JWS verified")'
```

`--omit-payload` leaves the payload out of the output (RFC 7515 appendix F); give it to the verifier separately.

### COSE_Sign1

```bash
pip install 'signing-tool[cose]'
signing-tool -n test.ini imagesigning envelope cose-sign1 -P <product-uuid> --operid <operation-uuid> \
    --state-dir .sign -F payload.bin -O payload.cose --alg ES256 --kid release-2026
pip install pycose
python3 -c 'from pycose.keys import CoseKey; from pycose.messages import CoseMessage; m = CoseMessage.decode(open("payload.cose", "rb").read()); m.key = CoseKey.from_pem_public_key(open("op.pub").read()); print("COSE verified" if m.verify_signature() else "COSE FAILED")'
```

`--external-aad FILE` adds external additional authenticated data; `--omit-payload` leaves the payload out.

### DSSE

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

The check needs `securesystemslib`. The example is for an EC P-256 key. For RSA, `--padding PSS` matches in-toto and
the default PKCS #1 v1.5 matches sigstore; pass the same scheme to `SSlibKey.from_crypto` (`rsassa-pss-sha256` or `rsa-pkcs1v15-sha256`).

### Kernel module

```bash
signing-tool -n test.ini imagesigning envelope kernel-module -P <product-uuid> --operid <operation-uuid> \
    --state-dir .sign -F drivers/a.ko -F drivers/net/b.ko -O signed/ --cms-cert signer.pem
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 chain.pem -CAfile root.pem \
    -purpose any -out /dev/null
```

The outputs are `signed/a.ko` and `signed/net/b.ko`; `scripts/extract-module-sig.pl` is in the kernel source
tree. The kernel loads the module when it trusts `signer.pem` (see
[Linux kernel modules](https://docs.laavat.io/usage/signing-encryption/digest-signing/#linux-kernel-modules)). `-H` selects SHA512 (default), SHA384 or SHA256;
`--cms-signer-id SUBJECT_KEY_ID` names the signer by key ID instead of issuer and serial. ML-DSA keys need
`-H SHA512`.

### IMA

```bash
signing-tool -n test.ini imagesigning envelope ima -P <product-uuid> --operid <operation-uuid> --state-dir .sign \
    -F rootfs/usr/bin/app -F rootfs/usr/lib/libapp.so -O ima/
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
(cd rootfs/usr && setfattr --restore=../../ima/ima.xattrs)
```

Each input gets `<path>.sig` with its `security.ima` value; `ima.xattrs` sets them all with `setfattr --restore`,
run from the inputs' common directory (here `rootfs/usr`). The key ID in the signature is taken from the public
key as `subjectKeyIdentifier=hash` computes it, so the kernel's `.ima` keyring needs a certificate made that way;
`--keyid` sets another. `-H` selects SHA256 (default), SHA384 or SHA512.

### Mender

```bash
tar -xf unsigned.mender manifest
signing-tool -n test.ini imagesigning envelope mender -P <product-uuid> --operid <operation-uuid> \
    --state-dir .sign -F manifest -O manifest.sig
mkdir artifact && tar -C artifact -xf unsigned.mender
cp manifest.sig artifact/
tar -C artifact --format=ustar -cf signed.mender $(tar -tf unsigned.mender | sed '/^manifest$/a manifest.sig')
mender-artifact validate -k op.pub signed.mender
```

The `tar` call keeps the Artifact's member order and puts `manifest.sig` directly after `manifest`. Mender takes
RSA (PKCS #1 v1.5, SHA-256) and ECDSA P-256.

## The result bundle

`--bundle FILE` on `imagesigning get` and `imagesigning envelope` writes a record of the request. `FILE.json` is
the record alone, with hashes instead of your files. `FILE.zip` or `FILE.tar.gz` holds the record as
`request.json` and the results themselves: for `get`, each payload's result under `payloads/<n>/`, as `-O`
writes it; for `envelope`, only the outputs, under `outputs/`. `--json` prints the record.

An abbreviated record from the [SWUpdate recipe](https://docs.laavat.io/usage/signing-encryption/digest-signing/#swupdate-with-openssl):

```json
{"bundle": {"signingTool": "3.11.0", "api": "https://<api>", "time": "2026-10-07T12:00:00Z"},
 "request": {"id": "...", "name": "swu", "description": "sw-description 1.4.0", "state": "Ready"},
 "payloads": [{"state": "Ready",
   "storedOptionalParameters": [
     {"type": "cmsCertificates", "sha256": "9f...", "size": 2412},
     {"type": "cmsSignedAttributes", "value": "STANDARD"},
     {"type": "hashAlgorithm", "value": "SHA256"},
     {"type": "signatureFormat", "value": "CMS"},
     {"type": "signaturePadding", "value": "PKCS_v1_5"}],
   "result": {"file": "sw-description.sig", "sha256": "3c...", "size": 1874}}],
 "operationPublicKey": {"pem": "-----BEGIN PUBLIC KEY-----...", "sha256": "..."},
 "verification": {"method": "certificate", "certificateSha256": "...", "publicKeySha256": "...",
                  "checked": "payloads/1/sw-description.sig: CMS signature verified against operationPublicKey; messageDigest is the submitted digest",
                  "repeat": "openssl cms -verify -binary -inform DER -in payloads/1/sw-description.sig -content <input> -CAfile <root.pem>"}}
```

- `verification.method` is `certificate` when the results embed one signer certificate, `publicKey` when
  signing-tool knows the signer's public key but no certificate, and `none` when it knows neither. `publicKeySha256` (and
  `certificateSha256` for `certificate`) binds the record to one signer key.
- `checked` is what signing-tool verified: one line per payload for `get`, one line for an envelope. `repeat` is written only for `certificate`: a
  command for your own trust chain, where `<root.pem>` is your CA and `<input>` the file you hashed.
- Every payload that can be checked must verify against the same key, or no bundle is written. A payload that
  cannot be checked is labelled "not verified" in `checked`: a raw signature or a CMS without signed attributes
  (`get` holds only the digest's SHA-256, not the digest), a CMS from an operation without a public key of its own
  that embeds no certificate, and the raw DER ECDSA signature that `--format CMS --padding PSS` without a
  `--cms-*` setting returns, which `-O` still writes. A CMS that does not verify writes no bundle.
- The record holds hashes instead of your files, certificates, encapsulated content and secrets, never holds a
  token and does not name approvers. A `.zip` or `.tar.gz` bundle carries the results, and for envelopes the
  outputs: `kernel-module`, `dsse` and attached `jws` and `cose-sign1` outputs contain the input.
- For an operation without a public key of its own (HAB, DetachedSignature), `operationPublicKey` is the
  embedded signer certificate's key, or `{"unavailable": "chain operation"}`.

## Verifying a raw signature

```bash
signing-tool -n test.ini product getpubkey -P <product-uuid> --operid <operation-uuid> -O op.pub
printf '%s' "$DIGEST" | base64 -d > digest.bin

# RSA PKCS #1 v1.5
openssl pkeyutl -verify -pubin -inkey op.pub -in digest.bin -sigfile image.sig -pkeyopt digest:sha256
# RSA-PSS (salt = hash length; use rsa_pss_saltlen:<N> for --salt-length N)
openssl pkeyutl -verify -pubin -inkey op.pub -in digest.bin -sigfile image.sig \
    -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pss -pkeyopt rsa_pss_saltlen:-1
# ECDSA DER
openssl pkeyutl -verify -pubin -inkey op.pub -in digest.bin -sigfile image.sig -pkeyopt digest:sha256
```

A raw `r‖s` signature verifies the same way after converting it to DER, for example with Python's
`cryptography.hazmat.primitives.asymmetric.utils.encode_dss_signature`.

## Errors

signing-tool refuses some command lines itself, before anything is sent (exit `2`): a digest longer than the
hash it sends, a `--cms-cert` file that holds a private key or no `CERTIFICATE` or `PKCS7` block, a
`--cms-preset` without `--format CMS`, a preset without a setting it needs (`rauc-verity` without
`--cms-content`), an empty `--cms-content` or `--cms-extra-attributes` file (`--cms-content <file>: the file is
empty`, `--cms-extra-attributes <file>: the file is empty`), and an empty `--cms-*` value (`<flag> needs a
value`).

The platform refuses the rest of what it can check without the key when the request is sent, with HTTP 400 and
before anyone approves it. The message names the payload (`payload.` or `payloads[<n>].`), for example:

```text
ERROR: HTTP 400 Bad Request: payload.optionalParameters: saltLength needs signaturePadding PSS
```

| What you send | Message after `payload.optionalParameters:` |
| --- | --- |
| a `--cms-*` setting without `--format CMS` | `<setting> needs signatureFormat CMS` |
| `--cms-*`, `--salt-length` or `--encoding` on an operation that does not sign digests | `<setting> applies only to DetachedSignature, DigestSigning, SignAHAB, SignHABCSF, SignHABIMG and SignOCI, not <operation type>` |
| `--salt-length` without `--padding PSS` | `saltLength needs signaturePadding PSS` |
| `--encoding` with `-H NONE` | `signatureEncoding cannot be used with hashAlgorithm NONE` |
| `--encoding RAW` or `RAW_LE` with `--format CMS` | `signatureEncoding RAW cannot be used with signatureFormat CMS` |
| `--cms-embed CALLER` without `--cms-cert` | `cmsEmbedCertificates CALLER needs cmsCertificates` |
| DigestSigning or SignOCI without `--cms-cert`, with `--cms-signer-id ISSUER_AND_SERIAL` | `DigestSigning keys have no certificate: cmsSignerIdentifier ISSUER_AND_SERIAL needs cmsCertificates` |
| DigestSigning or SignOCI without `--cms-cert`, with `--cms-embed SIGNER` or `ALL` | `DigestSigning keys have no certificate: cmsEmbedCertificates SIGNER needs cmsCertificates` |
| `--cms-signed-attributes NONE` with `--cms-content` | `cmsSignedAttributes NONE cannot be used with cmsContent` |
| `--cms-signed-attributes NONE` with `--cms-extra-attributes` | `cmsSignedAttributes NONE cannot be used with cmsExtraSignedAttributes` |
| `--cms-signed-attributes NONE` with another `--cms-content-type` | `cmsSignedAttributes NONE needs cmsContentType 1.2.840.113549.1.7.1` |
| a `--cms-cert` file the platform cannot read | a message that starts with `cmsCertificates` and names the problem |
| a `--cms-extra-attributes` file that is not a DER `SET OF Attribute`, a primitive SET included | `cmsExtraSignedAttributes is not a DER SET OF Attribute` |
| an extra attribute with more than one value, given twice, or one of contentType, signingTime and messageDigest | `cmsExtraSignedAttributes attribute <OID> must have exactly one value`, `... appears twice`, `... is set by the service` |
| a `--cms-content-type` that is not a dotted OID | `cmsContentType "<value>" is not a dotted OID` |
| a setting larger than the platform stores | a message that names the setting and the limit |

The operation type and the setting names in a message are the ones of your request. In the REST API a new
property (`saltLength`, `signatureEncoding`, `cms*`) must be the only property of its `optionalParameters` item
(`payload.optionalParameters[<n>]: <property> must be the only property of its item`), and in a payload that uses
a new property or `SHA384` the same parameter twice with different values is refused
(`payload.optionalParameters set <parameter> twice with different values`, followed by the two values except for
`cmsCertificates`, `cmsExtraSignedAttributes` and `cmsContent`).

A DigestSigning payload is checked before the request goes for approval: it must be uploaded, be base64 and not
be empty, and for an RSA or ML-DSA key decode to exactly the digest of the hash algorithm. A payload that fails
ends the request in state Failed without an approval, with `invalidDigestSize` or `payloadIsNotBase64Encoded` and
the fault, naming the `payloads` entry, in `errorDetail`. An EC key signs the digest it is given, so a shorter
digest is accepted.

A request that fails after approval ends in state Failed with `errorStatus` and `errorDetail`:

| errorStatus | Cause |
| --- | --- |
| `invalidDigestSize` | an RSA digest that is not exactly the hash length; in a CMS built from settings, or an RSA CMS, a digest that is not the hash length |
| `unsupportedSignaturePadding` | a salt longer than the key allows (the detail names the bound); `--padding PSS --salt-length` with an EC key (`saltLength needs an RSA key with PSS`); `--format CMS --padding PSS` with an RSA key and no `--cms-*` setting; padding `NONE` with a CMS |
| `invalidCMSCertificates` | no `--cms-cert` certificate matches the operation's key, the chain's leaf does not, or `SUBJECT_KEY_ID` with a signer certificate that has no SubjectKeyIdentifier |
| `invalidCMSSettings` | `--cms-content` whose hash is not the digest; ML-DSA with PSS or without `-H SHA512`; ML-DSA with `--cms-preset kernel-module` or `--cms-signed-attributes NONE` (`ML-DSA CMS needs signed attributes: without them the key signs the content, which the service never receives`); a CMS larger than the platform stores (the detail names the limit) |
| `payloadIsNotBase64Encoded` | a digest that is not base64 in a request for a CMS built from settings, including DigestSigning and SignOCI `--format CMS` without a `--cms-*` setting |
| `keyNotSuitableForSigning` | `--encoding` with an RSA key (`signatureEncoding needs an ECDSA key`) |
| `emptyCMSTree` | a HAB or DetachedSignature operation without a certificate chain, given `--cms-*` settings but no `--cms-cert` |

## Changes from earlier behaviour

signing-tool 3.11.0 and the platform update change these cases; everything else behaves as before.

- `-H` in another case (`-H sha256`) is sent as that algorithm when the digest has its length. Earlier versions
  sent SHA512, and RSA signatures and EC CMS results of those requests did not verify.
- A digest longer than the hash signing-tool sends is refused before it is sent; the platform refused it after
  approval.
- `--format cms` in another case is still sent as PKCS1, now with a warning.
- DigestSigning and SignOCI with `--format CMS` return a CMS; these requests failed before.
- RSA-PSS with HSM keys works, with a salt of the hash length unless you set one.
- `--format CMS --padding PSS` without a `--cms-*` setting fails for RSA keys with `unsupportedSignaturePadding`
  and a hint to add a setting; EC keys return the raw DER ECDSA signature as before.
- An RSA digest shorter than the hash fails with `invalidDigestSize`; the platform used to return a signature that
  never verified.
- Failed requests report the specific reason in `errorStatus` instead of `defaultErrorStatus`.
- REST: in a payload that uses a new property or `SHA384`, the same parameter twice with different values is
  refused; the platform used one of them. A new property
  sharing an `optionalParameters` item with another property is refused; it was ignored.
- `imagesigning get -O` on a request with several payloads fails and suggests `--bundle`.

See also the [signing overview](https://docs.laavat.io/usage/signing-encryption/signing-encryption/#digest-signing) and
[MCUboot signing](https://docs.laavat.io/usage/signing-encryption/mcuboot-signing/), where the platform signs the whole image instead.
