Digital Signature Algorithms Reference
This page provides detailed documentation for the digital signature algorithms implemented in the CryptoHives.Foundation.Security.Cryptography package.
Namespace
using CryptoHives.Foundation.Security.Cryptography.Dsa;
Overview
| Algorithm | Source | Security Category | Primary Use |
|---|---|---|---|
| ML-DSA-44 | FIPS 204 | 2 | Constrained environments, high signing volume |
| ML-DSA-65 | FIPS 204 | 3 | Recommended default |
| ML-DSA-87 | FIPS 204 | 5 | Maximum security margin |
| SLH-DSA-{SHA2,SHAKE}-{128,192,256}{s,f} | FIPS 205 | 1/3/5 | Conservative hash-based option: roots of trust, firmware/code signing |
Why Post-Quantum Signatures
ML-DSA (Module-Lattice-Based Digital Signature Algorithm, derived from CRYSTALS-Dilithium) is the NIST-standardized post-quantum replacement for RSA and ECDSA signatures, which are broken by a cryptographically relevant quantum computer. Together with ML-KEM it forms the complete CNSA 2.0 key-establishment + signature pair. Signatures dominate real-world PQC demand: code signing, firmware updates, certificates, and document signing all need them.
This implementation is fully managed and runs identically on every target framework — including .NET Framework 4.6.2 and .NET Standard 2.0, where the in-box System.Security.Cryptography.MLDsa (.NET 10+, OS-backed) is not available.
ML-DSA (FIPS 204)
ML-DSA is specified in FIPS 204 (final, August 2024). It is a Fiat–Shamir-with-aborts signature scheme over module lattices, strongly unforgeable under chosen-message attack.
Parameters (FIPS 204 Table 1)
| Parameter | ML-DSA-44 | ML-DSA-65 | ML-DSA-87 |
|---|---|---|---|
| Security category | 2 | 3 | 5 |
| Matrix dimensions (k × ℓ) | 4 × 4 | 6 × 5 | 8 × 7 |
| η / τ / γ₁ | 2 / 39 / 2¹⁷ | 4 / 49 / 2¹⁹ | 2 / 60 / 2¹⁹ |
| Public key | 1,312 bytes | 1,952 bytes | 2,592 bytes |
| Secret key (expanded) | 2,560 bytes | 4,032 bytes | 4,896 bytes |
| Private seed ξ | 32 bytes | 32 bytes | 32 bytes |
| Signature | 2,420 bytes | 3,309 bytes | 4,627 bytes |
Two API Levels
| API | Classes | Best For |
|---|---|---|
| Key-holding (recommended) | MLDsa, MLDsaAlgorithm |
Application code; mirrors .NET 10's System.Security.Cryptography.MLDsa |
| Low-level, stateless | IDsa, MLDsa44, MLDsa65, MLDsa87 |
Protocol implementations managing raw key bytes; allocation-conscious span APIs |
Key-Holding API (MLDsa)
using CryptoHives.Foundation.Security.Cryptography.Dsa;
// Signer: generate a key pair and publish the public key.
using var signer = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);
byte[] publicKey = signer.ExportMLDsaPublicKey();
byte[] signature = signer.SignData(message);
// Verifier:
using var verifier = MLDsa.ImportMLDsaPublicKey(MLDsaAlgorithm.MLDsa65, publicKey);
bool valid = verifier.VerifyData(message, signature);
Context Strings
FIPS 204 binds signatures to an optional context string (≤ 255 bytes) for domain separation. A signature created with a context only verifies with the same context:
byte[] signature = signer.SignData(message, "MyApp/v1"u8);
bool valid = verifier.VerifyData(message, signature, "MyApp/v1"u8);
Key Storage via Private Seed
The 32-byte seed ξ is the compact private-key form; a key created from a seed re-expands deterministically:
using var key = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);
byte[] seed = key.ExportMLDsaPrivateSeed(); // 32 bytes — store this
using var restored = MLDsa.ImportMLDsaPrivateSeed(MLDsaAlgorithm.MLDsa65, seed);
// restored is byte-identical to the original key pair
Keys imported from an expanded private key (ImportMLDsaPrivateKey) hold no seed; on import the
public key is reconstructed from (ρ, s1, s2) and validated against the embedded hash
tr = H(pk) — a corrupted key is rejected with a CryptographicException.
Methods
The member names match System.Security.Cryptography.MLDsa exactly, so porting code
between the two is a using swap.
| Method | Description |
|---|---|
IsSupported |
Always true — see Comparison with .NET Built-in |
GenerateKey(MLDsaAlgorithm) |
Generate a fresh key pair (retains the private seed) |
ImportMLDsaPrivateSeed(MLDsaAlgorithm, ReadOnlySpan<byte>) |
Expand a 32-byte seed ξ into a key pair |
ImportMLDsaPrivateKey(MLDsaAlgorithm, ReadOnlySpan<byte>) |
Import an expanded private key (reconstructs and validates the public key) |
ImportMLDsaPublicKey(MLDsaAlgorithm, ReadOnlySpan<byte>) |
Import a public key (verify-only instance) |
SignData(data, context) / SignData(data, destination, context) |
Hedged (randomized) signing |
VerifyData(data, signature, context) |
Verification; wrong-length signatures return false |
SignPreHash(hash, oid, context) / SignPreHash(hash, destination, oid, context) |
HashML-DSA signing over a caller-supplied digest |
VerifyPreHash(hash, signature, oid, context) |
HashML-DSA verification |
ExportMLDsaPrivateSeed() / ExportMLDsaPublicKey() / ExportMLDsaPrivateKey() |
Key export (span overloads available) |
ImportPkcs8PrivateKey / ImportSubjectPublicKeyInfo |
Import from DER, with byte[] and span overloads |
ImportEncryptedPkcs8PrivateKey |
Import a password-protected key |
ImportFromPem / ImportFromEncryptedPem |
Import from RFC 7468 text |
ExportPkcs8PrivateKey / ExportSubjectPublicKeyInfo |
Export to DER, with TryExport… and …Pem forms |
ExportEncryptedPkcs8PrivateKey |
Export password-protected, PBES2 |
Dispose() |
Zeroize the private seed and private key |
Every import takes a byte[] as well as a ReadOnlySpan<byte>, and SignData/VerifyData/
SignPreHash/VerifyPreHash have the byte[]-based overloads the in-box type provides. See
Pre-Hash Variants for the overload hazard those
bring with them.
Pairwise Consistency Test
Key generation runs a sign/verify round trip on the fresh key pair, as FIPS 140-3 IG 10.3.A
expects of a validated module. It is the dominant cost of key generation, because a sign is
itself a rejection loop that runs several iterations on average. GenerateKey and
ImportMLDsaPrivateSeed take an optional pairwiseConsistencyTest argument to skip it:
using var key = MLDsa.ImportMLDsaPrivateSeed(
MLDsaAlgorithm.MLDsa65, seed, pairwiseConsistencyTest: false);
The test guards against a fault — bad memory, a bit flip, a miscompiled build — producing a key pair that does not round-trip. It cannot catch an implementation bug, since both halves of the test would be wrong in the same way. Disable it only where that trade is understood and key generation throughput actually matters. Skipping it never changes the key that is produced: the test message is derived from the seed, so expanding a stored seed stays fully deterministic and draws no entropy from the OS.
Low-Level API (IDsa)
using CryptoHives.Foundation.Security.Cryptography.Dsa;
using var dsa = MLDsa65.Create();
byte[] pk = new byte[MLDsa65.PublicKeySizeBytesConst]; // 1952
byte[] sk = new byte[MLDsa65.SecretKeySizeBytesConst]; // 4032
dsa.GenerateKeyPair(pk, sk);
byte[] signature = new byte[MLDsa65.SignatureSizeBytesConst]; // 3309
dsa.Sign(sk, message, context: default, signature);
bool valid = dsa.Verify(pk, message, context: default, signature);
SignDeterministic implements the deterministic variant (rnd = 0³²) for reproducibility
requirements and known-answer testing; the hedged Sign is the FIPS 204 default and
should be preferred because it protects against fault attacks and randomness reuse.
Deterministic key generation from a seed (GenerateKeyPair(seed, …)) exists for test
vectors and derived-key schemes.
SLH-DSA (FIPS 205)
SLH-DSA is specified in FIPS 205 (final, August 2024), derived from SPHINCS+. It is a stateless hash-based signature scheme: security rests only on the underlying hash functions — the most conservative assumption available — making it the preferred choice where lattice assumptions are distrusted: long-lived roots of trust, firmware and code signing, CA keys.
Choosing a Parameter Set
Twelve sets: SHA2 or SHAKE instantiation × security category 1/3/5 × s (small) / f (fast):
| Trade-off | s (small) | f (fast) |
|---|---|---|
| Signature size | ~2× smaller (7.9–29.8 KB) | larger (17.1–49.9 KB) |
| Signing speed | slow (~10⁶–10⁷ hash calls; seconds) | ~10× faster |
| Key generation | slower (larger top trees) | fast |
| Verification | fast | fast |
Guidance: prefer the f sets unless minimal signature size matters more than signing time (e.g. verification-heavy firmware distribution). Public keys are tiny for every set (32–64 bytes). If signing throughput matters at all, use ML-DSA instead — SLH-DSA is the conservative fallback, not the general-purpose choice.
Usage
using CryptoHives.Foundation.Security.Cryptography.Dsa;
using var signer = SlhDsa.GenerateKey(SlhDsaAlgorithm.SlhDsaShake128f);
byte[] publicKey = signer.ExportSlhDsaPublicKey(); // 32 bytes
byte[] signature = signer.SignData(message); // 17,088 bytes, hedged
using var verifier = SlhDsa.ImportSlhDsaPublicKey(SlhDsaAlgorithm.SlhDsaShake128f, publicKey);
bool valid = verifier.VerifyData(message, signature);
The member names are the in-box ones, so SlhDsa is a drop-in for System.Security.Cryptography.SlhDsa — change the using and the code compiles unchanged, on .NET Framework 4.6.2 upward. IsSupported is always true here, which is the practical difference: the in-box type needs SLH-DSA from CNG or OpenSSL 3.5+, and no shipping Windows build provides it.
Context strings (≤ 255 bytes) work exactly as with ML-DSA. The 4n-byte private key is itself the compact storage form (SK.seed ‖ SK.prf ‖ PK.seed ‖ PK.root); there is no separate private seed. Key generation includes a sign/verify pairwise consistency test (FIPS 140-3), which for s sets makes GenerateKey take seconds by design — GenerateKey(algorithm, pairwiseConsistencyTest: false) opts out where that matters.
Methods (SlhDsa)
| Method | Description |
|---|---|
IsSupported |
Always true — fully managed, never OS-dependent |
GenerateKey(SlhDsaAlgorithm) |
Generate a fresh key pair |
GenerateKey(SlhDsaAlgorithm, bool) |
As above, optionally skipping the pairwise consistency test |
ImportSlhDsaPrivateKey(SlhDsaAlgorithm, ReadOnlySpan<byte>) |
Import a 4n-byte private key (embedded public key is extracted) |
ImportSlhDsaPublicKey(SlhDsaAlgorithm, ReadOnlySpan<byte>) |
Import a 2n-byte public key (verify-only instance) |
SignData(data, context) / SignData(data, destination, context) |
Hedged (randomized) signing |
VerifyData(data, signature, context) |
Verification; wrong-length signatures return false |
SignPreHash(hash, oid, context) / SignPreHash(hash, destination, oid, context) |
HashSLH-DSA signing over a caller-supplied digest |
VerifyPreHash(hash, signature, oid, context) |
HashSLH-DSA verification |
ExportSlhDsaPublicKey() / ExportSlhDsaPrivateKey() |
Key export (span overloads available) |
ImportPkcs8PrivateKey / ImportSubjectPublicKeyInfo |
Import from DER, with byte[] and span overloads |
ImportEncryptedPkcs8PrivateKey |
Import a password-protected key |
ImportFromPem / ImportFromEncryptedPem |
Import from RFC 7468 text |
ExportPkcs8PrivateKey / ExportSubjectPublicKeyInfo |
Export to DER, with TryExport… and …Pem forms |
ExportEncryptedPkcs8PrivateKey |
Export password-protected, PBES2 |
Dispose() |
Zeroize the private key |
Every import, export, SignData, VerifyData, SignPreHash and VerifyPreHash has a byte[] overload beside the span one, matching the in-box type. Note the inherited hazard that comes with that: SignData(byte[], byte[]) binds the second argument as the context string, not as a destination buffer, and SignPreHash's span overload takes the destination second while its byte[] overload takes the OID second. To sign into a buffer you own, spell the spans out.
PKCS#8, SubjectPublicKeyInfo and PEM import/export are implemented; see Key Import and Export.
Validation
Same three-way playbook as ML-KEM/ML-DSA (see SLH-DSA Test Vectors): NIST ACVP known-answer tests (keyGen for all 12 sets — byte-exact keys through the full hypertree; byte-exact deterministic and hedged signatures; sigVer including modified R/SIGFORS/SIGHT/message and wrong-length rejections), BouncyCastle interop in both directions, and .NET 10 SlhDsa cross-checks where the OS supports it.
Unlike ML-KEM and ML-DSA, the embedded vector file is a stratified selection rather than everything runnable: SLH-DSA signatures are 7,856–49,856 bytes each, so the full set is 11.8 MB gzipped. The committed 180 cases still cover every parameter set, and a weekly CI job downloads and runs the complete 456-case set. See SLH-DSA Test Vectors for the rule and for how to run the full set locally.
Pre-Hash Variants (HashML-DSA / HashSLH-DSA)
Both schemes support the FIPS pre-hash variants (FIPS 204 §5.4, FIPS 205 §10.2), where the caller hashes the message once with an approved hash or XOF and signs the digest. This suits large messages, streaming, and CMS/X.509 workflows where only the digest reaches the signer. The signature binds the pre-hash function via its DER-encoded OID inside M′ = 0x01 ‖ |ctx| ‖ ctx ‖ OID ‖ PH(M), so pre-hash and pure signatures over the same message are never interchangeable.
using CryptoHives.Foundation.Security.Cryptography.Dsa;
using CryptoHives.Foundation.Security.Cryptography.Hash;
const string Sha512Oid = "2.16.840.1.101.3.4.2.3";
// Caller computes PH(M) once — here SHA-512.
using var sha512 = SHA512.Create();
byte[] digest = new byte[64];
sha512.TryComputeHash(largeMessage, digest, out _);
using var signer = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);
byte[] signature = signer.SignPreHash(digest, Sha512Oid);
bool valid = signer.VerifyPreHash(digest, signature, Sha512Oid);
To sign into a buffer you already own, use the span overload — note that it takes the destination
second, which is where the byte[] overload takes the OID:
byte[] signature = new byte[MLDsaAlgorithm.MLDsa65.SignatureSizeInBytes];
signer.SignPreHash(new ReadOnlySpan<byte>(digest), new Span<byte>(signature), Sha512Oid);
SlhDsa.SignPreHash/VerifyPreHash work identically. All twelve approved pre-hash functions are
accepted (SHA-2 family incl. SHA-512/224 and SHA-512/256, SHA-3 family, SHAKE128/256); the digest
length is validated against the OID, so a SHAKE128 digest must be 32 bytes and a SHAKE256 one 64.
Choose a pre-hash function that meets the parameter set's security category — e.g. SHA-512 for
ML-DSA-65/87. The API shape mirrors .NET 10's SignPreHash/VerifyPreHash member for member.
Key Import and Export
MLDsa and SlhDsa — and MLKem — carry the key format block the in-box
types define, on every target framework down to .NET Framework 4.6.2. Twenty-seven members: the
in-box signatures, minus the ones that would have moved a secret through a string, plus the
Span<char> PEM exports AsymmetricAlgorithm has and the in-box PQC types do not — see
Erasable Memory.
using var signer = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);
// PKCS#8 and SubjectPublicKeyInfo, plus the PEM form of the public half.
byte[] pkcs8 = signer.ExportPkcs8PrivateKey();
byte[] spki = signer.ExportSubjectPublicKeyInfo();
string pem = signer.ExportSubjectPublicKeyInfoPem();
// Password-protected, PBES2 with PBKDF2-HMAC-SHA256 and AES-256-CBC.
var pbe = new PbeOptions(PbeEncryptionAlgorithm.Aes256Cbc, Pbkdf2Prf.HmacSha256, 600_000);
// Characters are text: the scheme encodes them before the key derivation function sees
// them - UTF-8 here, big-endian UTF-16 for the legacy PKCS#12 schemes. This is what a
// password a human typed should use. Hold it in storage you own so you can clear it.
char[] typed = ReadPassword();
try
{
string encrypted = signer.ExportEncryptedPkcs8PrivateKeyPem(typed, pbe);
using var restored = MLDsa.ImportFromEncryptedPem(encrypted.AsSpan(), typed);
}
finally
{
CryptographicOperations.ZeroMemory(typed);
}
// Bytes are not text: they are the key derivation input, verbatim, with no encoding step.
// Reach for this only to reproduce a derivation this library would not otherwise produce.
// A UTF-8 literal is the one spelling of a fixed password that never creates a String -
// though it is compiled into the assembly, so it is no more erasable than a literal was.
ReadOnlySpan<byte> exactBytes = "correct horse"u8;
byte[] alsoEncrypted = signer.ExportEncryptedPkcs8PrivateKey(exactBytes, pbe);
Under PBES2 the character encoding is UTF-8, so the two agree for any text — they part company only on a legacy PKCS#12 file, where characters become big-endian UTF-16, or on byte inputs that are not valid UTF-8. See Erasable Memory.
The private key is written as a seed when there is one
ML-KEM and ML-DSA private keys are an ASN.1 CHOICE of a seed, an expanded key, or both. A key
that still holds its seed — one that was generated, or imported from a seed — exports the seed
arm; a key imported from expanded bytes exports the expanded arm. This matches .NET 10 byte
for byte, and it means a seed survives a PKCS#8 round trip rather than being silently traded for
2.5–4.9 KB of expanded key:
using var generated = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);
using var restored = MLDsa.ImportPkcs8PrivateKey(generated.ExportPkcs8PrivateKey());
// 34 bytes of privateKey, and the seed is still there.
byte[] seed = restored.ExportMLDsaPrivateSeed();
SLH-DSA has no seed and no CHOICE: FIPS 205 private keys are the raw 4n bytes
SK.seed ‖ SK.prf ‖ PK.seed ‖ PK.root, and the PKCS#8 privateKey OCTET STRING carries them
directly.
Password-based encryption
Exports always use PBES2 (PBKDF2 + AES-128/192/256-CBC), which is what .NET 10 writes.
Imports additionally accept the legacy PKCS#12 schemes, because that is what older OpenSSL and
Windows tooling emitted and a reader that rejects them cannot open real files. Those need TripleDES
or RC2 — ciphers this library deliberately does not implement — so that path alone delegates to the
platform. They are readable but not selectable: PbeEncryptionAlgorithm does not offer them. RC2 is
Windows-only on modern .NET and reports that rather than failing obscurely.
Passwords are taken as characters or as raw bytes, and the two are not interchangeable: a character password is UTF-8 encoded for PBES2 and big-endian UTF-16 for the PKCS#12 schemes, while a byte password is fed to the key derivation function exactly as given.
Erasable memory only
No member here takes a password, or returns a plaintext private key, as a string. A string
cannot be overwritten, so a secret placed in one survives on the heap until the collector reuses the
memory. Passwords are ReadOnlySpan<char> or ReadOnlySpan<byte>, and the plaintext-PEM export
writes into a buffer you own:
int size = key.GetPkcs8PrivateKeyPemSize();
char[] pem = ArrayPool<char>.Shared.Rent(size);
try
{
key.TryExportPkcs8PrivateKeyPem(pem, out int charsWritten);
// ... use pem.AsSpan(0, charsWritten) ...
}
finally
{
CryptographicOperations.ZeroMemory(pem.AsSpan(0, size));
ArrayPool<char>.Shared.Return(pem);
}
Use CryptographicOperations.ZeroMemory rather than Array.Clear or Span<T>.Clear().
Clearing a buffer you are about to discard is a dead store - nothing reads the zeros back - so a
compiler or JIT is free to delete the write and leave the secret in memory. ZeroMemory is marked
NoInlining | NoOptimization, which removes that liberty. This library exposes its own because the
in-box type does not exist below .NET Standard 2.1 and, on every version that has it, still takes
only a Span<byte>: there is no character overload in the box on any .NET version.
If a file imports both CryptoHives.Foundation.Security.Cryptography and
System.Security.Cryptography, the two type names collide (CS0104) - alias one side, e.g.
using Bcl = System.Security.Cryptography;.
string survives on exactly two members, whose content is public by construction:
ExportSubjectPublicKeyInfoPem (a public key) and ExportEncryptedPkcs8PrivateKeyPem (ciphertext).
This is closer to the in-box shape than it may look. AsymmetricAlgorithm — the base RSA, ECDsa
and DSA inherit — has never had a string password overload either, and has always offered the
Span<char> PEM exports. It is the newer in-box MLDsa/MLKem/SlhDsa that departed from that,
because they are standalone classes whose key-format block was written fresh rather than inherited.
Erasable Memory has the comparison, the reasoning, and how to port each of the
twenty-one members this replaces.
PbeOptions: the one deliberate divergence
The six encrypted-export members take a PbeOptions where the in-box types take a PbeParameters.
It is the only place the key-format surface differs, and it is a considered trade rather than an
omission:
PbeParametersarrived in .NET Standard 2.1, and this library targets .NET Framework 4.6.2 upward. Supplying the type ourselves would mean defining it in theSystem.Security.Cryptographynamespace, which collides — a hardCS0433— with any consumer that also references a package defining it. This package declares nothing outside its own namespace.PbeParametersnames its pseudorandom function withHashAlgorithmName, a strongly-typed string that accepts anything. PBES2 does not: it records the function as an OID, and RFC 8018 assigns those to a fixed handful.Pbkdf2Prfis an enum of exactly the ones that can be written, so an unencodable choice fails to compile instead of throwing at export.
Porting therefore needs an edit at those call sites and nowhere else — every import path, and the plain PKCS#8, SubjectPublicKeyInfo and PEM members, keep their in-box signatures:
// System.Security.Cryptography
var pbe = new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, 600_000);
// CryptoHives.Foundation.Security.Cryptography
var pbe = new PbeOptions(PbeEncryptionAlgorithm.Aes256Cbc, Pbkdf2Prf.HmacSha256, 600_000);
Reading is not restricted to that set. A key written elsewhere with, say, HMAC-SHA-224 still opens: the import path maps whatever OID the file carries.
Security Properties
- Hedged signing by default — each signature mixes fresh randomness into ρ″ per FIPS 204 Algorithm 2.
- Strong unforgeability — the hint encoding is strictly validated on decode (positions strictly increasing, counts consistent, padding zero); malformed signatures are rejected before any arithmetic.
- Constant-time discipline — infinity-norm checks on secret-dependent vectors scan all coefficients without early exit; the rejection-loop restart itself is spec-sanctioned to be observable. Rounding uses branch-free multiply-shift arithmetic.
- Key hygiene — per-iteration secrets (y, rejected z candidates, c·s products) and decoded key material are zeroed; fresh key pairs run a sign/verify pairwise consistency test (FIPS 140-3);
MLDsa.Dispose()zeroizes retained key material.
Validation
The implementation is validated on every target framework by three independent means (see ML-DSA Test Vectors):
- NIST ACVP known-answer tests — official vectors for keyGen (seed → byte-exact keys), sigGen (byte-exact signatures for both deterministic and hedged signing, the latter with injected ACVP randomness), and sigVer including modified-commitment/z/hint/message rejection cases — all parameter sets, pure ML-DSA, external interface.
- BouncyCastle interop — same-seed key generation produces byte-identical keys; deterministic signatures match byte-for-byte; hedged sign/verify round-trips in both directions.
- .NET 10
MLDsainterop (on supported OS builds) — same-seed keys match the Windows CNG implementation byte-for-byte; cross sign/verify in both directions including context binding.
Comparison with .NET Built-in
| Feature | CryptoHives MLDsa |
System.Security.Cryptography.MLDsa |
|---|---|---|
| Availability | All TFMs (.NET Framework 4.6.2+) | .NET 10+ only |
| OS requirement | None (fully managed) | Windows CNG (recent builds) / OpenSSL 3.5+ |
IsSupported |
Always true |
Depends on the OS build |
| Member names | Identical — porting is a using swap |
— |
| Parameter sets | ML-DSA-44/65/87 | ML-DSA-44/65/87 |
| Private seed import/export | ✅ | ✅ |
| Context strings | ✅ | ✅ |
| Deterministic signing | ✅ (IDsa.SignDeterministic) |
❌ |
| Pairwise consistency test opt-out | ✅ | ❌ |
| HashML-DSA (pre-hash) | ✅ | ✅ |
External-μ signing (SignMu/VerifyMu) |
🔲 Planned | ✅ |
| PKCS#8 / SPKI / PEM | ✅ | ✅ |
The one deferred row is held back deliberately: external-μ signing lands on its own, after the key formats that the whole post-quantum set needed first.
Signature Roadmap
| Algorithm | Standard | Status |
|---|---|---|
| ML-DSA-44/65/87 (pure) | FIPS 204 | ✅ Implemented |
| SLH-DSA, all 12 parameter sets (pure) | FIPS 205 | ✅ Implemented |
| HashML-DSA / HashSLH-DSA (pre-hash variants) | FIPS 204 §5.4 / FIPS 205 §10.2 | ✅ Implemented |
External-μ signing (SignMu/VerifyMu) |
FIPS 204 §6.2 | 🔲 Planned |
| Ed25519 | RFC 8032 | 🔲 Under review |
| PKCS#8 / SPKI / PEM key formats | RFC 5958 / RFC 5280 / RFC 7468 | ✅ Implemented |
See Also
- KEM Algorithms — ML-KEM, the key-establishment half of the PQC pair
- Hash Algorithms — the SHA-2 and SHAKE cores underlying ML-DSA and SLH-DSA
- FIPS 204 Reference / FIPS 205 Reference
- ML-DSA Test Vectors / SLH-DSA Test Vectors
- Cryptography Package Overview
© 2026 The Keepers of the CryptoHives