Skip to content

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/HSS are stateful signature schemes. This is fundamentally different from every other algorithm in Crypto++ and cryptopp-modern. Each signature permanently consumes signer state. If that state is lost, duplicated, or rolled back, the key is compromised. 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. 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)

TypeTree HeightSignaturesPublic KeySignature
LMS_SHA256_M32_H553256 bytes1,292 bytes
LMS_SHA256_M32_H10101,02456 bytes1,452 bytes

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,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:

  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. 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 InsecureMemoryStateStore in 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 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);

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());
The example above uses 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:

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.
  • 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