LMS / HSS
Header: #include <cryptopp/lms.h> and #include <cryptopp/hss.h> | Namespace: CryptoPP
Since: cryptopp-modern 2026.6.0
Thread Safety: Signers must not be shared concurrently. Use separate signer and state-store instances unless your backend explicitly defines a safe coordination model. Verifiers are stateless and can be shared.
LMS (Leighton-Micali Signature) and HSS (Hierarchical Signature System) are hash-based signature schemes from NIST SP 800-208 and RFC 8554. Their security rests entirely on hash function properties. No lattice assumptions, no number theory.
The catch is that they are stateful. Every other signer in this library (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 |
|---|---|---|---|---|
LMS_SHA256_M32_H5 | 5 | 32 | 56 bytes | 1,292 bytes |
LMS_SHA256_M32_H10 | 10 | 1,024 | 56 bytes | 1,452 bytes |
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,996 bytes |
All parameter sets use SHA-256 with LM-OTS W=8 and uniform per-level parameters (same LMS/OTS type at every level). RFC 8554 allows mixed parameter sets across levels, but this implementation does not currently support them.
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 manage its own state. Instead, it takes a SignerStateStore reference and delegates state tracking to it. Every SignMessage() call:
- 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. Every restart reuses indices from zero. Do not use this in production.
FileStateStore is a reference implementation of the contract for single-writer, single-process desktop/server environments. It writes state to a local file with write-ahead crash-tolerant semantics. 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-process environments, or a custom backend for other storage and coordination models.
// First use: create a new state file
FileStateStore store = FileStateStore::Create("signer.state",
Params::TotalSignatures());
// Subsequent runs: reopen the existing state file
FileStateStore store = FileStateStore::Open("signer.state",
Params::TotalSignatures());FileStateStore advances the counter on disk before returning each reservation. If the process crashes between the write and the signature, one index is lost. That is a burned capability, not a reused one.
Limitations of FileStateStore
- It detects accidental corruption and, with a caller-provided integrity key, can detect wrong-key attachment or unauthorised modification. It does not provide durable anti-rollback across restart.
- It does not prevent an older valid file from being restored after a 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.
- It is single-writer only. Two processes writing the same state file will silently reuse indices.
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.
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 and let the signer enforce it.
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 share a state file between 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
- Provide an integrity key to your state backend derived from your private key where supported
- 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);SignMessage
void SignMessage(RandomNumberGenerator &rng,
const byte *message, size_t messageLen,
byte *signature);Sign a message. Consumes one signing index. The signature buffer must be exactly SignatureLength() 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
Public keys encode as X.509 SubjectPublicKeyInfo using the standards-facing LMS/HSS public-key format (RFC 9802).
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.
To fully back up a signing identity, you need the private key encoding and the current state store contents. Restoring an older state snapshot risks index reuse. Treat the state store as part of the key, not as disposable metadata.
// Save public key (DER format, interoperable)
std::string pubDer;
pubKey.DEREncode(StringSink(pubDer).Ref());
// Load public key
HSSPublicKey<Params> loadedPub;
StringSource(pubDer, true, new Redirector(loadedPub));
// Save private key (library format, not portable)
std::string privDer;
privKey.DEREncode(StringSink(privDer).Ref());StringSource(pubDer, true, ...) deliberately. The StringSource(der.c_str(), der.size()) form looks similar but compiles to a different overload, the size 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. No lattice problems, no number theory.
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. Get the capacity right from the start.
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.
- RFC 9802 covers the X.509 SubjectPublicKeyInfo encoding for LMS/HSS public keys.
- RFC 9708 assigns the OID.
HSS verification has been tested against RFC 8554 Appendix F Test Case 1 from the Cisco hash-sigs reference implementation.
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.