Skip to content

Commit c6aeaf9

Browse files
committed
docs: add docs/lms.md describing LMS signature support
Convert the working plan-lms.md document into proper user-facing documentation. The new docs/lms.md covers the algorithm and its parameters, the Mbed TLS 4.x constraints (single supported parameter set, verify-only, no PSA API yet), the imgtool signing path (pyhsslms, custom MCUBOOT LMS PEM envelope, atomic key-file rewrite), the bootloader verifier and its hash-and-sign rationale, the simulator's sig-lms plumbing including how it works around the 1024-signature H10 cap by regenerating from (id, seed), and operational considerations for anyone deploying LMS in a real signing pipeline. A TODO section captures the remaining work: Zephyr Kconfig + sample, generating LMS test keys at build time (a pilot for removing checked-in test keys for all sig algorithms), and porting to PSA when TF-PSA-Crypto exposes an LMS API. mbedtls-v4-port content from the working plan is intentionally not carried over — it belongs to a separate PR. docs/design.md gets a one-line pointer at the top of its existing LMS subsection. docs/index.md picks up an LMS entry next to the ECDSA one. Assisted-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: David Brown <david.brown@linaro.org>
1 parent c3c3efd commit c6aeaf9

3 files changed

Lines changed: 307 additions & 0 deletions

File tree

docs/design.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,9 @@ from the boot code.\
13391339

13401340
### [LMS signatures](#lms-signatures)
13411341

1342+
See [LMS](lms.md) for the algorithm description, imgtool/simulator/
1343+
bootloader integration, and operational considerations.
1344+
13421345
LMS (Leighton-Micali Signatures, RFC 8554) is a stateful hash-based
13431346
signature scheme intended for post-quantum authenticity of bootloader
13441347
images. MCUboot carries LMS signatures in `IMAGE_TLV_LMS` (0x26). Both

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ The MCUboot documentation is composed of the following pages:
4242
- [imgtool](imgtool.md) - image signing and key management
4343
- [ECDSA](ecdsa.md) - information about ECDSA signature formats
4444
- [Custom crypto backend](custom_crypto.md) - plugging in a custom crypto library
45+
- [LMS](lms.md) - LMS post-quantum signature support
4546
- [Serial Recovery](serial_recovery.md) - MCUmgr as the serial recovery protocol
4647
- Usage instructions:
4748
- [Zephyr](readme-zephyr.md)

docs/lms.md

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
# LMS signatures
2+
3+
## Overview
4+
5+
LMS (Leighton-Micali Signatures, RFC 8554) is a stateful hash-based
6+
signature scheme, recommended by NIST SP 800-208 and CNSA 2.0 for
7+
post-quantum authenticity of firmware images. Several of its
8+
properties make it a strong fit for a bootloader, even though they
9+
are limitations elsewhere:
10+
11+
- Public keys are very small (32 bytes when expressed as a hash; 56
12+
bytes in the RFC 8554 wire form actually carried by mcuboot — see
13+
[Image format](#image-format)).
14+
- Signatures are moderate in size — larger than ECDSA or RSA, but
15+
smaller than ML-DSA.
16+
- Verification is fast, on the order of tens of milliseconds, and is
17+
composed entirely of repeated SHA-256 invocations.
18+
- Most importantly, LMS is built around a **one-time signature**
19+
primitive (LM-OTS). Each underlying OTS leaf must sign at most
20+
once — reuse breaks security. LMS turns LM-OTS into a usable
21+
many-time signature by hanging OTS leaves off a Merkle tree, with
22+
the tree height fixed at key-generation time.
23+
24+
The stateful-key constraint is operational, not cryptographic: it
25+
shifts a hard problem from the verifier to the signer's key-storage
26+
discipline. For a typical OTA signing pipeline, where the signing
27+
key lives in a controlled environment and is consulted serially by a
28+
release process, this is a tractable trade in exchange for PQC
29+
security.
30+
31+
## Algorithm parameters
32+
33+
LMS keys are parameterized at generation time. The parameters split
34+
into two groups.
35+
36+
### LM-OTS (Winternitz)
37+
38+
LM-OTS uses a Winternitz one-time-signature construction. The two
39+
relevant parameters are:
40+
41+
- `n` — size of the hash function output. 32 bytes for SHA-256.
42+
- `w` — width in bits of each Winternitz coefficient. The primary
43+
size/speed trade-off:
44+
45+
| w | p (chains) | Chain length (2^w) | Hash ops to verify (worst case) | Signature size (p·n bytes) |
46+
|---|------------|--------------------|---------------------------------|----------------------------|
47+
| 1 | 265 | 2 | 265 | 8,480 |
48+
| 2 | 133 | 4 | 399 | 4,256 |
49+
| 4 | 67 | 16 | 1,072 | 2,144 |
50+
| 8 | 34 | 256 | 8,704 | 1,088 |
51+
52+
### LMS (Merkle tree)
53+
54+
- `m` — bytes per Merkle node. 32, matching SHA-256.
55+
- `h` — height of the Merkle tree. Determines the total number of
56+
signatures the key can ever produce, and adds `h` hash values to
57+
each signature:
58+
59+
| h | Total signatures | Additional signature bytes |
60+
|----|------------------|----------------------------|
61+
| 5 | 32 | 168 |
62+
| 10 | 1,024 | 328 |
63+
| 15 | 32,768 | 488 |
64+
| 20 | 1,048,576 | 648 |
65+
| 25 | 33,554,432 | 808 |
66+
67+
## Mbed TLS 4.x constraints
68+
69+
mcuboot's LMS verifier currently goes through Mbed TLS 4.x's
70+
implementation in `tf-psa-crypto/extras/lms.c`. That implementation
71+
imposes the parameters in practice:
72+
73+
- **Only one parameter set is supported**:
74+
`LMS_SHA256_M32_H10` + `LMOTS_SHA256_N32_W8` — h=10 (1024 signatures
75+
over the lifetime of one key), w=8 (1088-byte LM-OTS signature
76+
proper, 1452-byte total LMS signature, ~8,700 SHA-256 ops to
77+
verify). The w/h trade-offs above are theoretical until upstream
78+
extends the implementation.
79+
- **Only verification is shipped by default** (`MBEDTLS_LMS_C`); LMS
80+
signing requires `MBEDTLS_LMS_PRIVATE`. mcuboot does not enable it
81+
— signing happens out of tree in imgtool, using a separate Python
82+
library.
83+
- The PSA Crypto API does not yet expose an LMS interface. mcuboot
84+
calls `mbedtls_lms_*` directly, which Mbed TLS calls out as an
85+
intentional public exception until PSA grows `PSA_ALG_LMS` (see
86+
`1.0-migration-guide.md`, `psa-transition.md`, and
87+
`architecture/0e-plans.md` in the Mbed TLS 4.1 source tree). This
88+
is the only interface upstream offers today; we will move to the
89+
PSA API when it lands.
90+
91+
## Image format
92+
93+
The wire format of `IMAGE_TLV_LMS` (0x26), the LMS public key carried
94+
in `IMAGE_TLV_PUBKEY` / `IMAGE_TLV_KEYHASH`, and the rationale for
95+
the 56-byte public key and 1452-byte signature lengths, are
96+
documented in the bootloader design under
97+
[LMS signatures](design.md#lms-signatures). This document covers the
98+
algorithm, the imgtool/sim/bootloader plumbing, and the operational
99+
implications.
100+
101+
## Signing with imgtool
102+
103+
LMS signing is implemented in `scripts/imgtool/keys/lms.py`, which
104+
wraps the [`pyhsslms`](https://pypi.org/project/pyhsslms/) Pure-Python
105+
LMS library (Russ Housley, RFC 8554 author; BSD-3-Clause). pyhsslms
106+
was chosen because no pip-installable Python wrapper around a C LMS
107+
library exists today: `pyca/cryptography` does not implement LMS,
108+
and `liboqs-python` requires a system `liboqs` install which would
109+
break a vanilla `pip install imgtool` from PyPI.
110+
111+
### Key file
112+
113+
LMS private keys are not PEM/PKCS8 (the PEM ASN.1 universe has no
114+
agreed encoding for stateful-hash keys). imgtool stores them in a
115+
custom PEM-style envelope:
116+
117+
```
118+
-----BEGIN MCUBOOT LMS PRIVATE KEY-----
119+
<base64 of: lms_type || lmots_type || SEED || I || q>
120+
-----END MCUBOOT LMS PRIVATE KEY-----
121+
```
122+
123+
Total payload is 60 bytes: 4 bytes `lms_type`, 4 bytes `lmots_type`,
124+
32 bytes seed, 16 bytes key id `I`, 4 bytes leaf counter `q`. The
125+
`keys/__init__.py` loader recognises the `BEGIN MCUBOOT LMS` marker
126+
and dispatches before falling through to the `cryptography` PEM
127+
loader.
128+
129+
### Sign-time behaviour
130+
131+
Each call to `sign_digest()`:
132+
133+
1. Advances `q` in memory.
134+
2. Atomically rewrites the key file (tmpfile + fsync + rename)
135+
before returning the signature.
136+
3. If the rewrite fails, no signature is returned.
137+
138+
The hazard the user must manage is **never re-signing from a stale
139+
backup of the key file**: doing so reuses LM-OTS leaves and breaks
140+
LMS's security argument. This is documented in the `lms.py` module
141+
docstring; a production signing-policy document is likely warranted
142+
before any release uses this feature.
143+
144+
### Public key
145+
146+
The 56-byte RFC 8554 public key is exported as raw bytes or
147+
PEM-wrapped. `image.py` uses it directly for both `IMAGE_TLV_PUBKEY`
148+
and `IMAGE_TLV_KEYHASH` (SHA-256 over the same 56 bytes) — the
149+
public key is small enough that there is no real difference between
150+
"carry the key" and "carry the hash and look the key up".
151+
152+
### Cost
153+
154+
On a contemporary development laptop (CPython 3.13, single-thread):
155+
156+
- h=5: keygen 0.12 s, load 0.12 s, sign/verify ~2 ms.
157+
- h=10: keygen 3.9 s, load 3.9 s, sign/verify ~2 ms.
158+
159+
Each `imgtool sign` invocation pays the full load cost because
160+
pyhsslms only persists the 60-byte state, not the 1024-leaf
161+
precomputed tree. A long-lived signing daemon would amortize this;
162+
for a CLI invocation per build, ~4 s on top of the existing build is
163+
tolerable but worth flagging.
164+
165+
## Bootloader integration
166+
167+
The verifier is `boot/bootutil/src/image_lms.c`. It uses
168+
`mbedtls_lms_import_public_key` + `mbedtls_lms_verify` directly on
169+
the 56-byte serialized public key from `bootutil_keys[key_id]`.
170+
171+
### Hash-and-sign
172+
173+
mcuboot signs `SHA-256(image || protected_TLVs)` rather than the
174+
image bytes themselves. RFC 8554 defines LMS as a sign-the-message
175+
scheme (no "ph" / pre-hash variant exists), but a bootloader cannot
176+
in general load a full image into RAM — images may live behind a
177+
controller on external QSPI flash — and the Mbed TLS LMS verify API
178+
takes a single contiguous buffer. The hash-and-sign envelope keeps
179+
imgtool, the simulator, and the bootloader in agreement on what the
180+
"message" is.
181+
182+
The security argument is unchanged. A SHA-256 collision attack would
183+
break the hash-and-sign envelope, but LMS's own internal construction
184+
is also SHA-256-based — an effective break of SHA-256 already breaks
185+
LMS. This is the same trade-off that prevents the use of Ed25519 on
186+
this class of device.
187+
188+
### Buffer sizing
189+
190+
The LMS signature is 1452 bytes — much larger than ECDSA or RSA.
191+
`image_validate.c` was previously sharing one buffer for the hash,
192+
the public key, and the signature; this is now split into
193+
purpose-specific buffers so adding LMS does not push the hash and
194+
key buffers up to 1452 bytes apiece.
195+
196+
## Simulator (the `sig-lms` feature)
197+
198+
The `sig-lms` Cargo feature wires LMS through the simulator stack:
199+
200+
- `TlvGen` signs test images via the
201+
[`lms-signature`](https://crates.io/crates/lms-signature) crate,
202+
matching the `LMS_SHA256_M32_H10 + LMOTS_SHA256_N32_W8` parameter
203+
set the bootloader supports.
204+
- `sig-lms` automatically builds Mbed TLS 4.x for the bootloader side
205+
via `add_mbedtls_v4_psa_lms()` in `sim/mcuboot-sys/build.rs`
206+
there is no separate `mbedtls-v4` Cargo feature to enable.
207+
- An `lms_compat` integration test cross-checks wire-format
208+
compatibility between imgtool's pyhsslms wrapper and the
209+
`lms-signature` crate, in both directions (Python signs / Rust
210+
verifies, and vice versa).
211+
212+
### H10 exhaustion handling
213+
214+
The full sim test matrix easily exceeds the 1024-signature cap of a
215+
single H10 key. The simulator side-steps this by saving the 16-byte
216+
key identifier and 32-byte seed alongside the signing key and
217+
regenerating from `(id, seed)` whenever the key exhausts. RFC 8554's
218+
public-key derivation is deterministic in `(id, seed)`, so the
219+
regenerated key produces the same Merkle root and previously-signed
220+
images still verify against the same `bootutil_keys[]` entry.
221+
222+
This deliberately re-uses LM-OTS leaves across the regen — a real
223+
security violation in production. It is acceptable in the sim
224+
because the images never leave the test harness and the goal is
225+
wire-format round-trip, not forgery resistance. The mechanism is
226+
loudly documented at the singleton's definition in
227+
`sim/src/tlv.rs`.
228+
229+
### Process-wide singleton
230+
231+
LMS private keys are stateful, so a fresh keypair is generated once
232+
per process and shared across all tests. The 56-byte public key is
233+
pushed into a writable buffer in `keys.c` via the
234+
`mcuboot_sim_set_lms_pubkey` FFI hook, and `bootutil_keys[].key`
235+
points into that buffer — the bootloader-side verifier sees the
236+
same key the simulator just signed with.
237+
238+
### Threading
239+
240+
Mbed TLS's PSA core is only thread-safe with `MBEDTLS_THREADING_C`
241+
enabled, which our v4 sim configs do not set; the LMS verifier sits
242+
on top of `psa_hash_*` and inherits the constraint. The simulator's
243+
external-RNG stub also uses libc's non-thread-safe `rand()`. CI
244+
runs sig-lms test combinations with `--test-threads=1` — see
245+
`ci/sim_run.sh`.
246+
247+
## Operational considerations
248+
249+
For anyone deploying LMS in a real signing pipeline:
250+
251+
- **Never re-sign from a stale backup of the LMS private-key file.**
252+
Reusing LM-OTS leaves leaks enough hash-chain values for an
253+
adversary to forge — restoring a backup *after* signing has
254+
already advanced `q` will leak the key.
255+
- **Treat the key file as a source of truth.** imgtool's atomic
256+
rewrite (tmpfile + fsync + rename) is necessary but not sufficient:
257+
if a crash happens after the rewrite but before the caller
258+
persists the signed image, an LMS index is wasted (never reused),
259+
which is the safe outcome. Crashes that leave the key in any
260+
other state must not silently roll the file back.
261+
- **Plan for key exhaustion.** H10 caps a key at 1024 signatures.
262+
This is plenty for a single product line over many releases, but
263+
the signing infrastructure should track usage and rotate the
264+
signing key well before exhaustion, including the verifier-side
265+
key rollover required to accept a new public key.
266+
- **A signing-policy document is likely warranted** before LMS is
267+
used to sign release images. The single biggest operational risk
268+
is the stateful-private-key hazard above; it is not mitigated by
269+
the bootloader, only by procedure.
270+
271+
## TODO
272+
273+
The remaining work to reach a complete, mergeable LMS feature:
274+
275+
- **Zephyr Kconfig + sample.** Add `BOOT_SIGNATURE_TYPE_LMS` to
276+
`boot/zephyr/Kconfig`, wire it through `mcuboot_config.h` and
277+
`keys.c`, add `image_lms.c` to `boot/zephyr/CMakeLists.txt`, and
278+
add a build-only sample at `boot/zephyr/sample.yaml` so the
279+
bootloader is exercised in Zephyr's CI under LMS.
280+
- **Generate the LMS test key at build time, not commit it.**
281+
Stateful keys cannot be safely committed (each test run would
282+
re-use indices). The Zephyr sample should run `imgtool keygen` as
283+
a CMake step. This is a natural pilot for the broader goal of
284+
removing checked-in test keys for *all* signature algorithms — the
285+
build-time generation pattern designed for LMS should generalize
286+
to RSA / ECDSA / Ed25519.
287+
- **Move to the PSA API when upstream lands it.** When TF-PSA-Crypto
288+
exposes LMS through `psa_verify_hash` (or whatever interface the
289+
ongoing API design produces), `image_lms.c` should be ported off
290+
`mbedtls_lms_*` — the mbedtls-side APIs are public today only as
291+
a transitional measure for mechanisms with no PSA equivalent.
292+
293+
## Notes for future work
294+
295+
- LMS is verification-only in the bootloader. Signing remains in
296+
imgtool. Moving signing into the bootloader is not currently
297+
envisioned and would require either deep operational care or
298+
hardware-rooted state.
299+
- ML-DSA (Dilithium) source is already sitting in the Mbed TLS 4.1
300+
submodule at `tf-psa-crypto/drivers/pqcp/mldsa-native/`. With the
301+
CMake-driven 4.x build path in place for LMS, adding a second PQC
302+
algorithm is mostly a matter of extending the config header and
303+
wiring the TLV / bootloader plumbing.

0 commit comments

Comments
 (0)