LMS / HSS
Header: #include <cryptopp/lms.h> and #include <cryptopp/hss.h> | Namespace: CryptoPP
Since: cryptopp-modern 2026.6.0 (mixed parameter sets and LM-OTS W1/W2/W4: 2026.8.0)
Thread Safety: A signer instance must not be called concurrently. Separate signer instances using the same private key are safe only when their backend guarantees globally unique reservations and supports the intended concurrency model. Never use independent, uncoordinated state stores with the same private key. Verifiers are stateless and may be shared provided their key is not modified.
LMS (Leighton-Micali Signature) and HSS (Hierarchical Signature System) are hash-based signature schemes from NIST SP 800-208 and RFC 8554. Security relies on hash-function assumptions rather than lattice or number-theoretic assumptions.
The catch is that they are stateful. The library’s stateless signers (RSA, ECDSA, Ed25519, ML-DSA, SLH-DSA) can sign as many messages as you like without tracking anything between signatures. LMS/HSS cannot. Each signature uses a one-time index. Reusing that index is a total key compromise, not a degraded mode.
The API makes this explicit. Stateful signers use PK_StatefulSigner, a separate type that is intentionally not interchangeable with PK_Signer. Signing state is externalised through SignerStateStore, a backend interface that you provide. The library defines the contract; your deployment provides the durability.
LMS is the single-tree scheme. HSS stacks LMS trees in a hierarchy to multiply signing capacity.
When to use LMS/HSS
LMS/HSS makes sense when you need post-quantum signatures and either:
- You distrust lattice assumptions and want hash-only security
- Your signing volume is bounded and predictable (firmware signing, certificate issuance, code signing)
- You can manage signer state reliably in your deployment
If your signing pattern is high-volume or unpredictable, or you cannot guarantee state persistence, SLH-DSA is the stateless alternative. It has larger signatures and slower signing, but no state to manage.
If lattice assumptions are acceptable, ML-DSA gives you smaller signatures and faster signing with no state concerns.
Parameter Sets
LMS (single tree)
| Type | Tree Height | Signatures | Public Key | Signature (W=8) |
|---|---|---|---|---|
LMS_SHA256_M32_H5 | 5 | 32 | 56 bytes | 1,292 bytes |
LMS_SHA256_M32_H10 | 10 | 1,024 | 56 bytes | 1,452 bytes |
LM-OTS variants
The one-time signature layer comes in four Winternitz widths. W controls the trade between signature size and hashing work: higher W means smaller signatures and substantially more LM-OTS chain hashing.
| Type | W | Chains | Chain length | OTS signature |
|---|---|---|---|---|
LMOTS_SHA256_N32_W1 | 1 | 265 | 1 | 8,516 bytes |
LMOTS_SHA256_N32_W2 | 2 | 133 | 3 | 4,292 bytes |
LMOTS_SHA256_N32_W4 | 4 | 67 | 15 | 2,180 bytes |
LMOTS_SHA256_N32_W8 | 8 | 34 | 255 | 1,124 bytes |
The shipped LMS and uniform HSS typedefs all use W=8, the smallest-signature choice. The other widths are used through the LMS class templates directly or through mixed HSS hierarchies. The Choosing LMS/HSS Parameters guide works through the trade-offs.
HSS (hierarchical)
| Type | Levels | Per-Level | Total Signatures | Public Key | Signature |
|---|---|---|---|---|---|
HSS_SHA256_H5_W8_L2 | 2 | 32 | 1,024 | 60 bytes | 2,644 bytes |
HSS_SHA256_H10_W8_L2 | 2 | 1,024 | 1,048,576 | 60 bytes | 2,964 bytes |
HSS_SHA256_H5_W8_L3 | 3 | 32 | 32,768 | 60 bytes | 3,992 bytes |
HSS_SHA256_H5_W8_L4 | 4 | 32 | 1,048,576 | 60 bytes | 5,340 bytes |
HSS_SHA256_H10W4_H5W8_L2 | 2 | 1,024 / 32 | 32,768 | 60 bytes | 3,860 bytes |
Public key sizes in these tables are the raw LMS/HSS public-key sizes, not DER-encoded SubjectPublicKeyInfo sizes. The uniform typedefs repeat the same LMS/OTS pair at every level. HSS_SHA256_H10W4_H5W8_L2 is a mixed hierarchy: an H10/W4 root over an H5/W8 bottom tree, the configuration of RFC 8554 Appendix F Test Case 2. See Mixed hierarchies below for how level combinations are defined and which ones link out of the box.
Mixed hierarchies
An HSS parameter set is a list of two to four HSSLevel descriptors, top (root) level first. Each descriptor pairs an LMS parameter set with an LM-OTS parameter set for that level. This is how the shipped mixed typedef is defined in hss.h:
typedef HSS_Params<
HSSLevel<LMS_SHA256_M32_H10, LMOTS_SHA256_N32_W4>,
HSSLevel<LMS_SHA256_M32_H5, LMOTS_SHA256_N32_W8> > HSS_SHA256_H10W4_H5W8_L2_Params;
typedef HSS<HSS_SHA256_H10W4_H5W8_L2_Params> HSS_SHA256_H10W4_H5W8_L2;It provides Signer, Verifier, PrivateKey and PublicKey exactly like the uniform typedefs, and signatures interoperate with any RFC 8554 implementation configured the same way.
The HSS class templates are implemented in the library, not the header, and the library explicitly instantiates the five parameter sets in the table above. A new HSS_Params combination in application code compiles but fails at link time with undefined references. To add one, append template class instantiations for the four HSS classes to the block at the end of src/pqc/lms.cpp and rebuild the library. Single-tree LMS does not have this restriction: all eight LMS height/W combinations are instantiated.
cryptopp-modern rejects hierarchies outside its supported shape at compile time. These are implementation restrictions, not RFC 8554 rules (the RFC allows up to eight levels and does not require uniform N):
- Two to four levels.
- Every level must use the same LM-OTS hash output size N.
- Each level’s LMS hash output size M must match its LM-OTS N.
StaticAlgorithmName() for a mixed set lists the levels top-first, for example HSS[2]/(LMS-SHA256-M32-H10/LMOTS-SHA256-N32-W4,LMS-SHA256-M32-H5/LMOTS-SHA256-N32-W8). Uniform sets keep the short form.
For advanced use, HSS_Params exposes per-level accessors: LMSParamsAt<I>, OTSParamsAt<I>, LMSSignatureSizeAt<I>(), LMSPublicKeySizeAt<I>() and LeavesAt<I>(), with level 0 the root. Aggregates (TotalSignatures(), SignatureSize(), PublicKeySize()) cover the whole hierarchy.
HSS_Params moved from HSS_Params<LMS, OTS, LEVELS> to the HSSLevel list form, and the uniform member aliases LMSParameters and OTSParameters were replaced by the per-level accessors. The named scheme typedefs, wire formats, key encodings and state files are unchanged; only code that instantiated HSS_Params directly or read those members needs updating.Quick Example: LMS
#include <cryptopp/lms.h>
#include <cryptopp/stateful.h>
#include <cryptopp/osrng.h>
using namespace CryptoPP;
AutoSeededRandomPool rng;
// Generate key pair
LMSPrivateKey<LMS_SHA256_M32_H5, LMOTS_SHA256_N32_W8> privKey;
privKey.GenerateRandom(rng, g_nullNameValuePairs);
LMSPublicKey<LMS_SHA256_M32_H5, LMOTS_SHA256_N32_W8> pubKey;
privKey.MakePublicKey(pubKey);
// Testing only. NOT for production. See "State Management" below.
// For production use a durable backend appropriate to your deployment.
InsecureMemoryStateStore store(LMS_SHA256_M32_H5::TOTAL_LEAVES); // maximum signing capacity
// Sign
LMSSigner<LMS_SHA256_M32_H5, LMOTS_SHA256_N32_W8> signer(privKey, store);
SecByteBlock sig(signer.SignatureLength());
const byte msg[] = "Sign this message";
signer.SignMessage(rng, msg, sizeof(msg) - 1, sig);
// Verify. Verification is stateless and uses the conventional PK_Verifier interface.
LMSVerifier<LMS_SHA256_M32_H5, LMOTS_SHA256_N32_W8> verifier(
pubKey.GetPublicKeyBytePtr(), pubKey.GetPublicKeyByteLength());
bool valid = verifier.VerifyMessage(msg, sizeof(msg) - 1, sig, sig.size());Quick Example: HSS
#include <cryptopp/hss.h>
#include <cryptopp/stateful.h>
#include <cryptopp/osrng.h>
using namespace CryptoPP;
typedef HSS_SHA256_H5_W8_L2 Scheme; // convenience typedef
typedef HSS_SHA256_H5_W8_L2_Params Params; // parameter constants
AutoSeededRandomPool rng;
Scheme::PrivateKey privKey;
privKey.GenerateRandom(rng, g_nullNameValuePairs);
Scheme::PublicKey pubKey;
privKey.MakePublicKey(pubKey);
// Testing only. NOT for production. Use a durable backend.
InsecureMemoryStateStore store(Params::TotalSignatures()); // maximum signing capacity
Scheme::Signer signer(privKey, store);
Scheme::Verifier verifier(
pubKey.GetPublicKeyBytePtr(), pubKey.GetPublicKeyByteLength());
SecByteBlock sig(signer.SignatureLength());
const byte msg[] = "Sign this message";
signer.SignMessage(rng, msg, sizeof(msg) - 1, sig);
bool valid = verifier.VerifyMessage(msg, sizeof(msg) - 1, sig, sig.size());State Management
This is where LMS/HSS differs from every other signature scheme in the library. If you skip this section and treat the signer like a normal PK_Signer, you will break your keys.
The rule
Each signature consumes a one-time signing index. Once consumed, that index must never be used again. Not after a crash, not after a restart, not by another process. The state store enforces this.
How it works
The signer does not persist signing progress itself; key material and working state live in memory, while index allocation and durability are delegated to the SignerStateStore it is constructed with. After validating its arguments, each signing attempt:
- Reserves the next index from the store (this is the point of no return)
- Produces the signature
- Commits the reservation
If signing fails after reservation, the index is burned. It is gone, but safe. Callers should provide valid buffers and lengths; once reservation has occurred, failures burn capacity rather than retrying the same index.
What the library owns vs what your deployment owns
The library provides the signer framework and the SignerStateStore contract, not a one-size-fits-all persistence backend. Durable persistence is deployment-specific. The right backend depends on your storage environment, threat model, and coordination requirements.
Available store implementations
InsecureMemoryStateStore is for testing and examples only. State lives in memory and is lost when the process exits. A newly constructed store begins at zero. Do not use this in production.
FileStateStore is a reference implementation of the contract for single-writer local-filesystem environments. It writes state to a local file with write-ahead crash-tolerant semantics, and rejects simultaneous opens where the platform locking mechanism works. This is not a universal production answer. For anything beyond testing, use a durable backend appropriate to your deployment. That may be FileStateStore for simple single-writer environments, or a custom backend for other storage and coordination models.
// First run
FileStateStore store = FileStateStore::Create("signer.state",
Params::TotalSignatures());// Later runs
FileStateStore store = FileStateStore::Open("signer.state",
Params::TotalSignatures());Once the state file has been durably created, FileStateStore writes and flushes each reservation update before the reservation is returned. If the process crashes between the write and the signature, one index is lost. That is a burned capability, not a reused one.
What FileStateStore enforces
- Single-writer access. On POSIX an exclusive
flock(LOCK_EX | LOCK_NB)is acquired at open. On Windows the file is opened with no sharing viaCreateFileW. A secondFileStateStoreinstance trying to open the same file concurrently getsException::IO_ERROR. Cooperating processes only; an uncooperative process can defeat this by removing or renaming the file. - State-file size.
Open()verifies the file is exactly 64 bytes viafstaton POSIX andGetFileSizeExon Windows; rejects anything else withSignerStateIntegrityFailure. - Integrity.
Open()andIsHealthy()read and verify the complete state record; a failed state write poisons the open store, after whichReserveNext()andIsHealthy()throw. - Capability boundary. Each
StateReservationcarries the pointer to the store that issued it. A reservation cannot be committed or aborted against a different store; both paths throwSignerStateIntegrityFailure. - Non-zero capacity.
Create()andOpen()rejecttotalLeaves == 0withInvalidArgument.
Limitations of FileStateStore
- It does not provide durable anti-rollback across restart. If someone replaces the state file with a backup, the store will reopen and reissue old indices. True anti-rollback requires hardware support (TPM, RPMB) or an external monotonic counter.
- The integrity HMAC uses a deterministic default key unless the caller supplies one. The default catches accidental corruption but not adversarial tampering. If your threat model includes adversaries with file-system access, supply your own integrity key.
- The single-writer lock is advisory and process-cooperative. Removing or renaming the file defeats it; concurrent writers on different filesystems or NFS mounts may not coordinate. If your operational environment includes file replacement, you need a backend with stronger filesystem semantics.
Writing your own backend
If neither included backend fits your deployment, whether for embedded hardware, database-backed coordination, or HSM-internal counters, implement SignerStateStore directly. The interface is six methods:
class SignerStateStore {
public:
virtual StateReservation ReserveNext() = 0;
virtual void CommitReservation(const StateReservation &reservation) = 0;
virtual void AbortReservation(const StateReservation &reservation) = 0;
virtual bool IsExhausted() const = 0;
virtual bool IsHealthy() const = 0;
virtual uint64_t RemainingSignatures() const = 0;
};The invariant your implementation must satisfy: no signing index may ever be reissued. Lost indices are acceptable. Reused indices are not. If in doubt, burn the index and refuse to continue.
IsHealthy() is not a passive boolean probe. Backends may throw SignerStateIntegrityFailure rather than returning false when integrity can no longer be trusted.
CommitReservation() must be idempotent: committing the same valid reservation twice must not consume another index or corrupt state.
AbortReservation() confirms the burn. The index was already consumed by ReserveNext(), and abort acknowledges that signing did not complete. Backend implementations should treat this as a no-op or bookkeeping confirmation, not a rollback.
Thread safety of SignerStateStore methods is the backend’s responsibility. If your backend supports concurrent callers, serialise access to ReserveNext() internally. If it does not, document the single-caller expectation; the signer does not serialise concurrent calls, so coordination belongs to the caller or the backend.
Usage Guidelines
LMS/HSS is stateful. Misuse causes total key compromise.
- Never use
InsecureMemoryStateStorein production - Never delete and recreate a state file for an existing key
- Never open or use the same state file concurrently from multiple processes
- Never restore a state file from backup without understanding the implications
- Monitor
RemainingSignatures()and plan for key rotation before exhaustion
Do:
- Use a durable backend appropriate to your deployment for anything beyond testing
- Protect state files with the same care as private keys, but do not restore older state snapshots without understanding the rollback implications
- Use a single-writer process model
- Supply a separate integrity key to your state backend where supported, or use a documented, domain-separated derivation from protected key material
- Document your backend’s durability, rollback, and coordination assumptions explicitly
- Test your backend against the
SignerStateStorecontract before trusting it with real keys
Avoid:
- High-volume signing (consider SLH-DSA or ML-DSA instead)
- Environments where you cannot guarantee state persistence
- Multi-process signing against the same key without explicit coordination
- Assuming
FileStateStoreprovides rollback protection across restarts
Class Reference
LMSSigner / HSSSigner
Stateful signers that produce LMS or HSS signatures. These use PK_StatefulSigner, not PK_Signer. The distinction is intentional. Stateful and stateless signers have different operational requirements and should not be silently interchangeable.
// LMS
LMSSigner<LMS_SHA256_M32_H5, LMOTS_SHA256_N32_W8> signer(privKey, store);
// HSS
HSSSigner<HSS_SHA256_H5_W8_L2_Params> signer(privKey, store);The state store must outlive every signer bound to it. Signers hold a non-owning reference to the store; do not move or destroy the store while a signer still references it.
SignMessage
void SignMessage(RandomNumberGenerator &rng,
const byte *message, size_t messageLen,
byte *signature);Sign a message. Consumes one signing index. The signature buffer must point to at least SignatureLength() writable bytes. LMS/HSS signatures are fixed-length. There is no variable-length output.
Throws SignerExhausted if no indices remain. Throws SignerStateIntegrityFailure if the state backend detects corruption.
SignatureLength
size_t SignatureLength() const;RemainingSignatures
uint64_t RemainingSignatures() const;Number of signatures remaining. Never overcounts. Use for planning, not as a hard guarantee.
IsExhausted
bool IsExhausted() const;True if no signing indices remain.
LMSVerifier / HSSVerifier
Stateless verifiers using the conventional PK_Verifier interface. No state concerns.
// LMS
LMSVerifier<LMS_SHA256_M32_H5, LMOTS_SHA256_N32_W8> verifier(
pubKey.GetPublicKeyBytePtr(), pubKey.GetPublicKeyByteLength());
// HSS
HSSVerifier<HSS_SHA256_H5_W8_L2_Params> verifier(
pubKey.GetPublicKeyBytePtr(), pubKey.GetPublicKeyByteLength());VerifyMessage
bool VerifyMessage(const byte *message, size_t messageLen,
const byte *signature, size_t signatureLen) const;Returns true if the signature is valid for the message.
Key Classes
LMSPrivateKey / HSSPrivateKey
LMSPrivateKey<LMS_SHA256_M32_H5, LMOTS_SHA256_N32_W8> privKey;
privKey.GenerateRandom(rng, g_nullNameValuePairs);Private keys contain immutable seed material only. Signing progress lives in the state store, not in the key. Serialising a private key does not capture signing state.
LMSPublicKey / HSSPublicKey
LMSPublicKey<LMS_SHA256_M32_H5, LMOTS_SHA256_N32_W8> pubKey;
privKey.MakePublicKey(pubKey);MakePublicKey
void MakePublicKey(PublicKeyType &pub) const;Derives the public key from private key material. For HSS this involves computing the root Merkle tree, which can take a moment for larger parameter sets.
Key Serialisation
HSS public keys encode as X.509 SubjectPublicKeyInfo with the standards-facing HSS public-key format in the BIT STRING (RFC 9802). Single-tree LMS public keys currently place the raw LMS public key in the BIT STRING under the same algorithm OID, which is a cryptopp-modern-specific wrapping rather than the RFC 9802 form (the RFC expresses a single tree as HSS with L=1). Use the HSS classes where standards interoperability of encoded keys matters.
Private keys use a library-internal PKCS#8 wrapping that stores only the seed and identifier. This is not an RFC-defined format, is not portable across implementations, and does not include signing state. The private key encoding is for cryptopp-modern persistence only.
A conventional backup of the state file is not, by itself, a safe recovery mechanism. A snapshot may be restored only when no signatures were issued after that snapshot, or when an external monotonic mechanism can advance past every index that might have been used. Otherwise, retire the key. Treat the state store as part of the key, not as disposable metadata.
#include <cryptopp/filters.h>
#include <string>
// Continuing from the HSS quick example (Scheme, privKey, pubKey)
// Save public key (DER format)
std::string pubDer;
pubKey.DEREncode(StringSink(pubDer).Ref());
// Load public key
Scheme::PublicKey loadedPub;
loadedPub.Load(StringStore(pubDer).Ref());
// Save private key (library format, not portable)
std::string privDer;
privKey.DEREncode(StringSink(privDer).Ref());StringSource instead of StringStore, avoid the StringSource(der.c_str(), der.size()) overload: the size argument becomes a bool and the c-string is truncated at the first zero byte, silently corrupting binary DER. See the ML-KEM Loading Keys section for the safe forms.Security Considerations
Security assumptions
LMS/HSS security depends on:
- Hash function second-preimage resistance
- Hash function preimage resistance
These are conservative, well-studied assumptions.
The state problem
The main operational risk with LMS/HSS is not cryptographic. It is state management. If your state store fails to prevent index reuse, the scheme is broken regardless of hash function strength. This is why the API makes statefulness explicit rather than hiding it.
Signing capacity
LMS/HSS keys have finite signing capacity. An H5 tree gives you 32 signatures. An HSS L=2 H10 gives you about a million. Plan your tree height for your expected signing volume. Running out mid-deployment means generating a new key pair, which for use cases like certificate issuance or firmware signing implies new trust anchors and key distribution. Plan capacity before deploying the public key.
Object Identifiers
LMS and HSS share a single OID per RFC 9708:
| Algorithm | OID | Accessor |
|---|---|---|
| LMS / HSS | 1.2.840.113549.1.9.16.3.17 | ASN1::id_alg_hss_lms_hashsig() |
The same OID is used for all LMS and HSS parameter sets. The parameter set is identified by type codes embedded in the public key, not by the OID.
Specification Compliance
The implementation follows four specifications:
- RFC 8554 defines LMS and HSS signature generation and verification.
- NIST SP 800-208 is the recommendation for stateful hash-based signature schemes. cryptopp-modern implements the RFC 8554 algorithms using parameter sets approved by SP 800-208; an SP 800-208-conformant signing deployment has additional hardware-module, private-key non-export and operational requirements, and
FileStateStoredoes not satisfy those deployment requirements by itself. - HSS public-key encoding follows RFC 9802. Standalone LMS public-key encoding currently uses the cryptopp-modern-specific wrapping described under Key Serialisation.
- RFC 9708 assigns the OID.
HSS verification has been tested against RFC 8554 Appendix F Test Cases 1 and 2 (Test Case 2 is the mixed-parameter vector).
See Also
- Stateful Signing Integration Guide covers the state contract, backends, and common mistakes.
- NIST SP 800-208 is the recommendation for stateful hash-based signatures.
- RFC 8554 is the LMS/HSS specification itself.
- SLH-DSA is the stateless hash-based alternative. No state management required.
- ML-DSA is the lattice-based post-quantum signature scheme.
- PQC Overview compares all PQC algorithms.