Skip to content

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).
  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 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.

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.

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

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.

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. root.pem is your root CA.

SWUpdate with OpenSSL

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:

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:

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.

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:

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 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:

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

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):

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:

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.

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

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

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

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

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). -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

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

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:

{"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

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:

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 and MCUboot signing, where the platform signs the whole image instead.