Skip to content

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/HSS are stateful signature schemes. Unlike the library’s stateless signature schemes, each signature permanently consumes signer state. If state is duplicated or rolled back, or signing resumes from an older value after state loss, indices may be reused and the key is compromised. If current state is lost completely, retire the key unless your deployment has a safe recovery mechanism. Read the State Management section before writing any production code.

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)

TypeTree HeightSignaturesPublic KeySignature (W=8)
LMS_SHA256_M32_H553256 bytes1,292 bytes
LMS_SHA256_M32_H10101,02456 bytes1,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.

TypeWChainsChain lengthOTS signature
LMOTS_SHA256_N32_W1126518,516 bytes
LMOTS_SHA256_N32_W2213334,292 bytes
LMOTS_SHA256_N32_W4467152,180 bytes
LMOTS_SHA256_N32_W88342551,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)

TypeLevelsPer-LevelTotal SignaturesPublic KeySignature
HSS_SHA256_H5_W8_L22321,02460 bytes2,644 bytes
HSS_SHA256_H10_W8_L221,0241,048,57660 bytes2,964 bytes
HSS_SHA256_H5_W8_L333232,76860 bytes3,992 bytes
HSS_SHA256_H5_W8_L44321,048,57660 bytes5,340 bytes
HSS_SHA256_H10W4_H5W8_L221,024 / 3232,76860 bytes3,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.

Source compatibility change in 2026.8.0: 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:

  1. Reserves the next index from the store (this is the point of no return)
  2. Produces the signature
  3. 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 via CreateFileW. A second FileStateStore instance trying to open the same file concurrently gets Exception::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 via fstat on POSIX and GetFileSizeEx on Windows; rejects anything else with SignerStateIntegrityFailure.
  • Integrity. Open() and IsHealthy() read and verify the complete state record; a failed state write poisons the open store, after which ReserveNext() and IsHealthy() throw.
  • Capability boundary. Each StateReservation carries the pointer to the store that issued it. A reservation cannot be committed or aborted against a different store; both paths throw SignerStateIntegrityFailure.
  • Non-zero capacity. Create() and Open() reject totalLeaves == 0 with InvalidArgument.

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 InsecureMemoryStateStore in 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 SignerStateStore contract 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 FileStateStore provides 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());
If you load through 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:

AlgorithmOIDAccessor
LMS / HSS1.2.840.113549.1.9.16.3.17ASN1::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 FileStateStore does 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