Usage¶
The client-side tooling is the signing-tool command, which gathers the
different use cases into subcommands. A companion config-init command
generates a configuration file so settings (and the token) don't have to be
passed on every command line.
See Setup for installation.
Getting help¶
The top-level help lists every subcommand:
(venv) $ signing-tool -h
usage: signing-tool [-h] [--version] (-n N | -c) [-t <TOKEN>] [--skipssl]
[--json] [-a <ADDRESS>]
{imagesigning,group,product,production,escrow,fusemap,secrets,profile,client,ca}
...
Example Signing API Tool
positional arguments:
{imagesigning,group,product,production,escrow,fusemap,secrets,profile,client,ca}
Top level commands
imagesigning Imagesigning functionality
group Config group management functionality
product Product management functionality
production Production functionality E.g., Device certificates
escrow Escrow management functionality
fusemap Fusemap management functionality
secrets Secret management functionality
profile PKI Profile functionality
client Client functionality
ca CA functionality
options:
-h, --help show this help message and exit
--version, -V show program's version number and exit
-n N Config file path
-c Config from command line options
-t <TOKEN> OAUTH2 token for the API authentication. RECOMMENDED:
'@/path/to/file' reads the token from a file, or '@-'
reads it from stdin - both keep the secret out of
shell history and process listings. NOT RECOMMENDED:
passing the token literally (it is visible in shell
history and process listings).
--skipssl Skip TLS verification (INSECURE; local development
only)
--json Machine-readable output: exactly one JSON document on
stdout - the API response, or a summary of what the
command did when there is no response object
-a <ADDRESS> API address (eg. https://localhost:1234/api/v1)
Each subcommand has its own help (signing-tool <subcmd> -h) and each
operation a further level (signing-tool <subcmd> <op> -h). For example:
(venv) $ signing-tool product -h
usage: signing-tool product [-h]
{add,approve,get,getapproval,getpubkey,getall} ...
positional arguments:
{add,approve,get,getapproval,getpubkey,getall}
Product subcommands
add Add new product request from a template file
approve Approve product
get Get product
getapproval Get products requiring approval
getpubkey Export one operation's public key (for verifying what
it signs)
getall Get all products
Providing the token¶
The token is short-lived and is your credential to the API — treat it like a
password. Do not pass it literally on the command line (-t "$TOKEN");
it ends up in your shell history and in process listings (ps,
/proc/<pid>/cmdline). If you do, the tool prints a warning.
The recommended approach is a configuration file that references the token
by file, so the secret is never stored in the config and never appears on a
command line. Generate one with config-init:
# The token lives in its own file, restricted to your user:
(venv) $ printf '%s' "$TOKEN" > jwt.token && chmod 600 jwt.token
# config-init writes a 'token_file' reference (not the token itself) and
# creates the config 0600 on POSIX:
(venv) $ config-init -n test.ini -t @jwt.token -a https://app.laavat.io/<CustomerName>/api/v1
The resulting test.ini:
[service]
url = https://app.laavat.io/<CustomerName>/api/v1
token_file = jwt.token
Then run any command with -n test.ini — no token on the command line:
(venv) $ signing-tool -n test.ini product getall
Other ways to supply the token
-t @/path/to/filereads the token directly from a file.-t @-reads it from stdin:printf '%s' "$TOKEN" | signing-tool -c -t @- -a <url> product getall. This suits CI secret managers, which expose secrets as files or on stdin. Interactive commands (escrow add,group add) cannot use-t @-, because they read from stdin themselves — use a config file or-t @/pathfor those.- TLS verification is on by default.
--skipssldisables it and is intended for local development only.
The remaining examples on this page use -n test.ini.
Automating from CI¶
The client is usable from scripts and build systems without scraping its output.
Exit codes¶
Errors go to stderr and the process exits non-zero, so a script can tell success from failure:
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Error — API, HTTP, transport, anything unclassified |
2 |
Usage — a missing/malformed option, or a token that can't be resolved |
3 |
Not ready — --wait ran out of time with the request still pending. The request is still alive; approve it and re-run |
4 |
The request is Rejected or Failed — it can never produce a payload, so retrying is pointless |
130 |
Interrupted with Ctrl-C |
--json¶
--json prints exactly one JSON document on stdout (and nothing else), for
every command family. Human output is unchanged without it. It may appear
before or after the subcommand.
# Capture the request id without scraping prose off stdout:
(venv) $ ID=$(signing-tool --json -n test.ini imagesigning add SignUBoot \
-N build-42 -D "u-boot" -P "$PRODUCT" --operid "$OPER" \
-F u-boot.imx | jq -r .id)
JSON keys are snake_case
The document is the SDK's view of the API object, so the API's
modifiedSHA256 comes out as modified_sha256, originalSHA256 as
original_sha256. A jq expression written against the wire names
silently returns null.
Commands whose human-readable output is a single sentence (client approve,
ca patch, ca getcsr, ca getcrl, secrets get, secrets getkeys,
product getpubkey) emit a document describing what they did under --json. The
interactive commands (escrow add, group add, and the escrow/group/
product approvals) have no machine-readable form and refuse --json.
Signing¶
The signing flow is: submit a request, wait for approval if the product's
approval rule requires it, then download the signed payload with
imagesigning get.
CST signing (NXP i.MX6, HAB/AHAB)¶
NXP i.MX6 packages can be signed with the CST-tool-based SignHAB operation.
# Submit
(venv) $ signing-tool -n test.ini imagesigning add SignHAB \
-N test -D test2 -P adcb30d8-f009-438e-b1b2-96f507b306cb \
--operid 4add90e9-ffb3-4708-9554-ed2e82e8fd71 -F SPL-hab
# Approve
(venv) $ signing-tool -n approver.ini imagesigning approve \
-I a83081a6-1d3b-4117-a81b-0ebcfcf0669c
# Get the signed payload
(venv) $ signing-tool -n test.ini imagesigning get \
-I a83081a6-1d3b-4117-a81b-0ebcfcf0669c -O /tmp/signed.bin
SPSDK signing (AHAB)¶
SignSPSDK signs an NXP AHAB boot container (i.MX8QXP / i.MX9) with the SPSDK
tool. Reach for SignSPSDK for SPSDK-based AHAB, and SignHAB for the CST-tool
HAB/AHAB flow.
The payload is a single compressed archive containing the image, an SPSDK
configuration file, and a request.json naming them. See
SPSDK signing for the archive
contents and which configuration fields you own.
# Build the archive (image + SPSDK config + request.json)
$ tar czf payload.tar.gz request.json mimx9352-sign.yaml imx9-container.bin
# Submit. SRK0 signs by default; --signingKeyIndex 0-3 selects another SRK.
(venv) $ signing-tool -n test.ini imagesigning add SignSPSDK \
-N build-42 -D "imx93 AHAB" \
-P adcb30d8-f009-438e-b1b2-96f507b306cb \
--operid 4add90e9-ffb3-4708-9554-ed2e82e8fd71 \
--signingKeyIndex 0 -F payload.tar.gz
# Approve
(venv) $ signing-tool -n approver.ini imagesigning approve \
-I a83081a6-1d3b-4117-a81b-0ebcfcf0669c
# Get the signed payload
(venv) $ signing-tool -n test.ini imagesigning get \
-I a83081a6-1d3b-4117-a81b-0ebcfcf0669c -O /tmp/signed.bin
FIT signing¶
# Submit
(venv) $ signing-tool -n test.ini imagesigning add SignUBoot \
-N test -D test2 -P adcb30d8-f009-438e-b1b2-96f507b306cb \
--operid 4add90e9-ffb3-4708-9554-ed2e82e8fd71 -F linux-uImage.bin
# Approve
(venv) $ signing-tool -n approver.ini imagesigning approve \
-I a83081a6-1d3b-4117-a81b-0ebcfcf0669c
# Get the signed payload
(venv) $ signing-tool -n test.ini imagesigning get \
-I a83081a6-1d3b-4117-a81b-0ebcfcf0669c -O /tmp/signed.bin
RAUC signing¶
# Submit
(venv) $ signing-tool -n test.ini imagesigning add RaucBundleSigningWithOneTimeKey \
-N test -D test2 -P adcb30d8-f009-438e-b1b2-96f507b306cb \
--operid 4add90e9-ffb3-4708-9554-ed2e82e8fd71 -F rauc-verity.tar.gz
# Approve
(venv) $ signing-tool -n approver.ini imagesigning approve \
-I a83081a6-1d3b-4117-a81b-0ebcfcf0669c
# Get the signed payload
(venv) $ signing-tool -n test.ini imagesigning get \
-I a83081a6-1d3b-4117-a81b-0ebcfcf0669c -O /tmp/signed.rauc.bundle
--metadata
Every imagesigning add operation accepts --metadata '[{"name":"...","value":"..."}]' to attach JSON key/value pairs to the request.
Downloading: waiting and verification¶
--wait¶
Instead of hand-rolling a retry loop, imagesigning get --wait polls until the
request is Ready and then downloads. It requires -I.
(venv) $ signing-tool -n test.ini imagesigning get -I "$ID" --wait \
--wait-timeout 1800 -O u-boot.signed
--wait-timeout SEC (default 1800) bounds the wait. It is not a hard
guarantee: one check is always made at the boundary with a five-second grace,
so the command can return up to ~5s late rather than stop short and miss an
approval made in the last stretch. A caller that needs a hard bound must impose
it.
On timeout with the request still pending, get --wait exits 3 — the request
is still alive, so approve it and re-run. It exits 4 if the request was
rejected or failed. Without --wait, get -O on a request that isn't Ready
fails the same way rather than silently writing nothing.
The two digests¶
get answers two different questions with two different digests:
| Check | Answers | Default |
|---|---|---|
modifiedSHA256 (automatic) |
Did the artifact arrive intact? | Always on — refuses to write the file on a mismatch, or when the service reports no digest |
originalSHA256 (via --expect-input FILE) |
Was it signed over the bytes I still hold? | Off — you name the file |
The first can't answer the second: approval is a human step that can take days,
over which the build tree moves (a rebuild, another branch, a stale request id),
so a perfectly intact artifact can carry a signature over an input that's no
longer on disk. --expect-input compares the service's originalSHA256
against your file before downloading, and requires -I:
(venv) $ signing-tool -n test.ini imagesigning get -I "$RID" --wait \
--expect-input u-boot.bin -O u-boot.bin.signed
--allow-unverified downgrades both checks to warnings — for a legacy service
that reports no digest, or alongside --skipBase64 when you want the stored
representation rather than the signed artifact.
Exporting a verification public key: product getpubkey¶
product getpubkey exports the public key of one signing operation, so a
build can embed it into a kernel, device tree or firmware image and the device
can verify what that operation signed. It reports the SHA-256 of the key's DER
SubjectPublicKeyInfo.
(venv) $ signing-tool -n test.ini product getpubkey \
-P adcb30d8-f009-438e-b1b2-96f507b306cb \
--operid 4add90e9-ffb3-4708-9554-ed2e82e8fd71 \
-O uboot-verify.pem
public key written to: uboot-verify.pem
SHA-256 (DER SubjectPublicKeyInfo): 9f2b…c41d
--fingerprint <sha256> makes the export fail unless the key matches, so a
rotated key breaks the build instead of being injected silently
(case-insensitive):
(venv) $ signing-tool -n test.ini product getpubkey \
-P "$PRODUCT" --operid "$OPER" \
--fingerprint 9f2b...c41d -O uboot-verify.pem
Intended use is producing a reviewed, pinned bundle — not a live fetch on every
build. It works for operations whose token carries a SubjectPublicKeyInfo
(SignKernel, SignUBoot); other operation types give a clear error. It does
not do X.509 conversion (use your board's own tool, e.g. fdt_add_pubkey)
and does not resolve indexed HAB/SPSDK SRK chain material.
Exporting AES product keys¶
For products with exportable AES keys, a registered SecurityEngineer
client can pull them out with secrets getkeys. Other key material is not
exported this way — see the note in
Clients. Keys are
wrapped per-key with an ephemeral AES KEK derived via ECDH+HKDF against the
client's EC private key, and the whole response is delivered inside a JWE
addressed to that key. See
Clients
for the full scheme and registration steps.
(venv) $ signing-tool -n test.ini secrets getkeys \
-P 6f603401-6723-4dec-a4a3-a8749865b46d \
-C client.ec.private -O /tmp/prod.keys
# Add --cloudhsm when the deployment is backed by AWS CloudHSM
(venv) $ signing-tool -n test.ini secrets getkeys \
-P 6f603401-6723-4dec-a4a3-a8749865b46d \
-C client.ec.private -O /tmp/prod.keys --cloudhsm
One <prefix><i>.bin file is written per wrapped key. Asymmetric keys are
additionally written as PKCS#8 / SubjectPublicKeyInfo PEM pairs
(<prefix><i>.priv.pem, <prefix><i>.pub.pem); symmetric keys are raw bytes.
Output files and permissions¶
- Exported key material and secrets are written
0600; public certificates andproduct getpubkeyPEMs0644; downloaded signed artifacts follow the umask (0644under the usual022). - Exports of secret or verification material never overwrite an existing path —
they fail instead. The re-fetchable outputs are the exception: the JSON from
product get -O/group get -Oand the signed artifact fromimagesigning get -Oare refreshed in place.
Material exported by versions before 3.9.0 may have been world-readable
Before 3.9.0, exported secrets (private keys, KEK-IVs, SRK hashes, fusemaps,
unwrapped key material) were written with default permissions rather than
0600. If you exported secrets with an earlier version, treat that material
as having been world-readable and rotate as appropriate.
Proxies, internal CAs and timeouts¶
For enterprise networks:
| Variable | Effect |
|---|---|
https_proxy / HTTPS_PROXY |
Honoured for API calls (not just the S3 upload/download legs) |
no_proxy / NO_PROXY |
Honoured; port-qualified entries (sign.internal:8443) and bare IPv6 (::1) both match |
REQUESTS_CA_BUNDLE / SSL_CERT_FILE |
Trust an internal CA without disabling TLS verification — prefer this over --skipssl |
Every network call is bounded (10s connect / 60s read for API calls, 10s / 300s for S3 transfers), so a call can no longer hang indefinitely.
End-to-end CI example¶
This shows the exit codes doing their job — submit, wait for a human approval, download only the artifact signed over the exact input, then pin the verification key.
#!/bin/sh
set -eu
URL=https://app.laavat.io/<CustomerName>/api/v1
TOKENFILE=/run/secrets/jwt # chmod 600
PRODUCT=adcb30d8-f009-438e-b1b2-96f507b306cb
OPER=4add90e9-ffb3-4708-9554-ed2e82e8fd71
# Behind a proxy or an internal CA:
# export https_proxy=http://proxy.internal:3128
# export NO_PROXY=app.laavat.io
# export REQUESTS_CA_BUNDLE=/etc/ssl/certs/internal-ca.pem
# 1. Submit. --json gives one document; the id comes out of it, not out of prose.
ID=$(signing-tool --json -c -a "$URL" -t @"$TOKENFILE" \
imagesigning add SignUBoot \
-N "build-$CI_PIPELINE_ID" -D "u-boot" \
-P "$PRODUCT" --operid "$OPER" -F u-boot.imx \
| jq -r .id)
echo "request $ID submitted"
# 2. Wait for the human approval, then download. --expect-input refuses a
# signature over anything but the bytes we still hold.
set +e
signing-tool -c -a "$URL" -t @"$TOKENFILE" \
imagesigning get -I "$ID" --wait --wait-timeout 1800 \
--expect-input u-boot.imx -O u-boot.imx.signed
rc=$?
set -e
case $rc in
0) echo "signed artifact: u-boot.imx.signed" ;;
3) echo "still awaiting approval; re-run with the same id ($ID) to resume" >&2
exit 1 ;;
4) echo "request $ID was rejected or failed" >&2
exit 1 ;;
*) echo "signing failed (exit $rc)" >&2
exit 1 ;;
esac
# 3. Pin the verification key that goes into the image.
signing-tool -c -a "$URL" -t @"$TOKENFILE" \
product getpubkey -P "$PRODUCT" --operid "$OPER" \
--fingerprint "$EXPECTED_PUBKEY_SHA256" -O uboot-verify.pem