|
| 1 | +# LMS / XMSS Crypto Callback support |
| 2 | + |
| 3 | +This document describes the wolfSSL-side groundwork that lets LMS / HSS |
| 4 | +(RFC 8554) and XMSS / XMSS^MT (RFC 8391, both profiled in NIST SP 800-208) |
| 5 | +participate in the `WOLF_CRYPTO_CB` framework. With this layer in place, the |
| 6 | +wolfSSL PKCS#11 provider and the wolfHSM client can host stateful |
| 7 | +hash-based keys on a device without the wolfSSL public API changing. |
| 8 | + |
| 9 | +No HSM-side or PKCS#11-provider code lives in this layer. It only adds the |
| 10 | +dispatcher surface, the per-key device binding, and the helpers a backend |
| 11 | +needs to answer the request. |
| 12 | + |
| 13 | +## Why route stateful hash-based keys through a device |
| 14 | + |
| 15 | +LMS and XMSS are one-time-signature trees: the private key holds a counter |
| 16 | +that must be incremented on every signature, and signing the same index twice |
| 17 | +breaks the security proof. Moving that counter to a hardware module is the |
| 18 | +clean way to make the scheme operationally safe — the HSM is the natural |
| 19 | +owner of the index, and an attacker who steals a host snapshot cannot replay |
| 20 | +old indices. |
| 21 | + |
| 22 | +## PKCS#11 mapping |
| 23 | + |
| 24 | +PKCS#11 v3.1 standardised HSS and v3.2 added XMSS / XMSS^MT. The CryptoCb |
| 25 | +surface mirrors what those mechanisms expose: |
| 26 | + |
| 27 | +| wolfSSL API | PKCS#11 analog | CryptoCb dispatcher | |
| 28 | +|-----------------------|------------------------------------------------|----------------------------------------------| |
| 29 | +| `wc_LmsKey_MakeKey` | `CKM_HSS_KEY_PAIR_GEN` | `wc_CryptoCb_PqcStatefulSigKeyGen` | |
| 30 | +| `wc_LmsKey_Sign` | `CKM_HSS` (sign) | `wc_CryptoCb_PqcStatefulSigSign` | |
| 31 | +| `wc_LmsKey_Verify` | `CKM_HSS` (verify) | `wc_CryptoCb_PqcStatefulSigVerify` | |
| 32 | +| `wc_LmsKey_SigsLeft` | `CKA_HSS_KEYS_REMAINING` attribute | `wc_CryptoCb_PqcStatefulSigSigsLeft` | |
| 33 | +| `wc_XmssKey_MakeKey` | `CKM_XMSS_KEY_PAIR_GEN` / `CKM_XMSSMT_KEY_PAIR_GEN` | `wc_CryptoCb_PqcStatefulSigKeyGen` | |
| 34 | +| `wc_XmssKey_Sign` | `CKM_XMSS` / `CKM_XMSSMT` (sign) | `wc_CryptoCb_PqcStatefulSigSign` | |
| 35 | +| `wc_XmssKey_Verify` | `CKM_XMSS` / `CKM_XMSSMT` (verify) | `wc_CryptoCb_PqcStatefulSigVerify` | |
| 36 | +| `wc_XmssKey_SigsLeft` | XMSS remaining-sigs attribute | `wc_CryptoCb_PqcStatefulSigSigsLeft` | |
| 37 | + |
| 38 | +The four dispatchers are shared between LMS and XMSS, following the |
| 39 | +`wc_CryptoCb_PqcSign*` family used for Dilithium and Falcon. A new |
| 40 | +discriminator enum `wc_PqcStatefulSignatureType` (`WC_PQC_STATEFUL_SIG_TYPE_LMS`, |
| 41 | +`WC_PQC_STATEFUL_SIG_TYPE_XMSS`) tells the callback which of `LmsKey*` or |
| 42 | +`XmssKey*` the `void* key` field is. XMSS vs XMSS^MT is decided inside the |
| 43 | +callback via the existing `XmssKey::is_xmssmt` field. |
| 44 | + |
| 45 | +`Reload`, `GetKid`, and `ExportPub` are not routed through CryptoCb, but each |
| 46 | +is aware of HSM-backed keys: `Reload` short-circuits because state lives in |
| 47 | +the device, `GetKid` logs a warning since `priv_raw` may be uninitialised, |
| 48 | +and `ExportPub` preserves the source key's `devId` so the verify-only copy |
| 49 | +keeps dispatching through the same device. The external-backend variants |
| 50 | +(`ext_lms.c` / `ext_xmss.c`, selected by `--with-liblms` / `--with-libxmss`) |
| 51 | +are intentionally outside the scope of this layer and execute purely in |
| 52 | +software. |
| 53 | + |
| 54 | +## Per-key device binding |
| 55 | + |
| 56 | +Each key carries the device-binding fields that other key types |
| 57 | +(`RsaKey`, `ecc_key`, `dilithium_key`) already expose: |
| 58 | + |
| 59 | +```c |
| 60 | +struct LmsKey { |
| 61 | + /* ... existing fields ... */ |
| 62 | +#ifdef WOLF_CRYPTO_CB |
| 63 | + int devId; /* device identifier */ |
| 64 | + void* devCtx; /* opaque per-device state, owned by the callback */ |
| 65 | +#endif |
| 66 | +#ifdef WOLF_PRIVATE_KEY_ID |
| 67 | + byte id[LMS_MAX_ID_LEN]; /* device-side key identifier */ |
| 68 | + int idLen; |
| 69 | + char label[LMS_MAX_LABEL_LEN]; /* device-side key label */ |
| 70 | + int labelLen; |
| 71 | +#endif |
| 72 | +}; |
| 73 | +``` |
| 74 | + |
| 75 | +`XmssKey` carries the equivalent set under the same macro guards, with |
| 76 | +`XMSS_MAX_ID_LEN` / `XMSS_MAX_LABEL_LEN`. The `*_MAX_ID_LEN` and |
| 77 | +`*_MAX_LABEL_LEN` constants default to 32 and can be overridden by |
| 78 | +predefining the macros. |
| 79 | + |
| 80 | +`devCtx`, `id`, and `label` are storage only — wolfSSL never reads or writes |
| 81 | +them internally. Backends populate `devCtx` from the callback (typically the |
| 82 | +first time they touch the key) and consume `id` / `label` to resolve the |
| 83 | +on-device handle. |
| 84 | + |
| 85 | +## Public API additions |
| 86 | + |
| 87 | +```c |
| 88 | +/* Bind a key to a device-side identifier or label. */ |
| 89 | +#ifdef WOLF_PRIVATE_KEY_ID |
| 90 | +WOLFSSL_API int wc_LmsKey_InitId (LmsKey * key, const unsigned char * id, |
| 91 | + int len, void * heap, int devId); |
| 92 | +WOLFSSL_API int wc_LmsKey_InitLabel(LmsKey * key, const char * label, |
| 93 | + void * heap, int devId); |
| 94 | +WOLFSSL_API int wc_XmssKey_InitId (XmssKey* key, const unsigned char* id, |
| 95 | + int len, void* heap, int devId); |
| 96 | +WOLFSSL_API int wc_XmssKey_InitLabel(XmssKey* key, const char* label, |
| 97 | + void* heap, int devId); |
| 98 | +#endif |
| 99 | + |
| 100 | +/* Compute the digest of a message with the hash function dictated by |
| 101 | + * the parameter set. Useful for backends that follow the PKCS#11 v3.2 |
| 102 | + * CKM_HSS / CKM_XMSS / CKM_XMSSMT convention of operating on a |
| 103 | + * pre-computed digest (see "Sign / verify input format" below). */ |
| 104 | +WOLFSSL_API int wc_LmsKey_HashMsg (const LmsKey * key, const byte * msg, |
| 105 | + word32 msgSz, byte * hash, |
| 106 | + word32 * hashSz); |
| 107 | +WOLFSSL_API int wc_XmssKey_HashMsg(const XmssKey* key, const byte* msg, |
| 108 | + word32 msgSz, byte* hash, |
| 109 | + word32* hashSz); |
| 110 | +``` |
| 111 | +
|
| 112 | +The `Init*` helpers follow the `wc_InitRsaKey_Id` / `wc_InitRsaKey_Label` |
| 113 | +shape: they validate length bounds, delegate the rest of init to |
| 114 | +`wc_LmsKey_Init` / `wc_XmssKey_Init`, then copy id / label onto the key. |
| 115 | +
|
| 116 | +The `HashMsg` helpers honour the parameter set: |
| 117 | +
|
| 118 | +| Algorithm | Hash families covered | |
| 119 | +|-----------|-----------------------------------------------------------------| |
| 120 | +| LMS / HSS | SHA-256 (32 bytes), SHA-256/192 (24 bytes), SHAKE256 (32 / 24) | |
| 121 | +| XMSS / MT | SHA-256, SHA-512, SHAKE128, SHAKE256 (per `params->hash`) | |
| 122 | +
|
| 123 | +`*hashSz` is in / out: callers pass the buffer size and receive the digest |
| 124 | +length on success. |
| 125 | +
|
| 126 | +## Sign / verify input format |
| 127 | +
|
| 128 | +The CryptoCb dispatcher forwards the raw message to the callback. PKCS#11 |
| 129 | +v3.2 section 6.66.8 ("XMSS and XMSSMT without hashing") and the analogous |
| 130 | +text for HSS specify that those mechanisms take a pre-computed digest |
| 131 | +rather than the message. Backends that need that behaviour — typically |
| 132 | +PKCS#11 providers — call `wc_LmsKey_HashMsg` or `wc_XmssKey_HashMsg` from |
| 133 | +inside the callback to produce the algorithm-dictated digest. Backends |
| 134 | +that take the full message (typically wolfHSM) consume `msg` / `msgSz` |
| 135 | +directly. Picking one or the other is a callback decision; the dispatcher |
| 136 | +is agnostic. |
| 137 | +
|
| 138 | +## Build configuration |
| 139 | +
|
| 140 | +| `./configure` flag(s) | Effect | |
| 141 | +|--------------------------------------------------------|-------------------------------------------------------| |
| 142 | +| `--enable-lms --enable-xmss --enable-cryptocb` | Primary target. Full dispatcher and round-trip tests. | |
| 143 | +| `--enable-lms --enable-xmss` | New dispatcher code is fully `#ifdef`-elided. | |
| 144 | +| `--enable-cryptocb` | LMS / XMSS-less build; nothing CryptoCb-side breaks. | |
| 145 | +| `CPPFLAGS=-DWOLF_PRIVATE_KEY_ID …` | Adds `id` / `label` fields and the `Init*` helpers. | |
| 146 | +
|
| 147 | +## Verification |
| 148 | +
|
| 149 | +`./wolfcrypt/test/testwolfcrypt` exercises the full dispatcher round trip: |
| 150 | +inside `cryptocb_test`, `lms_test` and `xmss_test` run with the harness's |
| 151 | +registered `myCryptoDevCb`, which clears the key's `devId`, invokes the |
| 152 | +software API recursively, then restores `devId`. Sign and verify both go |
| 153 | +through the dispatcher, so the produced signatures self-verify within the |
| 154 | +harness. With no device registered, `lms_test` and `xmss_test` remain on the |
| 155 | +software path and produce bit-identical KAT output. |
| 156 | +
|
| 157 | +## Design notes |
| 158 | +
|
| 159 | +- **Shared dispatcher, separate type tag.** The eight LMS / XMSS operations |
| 160 | + collapse to four shared dispatchers (`KeyGen`, `Sign`, `Verify`, |
| 161 | + `SigsLeft`) keyed on `wc_PqcStatefulSignatureType`. The pattern matches |
| 162 | + the `PqcSign` family used for Dilithium / Falcon and reduces the surface |
| 163 | + area a backend has to implement. |
| 164 | +- **Verify carries `int* res`.** Following the Ed25519 / ECC / PqcVerify |
| 165 | + convention, the verify dispatcher reports validity through a separate |
| 166 | + `*res` flag, so a backend can distinguish a transport error from a |
| 167 | + forged signature. The wrapping wolfSSL function still translates |
| 168 | + `res != 1` to `SIG_VERIFY_E` for callers that do not see `res`. |
| 169 | +- **`SigsLeft` carries `word32* sigsLeft`.** PKCS#11 defines |
| 170 | + `CKA_HSS_KEYS_REMAINING` as a `CK_ULONG`-sized attribute; the callback |
| 171 | + uses `word32*` so an HSS key at its 2^32 limit can be expressed |
| 172 | + unambiguously. The wolfSSL public API still returns `int` and clamps at |
| 173 | + `0x7FFFFFFF`. |
| 174 | +- **HSM-backed keys skip the software write / read callbacks.** |
| 175 | + `wc_LmsKey_MakeKey` / `_Sign` and the XMSS equivalents dispatch through |
| 176 | + CryptoCb *before* validating `write_private_key` / `read_private_key` / |
| 177 | + `context`. A device-backed key does not need dummy software callbacks. |
| 178 | + On `CRYPTOCB_UNAVAILABLE` fall-through the software validations are |
| 179 | + re-applied as normal. |
0 commit comments