|
| 1 | +# Plan to implement LMS in MCUboot |
| 2 | + |
| 3 | +The LMS digital signature algorithm {insert RFC and FIPS references with links} is a post-quantum |
| 4 | +digital signature algorithm. It has some unusual features that make it a reasonable choice for a |
| 5 | +bootloader, even though these are limitations for other applications: |
| 6 | + |
| 7 | +- Keys are very small. The public key is a single SHA, for SHA-256 (recommended by CNSA-2.0), |
| 8 | + that's 32 bytes. The private key is generally 32-bytes plus a small counter. |
| 9 | +- Signatures are moderate. Larger than ECDSA or RSA signatures, but smaller than ML-DSA signature. |
| 10 | + The algorithm is parameterizable with the main tradeoff being between signature size, and speed of |
| 11 | + verification. |
| 12 | +- Verification is fast, typically on the order of 10s of ms, and consist entirely of the repeated |
| 13 | + application of the given hash function. |
| 14 | +- Importantly: it is based on a **one time signature** algorithm, LM-OTS. If the same underlying |
| 15 | + key is ever used to sign more than one message, security is broken. LMS builds a Merkle tree |
| 16 | + around this, allowing at key generation time, this balance to be determined. |
| 17 | + |
| 18 | +At key generation time, there are a small set of parameters. For the LM-OTS part: |
| 19 | + |
| 20 | +- N - size of hash function, 32 for SHA-256. |
| 21 | +- W - the width (in bits) of the Winternitz coefficients, this is the primary tradeoff value |
| 22 | + |
| 23 | +| w | p (chains) | Chain length (2^w) | Hash ops to verify (worst case) | Signature size (p×n bytes) | |
| 24 | +|---|---|---|---|---| |
| 25 | +| 1 | 265 | 2 | 265 | 8,480 bytes | |
| 26 | +| 2 | 133 | 4 | 399 | 4,256 bytes | |
| 27 | +| 4 | 67 | 16 | 1,072 | 2,144 bytes | |
| 28 | +| 8 | 34 | 256 | 8,704 | 1,088 bytes | |
| 29 | + |
| 30 | +For LMS, there are two parameters: |
| 31 | + |
| 32 | +- m - The number of bytes with each node, also 32, the size of the hash function. |
| 33 | +- h - The height of the merkel tree. |
| 34 | + |
| 35 | +The height determines the number of total signatures that can be made over the lifetime of the key. |
| 36 | +Larger values allow for more signatures to be made, but also increases the size of the message. |
| 37 | + |
| 38 | +| h | Total signatures | Additional signature bytes | |
| 39 | +|---|---|---| |
| 40 | +| 5 | 32 | 168 bytes | |
| 41 | +| 10 | 1,024 | 328 bytes | |
| 42 | +| 15 | 32,768 | 488 bytes | |
| 43 | +| 20 | 1,048,576 | 648 bytes | |
| 44 | +| 25 | 33,554,432 | 808 bytes | |
| 45 | + |
| 46 | +## Mbed TLS 4.1 constraints on LMS |
| 47 | + |
| 48 | +Mbed TLS 4.1.0 ships an LMS implementation in |
| 49 | +`ext/mbedtls-4.1.0/tf-psa-crypto/extras/lms.c` (header |
| 50 | +`tf-psa-crypto/include/mbedtls/lms.h`). The relevant facts that shape |
| 51 | +this plan: |
| 52 | + |
| 53 | +- The implementation **supports only one parameter set**: |
| 54 | + `MBEDTLS_LMS_SHA256_M32_H10` + `MBEDTLS_LMOTS_SHA256_N32_W8`. The w |
| 55 | + and h tradeoff tables above become theoretical — in practice we get |
| 56 | + h=10 (1024 signatures) and w=8 (1088-byte signature, ~8,700 SHA-256 |
| 57 | + ops to verify) and nothing else. |
| 58 | +- The library advertises only LMS *verification* by default |
| 59 | + (`MBEDTLS_LMS_C`). LMS *signing* requires the separate |
| 60 | + `MBEDTLS_LMS_PRIVATE`, and since mcuboot only verifies, we do not |
| 61 | + need to enable it. Signing is done externally by imgtool using a |
| 62 | + non-Mbed-TLS LMS library. |
| 63 | +- The public key is **56 bytes**, not 32: 4 bytes LMS type + 4 bytes |
| 64 | + LMOTS type + 16 bytes I (key id) + 32 bytes M-node. A future TLV |
| 65 | + design must allocate for 56 bytes. |
| 66 | + |
| 67 | +## Current state |
| 68 | + |
| 69 | +The work below was done on the `add-lms` branch. Commits in order: |
| 70 | + |
| 71 | +- `ext/mbedtls: rename submodule to ext/mbedtls-3.6.0` |
| 72 | +- `ext/mbedtls-4.1.0: add as second Mbed TLS submodule` |
| 73 | +- `ptest: ignore local .log output files` |
| 74 | +- `ext/tinycrypt: hmac.c: avoid Clang 19+ const-VLA diagnostic` |
| 75 | +- `sim: build Mbed TLS 4.1.0 via upstream CMake for mbedtls-v4` |
| 76 | +- `plan-lms: reflect CMake-driven Mbed TLS 4.1 build` |
| 77 | +- `sim: expand CI matrix to cover mbedtls-v4 combinations` |
| 78 | +- `boot: encrypted_psa: Mbed TLS 4.x compat and -Werror fixes` |
| 79 | +- `sim: enable genuine PSA encryption for sig-ecdsa-psa+enc-ec256+mbedtls-v4` |
| 80 | +- `sim: extend mbedtls-v4 PSA encryption to enc-aes256-ec256` |
| 81 | + |
| 82 | +### Dual-submodule arrangement |
| 83 | + |
| 84 | +Rather than replacing Mbed TLS outright, the submodule was renamed to |
| 85 | +`ext/mbedtls-3.6.0` and a second submodule `ext/mbedtls-4.1.0` was |
| 86 | +added alongside. This preserves the LTS-backed build paths for the |
| 87 | +legacy crypto simulator features (`sig-rsa`, `sig-ecdsa-mbedtls`, |
| 88 | +`enc-rsa`, `enc-ec256-mbedtls`, `enc-x25519-mbedtls`), whose APIs are |
| 89 | +demoted to `private/` headers in 4.x and would require substantial |
| 90 | +rework to port. New code and PSA-based features migrate to 4.1 |
| 91 | +incrementally via the `mbedtls-v4` Cargo feature. |
| 92 | + |
| 93 | +The Zephyr port is unaffected — it consumes Zephyr's own Mbed TLS |
| 94 | +module, not either of our submodules. |
| 95 | + |
| 96 | +### PSA features ported (CMake-driven) |
| 97 | + |
| 98 | +The 4.x build is driven through upstream's own CMake, matching the |
| 99 | +approach Zephyr adopted when it moved to Mbed TLS 4.1: stop shadowing |
| 100 | +the upstream build and let it own the file layout, generator |
| 101 | +plumbing, and config-adjustment logic. Mechanism: |
| 102 | + |
| 103 | +- `sim/mcuboot-sys/Cargo.toml`: `mbedtls-v4` feature and |
| 104 | + `cmake = "0.1"` build-dep. |
| 105 | +- `sim/mcuboot-sys/csupport/config-ec-psa-v4.h`: TF-PSA-Crypto 1.1.0 |
| 106 | + config using `PSA_WANT_*` macros. The 4.x |
| 107 | + `crypto_adjust_config_enable_builtins.h` infrastructure |
| 108 | + auto-enables the corresponding `MBEDTLS_*_C` internals. Gated |
| 109 | + blocks (`#if defined(MCUBOOT_SIGN_EC384)`, |
| 110 | + `#if defined(MCUBOOT_ENCRYPT_EC256)`, etc.) layer in algorithms |
| 111 | + for each enabled Cargo feature. |
| 112 | +- `sim/mcuboot-sys/build.rs`: new `add_mbedtls_v4_psa_ecdsa()` |
| 113 | + branch invokes the `cmake` crate on |
| 114 | + `ext/mbedtls-4.1.0/tf-psa-crypto` with `TF_PSA_CRYPTO_CONFIG_FILE`, |
| 115 | + `ENABLE_{PROGRAMS,TESTING}=OFF`, |
| 116 | + `USE_STATIC_TF_PSA_CRYPTO_LIBRARY=ON`, |
| 117 | + `TF_PSA_CRYPTO_FATAL_WARNINGS=OFF`, builds the `tfpsacrypto` |
| 118 | + target, and links the resulting `libtfpsacrypto.a`. CMake's own |
| 119 | + `GEN_FILES` path invokes the upstream Python generators for |
| 120 | + `psa_crypto_driver_wrappers*` and `tf_psa_crypto_config_check_*.h` |
| 121 | + — we don't replicate that plumbing. |
| 122 | +- `sim/mcuboot-sys/csupport/psa_rng_stub_v4.c`: self-contained stub |
| 123 | + for `mbedtls_psa_external_get_random` plus empty |
| 124 | + enable/disable-insecure-RNG toggles. The equivalent upstream test |
| 125 | + shim (`framework/tests/src/fake_external_rng_for_test.c`) drags in |
| 126 | + a large transitive header tree (`test_common.h` → |
| 127 | + `tf_psa_crypto_common.h` → `<test/build_info.h>`), which |
| 128 | + verification-only tests do not need. |
| 129 | +- `scripts/requirements.txt`: `jinja2`, `jsonschema` — required by |
| 130 | + upstream's generators. 4.x does not ship pre-generated files; |
| 131 | + CMake regenerates them on every configure. |
| 132 | + |
| 133 | +Feature combinations that build and pass 25/25 tests on `mbedtls-v4`: |
| 134 | + |
| 135 | +- `sig-ecdsa-psa` (base — signature verification only) |
| 136 | +- `sig-ecdsa-psa sig-p384` |
| 137 | +- `sig-ecdsa-psa` crossed with each of: `swap-move`, `swap-offset`, |
| 138 | + `bootstrap`, `max-align-16`, `validate-primary-slot`, |
| 139 | + `overwrite-only`, `multiimage`, `ram-load`, `direct-xip`, |
| 140 | + `overwrite-only downgrade-prevention`, |
| 141 | + `hw-rollback-protection multiimage`. |
| 142 | +- `sig-ecdsa-psa enc-ec256` — first real-PSA encryption path (vs. |
| 143 | + the 3.6 stub). Uses `encrypted_psa.c` for primitives + `encrypted.c` |
| 144 | + for the high-level `boot_enc_*` interface, with |
| 145 | + `CONFIG_BOOT_ECDSA_PSA` defined to skip `encrypted.c`'s duplicated |
| 146 | + legacy ASN.1/ECDH block. |
| 147 | +- `sig-ecdsa-psa enc-aes256-ec256` — same machinery, with |
| 148 | + `MCUBOOT_AES_256` defined. PSA_KEY_TYPE_AES covers all AES key |
| 149 | + sizes, so no config delta. |
| 150 | + |
| 151 | +### Build-time prerequisites for `mbedtls-v4` |
| 152 | + |
| 153 | +- `cmake` in PATH (most Rust developers already have it; the `cmake` |
| 154 | + crate requires it). |
| 155 | +- `python3` with `jinja2` and `jsonschema` available to the |
| 156 | + interpreter CMake finds via `find_package(Python3)`. |
| 157 | + |
| 158 | +### Gotchas / lessons learned |
| 159 | + |
| 160 | +- **Any `MCUBOOT_*` macro that gates `PSA_WANT_*` entries in |
| 161 | + `config-ec-psa-v4.h` must be forwarded to CMake**, not just to the |
| 162 | + `cc::Build`. Otherwise the library compiles without that algorithm |
| 163 | + enabled while the boot code uses it, yielding silent |
| 164 | + PSA_ERROR_NOT_SUPPORTED failures (or, in the P-384 case we hit, |
| 165 | + seven upgrade tests failing under `sig-p384 mbedtls-v4` because |
| 166 | + `PSA_WANT_ECC_SECP_R1_384` wasn't switched on in the library build). |
| 167 | + `add_mbedtls_v4_psa_ecdsa()` handles this via |
| 168 | + `cmake_conf.cflag("-DMCUBOOT_SIGN_EC384")` / |
| 169 | + `"-DMCUBOOT_ENCRYPT_EC256"`; any future gate macro needs the same. |
| 170 | +- **`encrypted.c` guards its legacy ASN.1/ECDH block with Zephyr |
| 171 | + Kconfig symbols** (`CONFIG_BOOT_ECDSA_PSA`, |
| 172 | + `CONFIG_BOOT_ED25519_PSA`) rather than `MCUBOOT_USE_PSA_CRYPTO`. |
| 173 | + Our sim build therefore defines `CONFIG_BOOT_ECDSA_PSA` — it's |
| 174 | + load-bearing, not cosmetic. This is a small OS-leakage that |
| 175 | + arguably ought to be cleaned up upstream (the guard should key on |
| 176 | + `MCUBOOT_USE_PSA_CRYPTO`, not a Zephyr-namespace symbol). |
| 177 | +- **`encrypted_psa.c`'s `bootutil_aes_ctr_{encrypt,decrypt}` ignore |
| 178 | + `blk_off`**. The tinycrypt and mbedtls versions both thread it |
| 179 | + through as the CTR sub-block offset. This may be fine for paths |
| 180 | + that stay block-aligned, but is almost certainly the cause of the |
| 181 | + x25519 failure below — the PSA-side comment claiming "PSA handles |
| 182 | + CTR block alignment internally" is not accurate. |
| 183 | + |
| 184 | +### Local carries |
| 185 | + |
| 186 | +- `ext/tinycrypt/lib/source/hmac.c` — Clang 19+ const-VLA diagnostic. |
| 187 | + See `TASKS.md`. Vendored tinycrypt, lives forever. |
| 188 | +- `boot/bootutil/src/encrypted_psa.c` — three carry patches for |
| 189 | + Mbed TLS 4.x + `-Werror -Wall -Wextra`: define |
| 190 | + `MBEDTLS_OID_EC_ALG_UNRESTRICTED` / `_EC_GRP_SECP256R1` when |
| 191 | + unavailable (they moved to a private header in 4.x), cast the |
| 192 | + `"MCUBoot_ECIES_v1"` string literal to `const uint8_t *`, mark |
| 193 | + `blk_off` as intentionally unused. Should flow back upstream as a |
| 194 | + proper 4.x compatibility fix; drop this carry when it does. |
| 195 | + |
| 196 | +### Known gaps |
| 197 | + |
| 198 | +- The main `~/ai/pythons` venv needs `jinja2` + `jsonschema` for |
| 199 | + `cargo test` to work without a PATH override. Update the uv-managed |
| 200 | + `pyproject.toml` alongside this plan's completion. |
| 201 | +- `sig-ecdsa-psa enc-x25519 mbedtls-v4` (and the aes256 variant) is |
| 202 | + wired but fails 7/25 upgrade-path tests. Private key parses, HKDF |
| 203 | + and MAC verification both succeed, but image-payload AES-CTR |
| 204 | + produces wrong bytes. Most likely cause is the `blk_off`-is-ignored |
| 205 | + issue in `encrypted_psa.c` noted above; full triage notes and a |
| 206 | + concrete resume step are in `TASKS.md`. The combo works on the 3.6 |
| 207 | + stub path, so this blocks only the 4.x port of those two features. |
| 208 | + |
| 209 | +## Plan |
| 210 | + |
| 211 | +- [x] Add Mbed TLS 4.1.0 alongside the 3.6 LTS as a second |
| 212 | + submodule (`ext/mbedtls-4.1.0`). |
| 213 | +- [x] Port one PSA-only simulator feature (`sig-ecdsa-psa`) to build |
| 214 | + against 4.1 via the `mbedtls-v4` Cargo feature. |
| 215 | +- [x] Resolve hand-picked-vs-CMake question: adopted CMake via the |
| 216 | + `cmake` crate for the 4.x build path, per the Zephyr precedent. |
| 217 | +- [x] Port remaining PSA-based simulator features to `mbedtls-v4` |
| 218 | + (done except for the x25519 variants): |
| 219 | + - [x] `sig-p384` — verified; uncovered and fixed a bug where |
| 220 | + `MCUBOOT_SIGN_EC384` wasn't being forwarded to the CMake build, |
| 221 | + so the library lacked P-384 support. |
| 222 | + - [x] `sig-ecdsa-psa swap-move bootstrap max-align-16` plus a |
| 223 | + broader set of orthogonal feature crosses (see above). |
| 224 | + - [x] `sig-ecdsa-psa enc-ec256` / `enc-aes256-ec256` — moved from |
| 225 | + the `psa_crypto_init_stub.c` path to genuine PSA-based |
| 226 | + encryption via `encrypted_psa.c`. |
| 227 | + - [ ] `sig-ecdsa-psa enc-x25519` / `enc-aes256-x25519` — tests |
| 228 | + fail, probably `blk_off` handling. See Known gaps and |
| 229 | + `TASKS.md`. |
| 230 | +- [x] Add and document a new image-trailer TLV type for LMS signatures |
| 231 | + in `boot/bootutil/include/bootutil/image.h` and `docs/design.md`. |
| 232 | + Allocated `IMAGE_TLV_LMS = 0x26` (next free slot after |
| 233 | + `IMAGE_TLV_SIG_PURE`). Added to the unprotected-TLV allow list in |
| 234 | + `image_validate.c`. `docs/design.md` now has a short subsection |
| 235 | + spelling out the RFC 8554 wire formats so the 56-byte public key |
| 236 | + (`u32 lms_type | u32 lmots_type | 16-byte I | 32-byte T[1]`) and |
| 237 | + 1452-byte signature (for the single `LMS_SHA256_M32_H10` + |
| 238 | + `LMOTS_SHA256_N32_W8` set Mbed TLS 4.x supports) are documented at |
| 239 | + the level a future reader will want. No new TLV is needed for the |
| 240 | + public key itself — the existing `IMAGE_TLV_PUBKEY` / `KEYHASH` |
| 241 | + pair covers it. |
| 242 | +- [x] Add LMS key generation and image signing to imgtool. Landed via |
| 243 | + the `pyhsslms` Pure-Python package (Russ Housley, RFC 8554 author; |
| 244 | + BSD-3-Clause). No pip-installable Python wrapper around a C LMS |
| 245 | + exists today — `pyca/cryptography` does not implement LMS, and |
| 246 | + `liboqs-python` requires a system `liboqs` install which would |
| 247 | + break a vanilla `pip install` of imgtool from PyPI. |
| 248 | + |
| 249 | + Implementation notes: |
| 250 | + |
| 251 | + - `scripts/imgtool/keys/lms.py` wraps `pyhsslms.LmsPrivateKey` / |
| 252 | + `LmsPublicKey`. Constructor takes `(lms_type, lmots_type)` so |
| 253 | + additional parameter sets are a one-line addition to |
| 254 | + `LMS_PARAM_SETS`. Initially exposes `lms-sha256-h10-w8` (the |
| 255 | + only set Mbed TLS 4.x verifies) and `lms-sha256-h5-w8` (much |
| 256 | + faster, useful for round-trip tests). |
| 257 | + - Private-key file is a custom PEM-style envelope (`-----BEGIN |
| 258 | + MCUBOOT LMS PRIVATE KEY-----`) holding the 60-byte serialized |
| 259 | + state `lms_type || lmots_type || SEED || I || q`. |
| 260 | + `keys/__init__.py:load()` sniffs the `BEGIN MCUBOOT LMS` marker |
| 261 | + before falling through to the cryptography PEM loader. |
| 262 | + - `sign_digest()` advances `q` in-memory then atomically rewrites |
| 263 | + the key file (tmpfile + fsync + rename) before returning the |
| 264 | + signature. If the disk write fails, no signature is produced; if |
| 265 | + a crash happens after the write but before the caller persists |
| 266 | + the signed image, an LMS index is wasted but never reused. The |
| 267 | + signing-policy hazard (re-signing from a stale backup) lives in |
| 268 | + the `lms.py` module docstring. |
| 269 | + - Public key is exported in the 56-byte RFC 8554 form (raw or |
| 270 | + PEM-wrapped). `image.py` uses it directly for both |
| 271 | + `IMAGE_TLV_PUBKEY` and `IMAGE_TLV_KEYHASH` (SHA-256 over the |
| 272 | + same 56 bytes). |
| 273 | + - `image.py:TLV_VALUES['LMS'] = 0x26` and a corresponding |
| 274 | + `ALLOWED_KEY_SHA[LMS] = ['256']` entry; the rest of the verify |
| 275 | + path picks up `verify_digest()` automatically since the LMS |
| 276 | + wrapper deliberately omits a `verify` method. |
| 277 | + |
| 278 | + Timing on a 2026 development laptop (CPython 3.13, single-thread): |
| 279 | + |
| 280 | + - h=5 keygen: 0.12 s; load (rebuilds the tree): 0.12 s. |
| 281 | + - h=10 keygen: 3.9 s; load: 3.9 s. Each `imgtool sign` invocation |
| 282 | + pays the load cost because pyhsslms only persists the 60-byte |
| 283 | + state, not the 1024-leaf precomputed tree. A long-lived signing |
| 284 | + daemon would amortize this; for a CLI invocation per build, 4 s |
| 285 | + on top of the existing build is tolerable but worth flagging. |
| 286 | + - Sign and verify (post-load) are both ~2 ms. |
| 287 | + - Signature size is 1452 bytes for h=10/w=8, matching |
| 288 | + `docs/design.md`. |
| 289 | + |
| 290 | + Tests in `scripts/tests/test_keys.py` parameterize over |
| 291 | + `keygens.keys()`; the LMS variants pass `test_keygen`, |
| 292 | + `test_getpub`, `test_getpubhash`, and `test_sign_verify`. |
| 293 | + `test_keygen_type` (uses `openssl pkey`) and `test_getpriv` |
| 294 | + (PKCS8/openssl formats) skip for LMS since LMS keys are not |
| 295 | + PEM/PKCS8. |
| 296 | +- [ ] Add simulator support for LMS signatures. |
| 297 | + - [x] Wire-format compatibility tests between imgtool's pyhsslms |
| 298 | + wrapper and the `lms-signature` crate the simulator will use |
| 299 | + (`sim/tests/lms_compat.rs`). Round-trips a fresh keypair in |
| 300 | + both directions (Python→Rust and Rust→Python verification), |
| 301 | + no committed key material; uses the H5/W8 set for keygen |
| 302 | + speed. `lms-signature` is now a regular `[dependencies]` of |
| 303 | + bootsim alongside `getrandom`, `rand_core`, `hybrid-array`, |
| 304 | + `signature`. |
| 305 | + - [x] Hook `lms-signature` into `TlvGen` so the simulator can |
| 306 | + sign LMS test images and feed them through the bootloader's |
| 307 | + verifier. The tests using this path will fail until the |
| 308 | + bootloader-side LMS verification below lands. |
| 309 | + |
| 310 | + Implementation notes: |
| 311 | + |
| 312 | + - `sim-lms` Cargo feature added in `sim/Cargo.toml` and |
| 313 | + `sim/mcuboot-sys/Cargo.toml`. No C code is gated by it yet — |
| 314 | + the bootloader-side verifier doesn't exist. |
| 315 | + - `TlvKinds::LMS = 0x26` matches `IMAGE_TLV_LMS` from the |
| 316 | + bootutil header. |
| 317 | + - `TlvGen::new_lms()` generates a fresh `SigningKey<LmsScheme>` |
| 318 | + per call. `LmsScheme` is `LmsSha256M32H5<LmsOtsSha256N32W8>` |
| 319 | + for now: H5 keygen runs in tens of ms, vs. seconds for H10. |
| 320 | + When bootloader-side LMS verification (Mbed TLS 4.x) is |
| 321 | + wired up, the alias must move to H10 because that is the |
| 322 | + only scheme Mbed TLS 4.x supports — both the alias and |
| 323 | + `LMS_SIG_LEN` (currently 1292; H10 = 1452) need updating |
| 324 | + together. |
| 325 | + - `make_tlv()` now takes `mut self: Box<Self>` so it can |
| 326 | + `take()` the LMS signing key. It emits a `KEYHASH` TLV |
| 327 | + (SHA-256 over the 56-byte serialized public key) followed |
| 328 | + by an `LMS` TLV containing the 1292-byte signature. |
| 329 | + - `image.rs:make_tlv()` short-circuits to `TlvGen::new_lms()` |
| 330 | + when `cfg!(feature = "sig-lms")` is set, since there is no |
| 331 | + `Caps::Lms` bit yet (no bootloader code to publish it). |
| 332 | + - `TlvGen` no longer derives `Debug` because |
| 333 | + `LmsSigningKey` does not implement it. Nothing in the tree |
| 334 | + printed `TlvGen` via `{:?}`. |
| 335 | + |
| 336 | + With `--features sig-lms`, all tests fail at the upgrade step |
| 337 | + ("Unable to perform basic upgrade") because the bootloader |
| 338 | + rejects the image. That confirms the simulator-side path is |
| 339 | + sound and isolates the remaining work to the C side. |
| 340 | +- [ ] Generalize the signing/verification interface as needed to |
| 341 | + support LMS (follow `plan-refactor.md`). For verification the |
| 342 | + interface should look much like the existing ones — no state to |
| 343 | + maintain at verify time. |
| 344 | +- [ ] Add LMS signature verification support to the bootloader: |
| 345 | + - Config knob (`MCUBOOT_SIGN_LMS` or similar) |
| 346 | + - Zephyr Kconfig support |
| 347 | + - Integration into the boot path (`image_lms.c` alongside the |
| 348 | + existing `image_ecdsa.c`, `image_rsa.c`, `image_ed25519.c`) |
| 349 | +- [ ] Expand `.github/workflows/sim.yaml` matrix to cover LMS |
| 350 | + (probably `sig-lms` feature, combined with the usual swap/validate/ |
| 351 | + enc matrix, though see constraint note below). |
| 352 | +- [ ] Retire `ext/mbedtls-3.6.0` once all features have been ported |
| 353 | + to 4.x (or explicitly dropped). 3.6 has LTS coverage until 2027, so |
| 354 | + no rush; this is a longer-term cleanup. |
| 355 | + |
| 356 | +## Notes for future work |
| 357 | + |
| 358 | +- LMS is verification-only in this plan; signing stays in imgtool. |
| 359 | + The stateful-private-key hazard is the single biggest operational |
| 360 | + risk — a signing-policy document may be warranted before this lands |
| 361 | + in production (not part of this branch's scope). |
| 362 | +- ML-DSA (Dilithium) source is already sitting in the 4.1 submodule |
| 363 | + at `ext/mbedtls-4.1.0/tf-psa-crypto/drivers/pqcp/mldsa-native/`. |
| 364 | + With the CMake path in place, adding a second PQC algorithm is |
| 365 | + primarily a matter of extending the config header and declaring |
| 366 | + the TLV/bootloader glue. |
0 commit comments