Expand description
§pqc-sig — Post-Quantum Digital Signatures
A standalone, WASM-compatible library implementing post-quantum signature algorithms:
-
ML-DSA (NIST FIPS 204) — Primary standard, pure Rust, WASM-native.
ml-dsafeature (default)fips204::MlDsa44Keypair— Security Level 2fips204::MlDsa65Keypair— Security Level 3 (recommended)fips204::MlDsa87Keypair— Security Level 5
-
SLH-DSA (NIST FIPS 205) — Stateless hash-based, pure Rust, WASM-native.
slh-dsafeature (default)- SHA2 variants: 128s/128f, 192s/192f, 256s/256f
- SHAKE variants: 128s/128f, 192s/192f, 256s/256f
-
FN-DSA (NIST FIPS 206 / Falcon) — requires
fndsafeature, pure Rust, WASM-compatiblefips206::FnDsa512Keypair— Security Level 1fips206::FnDsa1024Keypair— Security Level 5- Multikey/DID Document encoding works via a provisional
0x307-reserved private-use multicodec code (no upstream multiformats/multicodec registration exists yet) — seetypes::FN_DSA_PRIVATE_USE_BASE.
-
Hybrid Ed25519 + ML-DSA-65 — requires
hybridfeature; classical/PQC combiner for migration bridging
§Quick Start
use pqc_sig::fips204::MlDsa65Keypair;
use rand::rngs::OsRng;
// Generate a keypair
let keypair = MlDsa65Keypair::generate(&mut OsRng).unwrap();
let pk = keypair.public_key();
// Sign a message
let message = b"Hello, post-quantum world!";
let signature = keypair.sign(&mut OsRng, message).unwrap();
// Verify the signature
MlDsa65Keypair::verify(&pk, message, &signature).unwrap();§Domain separation
Every algorithm family (ML-DSA, SLH-DSA, and — behind the fndsa feature — FN-DSA)
also exposes a sign_ctx/sign_ctx_deterministic/verify_ctx trio that takes a FIPS
204/205/206 “context” byte string (≤ MAX_CONTEXT_LEN bytes) in addition to the
message. Use this to bind a signature to a specific application or protocol so that a
signature produced for one purpose cannot be replayed as valid for another:
use pqc_sig::fips204::MlDsa65Keypair;
use rand::rngs::OsRng;
let keypair = MlDsa65Keypair::generate(&mut OsRng).unwrap();
let pk = keypair.public_key();
let msg = b"Hello, post-quantum world!";
let sig = keypair.sign_ctx(&mut OsRng, b"8gentz-agent-v1", msg).unwrap();
MlDsa65Keypair::verify_ctx(&pk, b"8gentz-agent-v1", msg, &sig).unwrap();
// A signature made under one context MUST NOT verify under another.
assert!(MlDsa65Keypair::verify_ctx(&pk, b"8gentz-fabric-v1", msg, &sig).is_err());An empty context (&[]) is byte-identical to the plain, non-ctx API: sign_ctx(&[], m)/sign_ctx_deterministic(&[], m) produce the same signature as sign/
sign_deterministic, and verify_ctx(pk, &[], m, sig) accepts anything verify does
(and vice versa). See MAX_CONTEXT_LEN for the 255-byte limit shared by all three
FIPS standards, and examples/domain_separation.rs for a runnable walkthrough.
§Pre-hash signing (large payloads)
Every algorithm family also exposes a sign_prehash/sign_prehash_deterministic/
verify_prehash trio for signing very large payloads (multi-MB WASM modules, log
bundles, …) without holding the whole message in memory twice. The caller
hashes the payload once with an approved prehash::PreHash function — this crate
never hashes on the caller’s behalf — and passes the resulting digest:
use pqc_sig::fips204::MlDsa65Keypair;
use pqc_sig::prehash::PreHash;
use rand::rngs::OsRng;
let keypair = MlDsa65Keypair::generate(&mut OsRng).unwrap();
let pk = keypair.public_key();
// The host already hashes large payloads ad hoc; feed that digest in directly.
let digest = [0x11u8; 64]; // stand-in for e.g. Sha512::digest(&module_bytes)
let sig = keypair
.sign_prehash_deterministic(b"8gentz-module-v1", PreHash::Sha512, &digest)
.unwrap();
MlDsa65Keypair::verify_prehash(&pk, b"8gentz-module-v1", PreHash::Sha512, &digest, &sig)
.unwrap();For ML-DSA and SLH-DSA, this is the literal FIPS HashML-DSA/HashSLH-DSA
construction (FIPS 204 §5.4 Algorithm 4/5, FIPS 205 §10.2.2 Algorithm 23/25): it
builds M' = 0x01 ‖ len(ctx) ‖ ctx ‖ OID(hash) ‖ digest and signs/verifies M' via
the upstream ml-dsa/slh-dsa crates’ sign_internal/verify_internal and
slh_sign_internal/slh_verify_internal primitives — both confirmed pub and
reachable in the pinned versions. This is byte-for-byte interoperable with any other
conformant HashML-DSA/HashSLH-DSA implementation. FN-DSA’s sign_prehash/
verify_prehash use the upstream fn-dsa crate’s native HashIdentifier
pre-hash support directly (FIPS 206 draft framing, performed by fn-dsa itself).
The pre-hash function’s collision strength must be at least the parameter set’s
security strength (each keypair type exposes this as SECURITY_STRENGTH_BITS):
128 bits for ML-DSA-44/SLH-DSA-128*/FN-DSA-512, 192 bits for ML-DSA-65/
SLH-DSA-192*, 256 bits for ML-DSA-87/SLH-DSA-256*/FN-DSA-1024. Violating this
returns SigError::PreHashTooWeak; a digest of the wrong length for the chosen
prehash::PreHash returns SigError::InvalidDigestLength. A pre-hash signature
does not verify via verify/verify_ctx (and vice versa) — the leading 0x01
domain byte structurally separates the two modes. See examples/prehash_large_payload.rs
for a runnable walkthrough with a multi-MB payload.
§Hybrid bridge (migrating a classical identity)
The hybrid feature’s hybrid::HybridSigner also supports bridging an existing
classical Ed25519 identity — rather than only generating a brand-new one — via
hybrid::HybridSigner::from_ed25519_secret, plus a secret_key()/
from_secret_key_bytes pair for persisting both halves across restarts, and its own
domain-separated sign_ctx/verify_ctx trio (S-1 parity, with a crate-defined
Ed25519 context framing since RFC 8032 has none — see
hybrid::HYBRID_CTX_FRAME_DOMAIN). See examples/hybrid_bridge.rs for a runnable
walkthrough and docs/SAGP_NOTES.md for Wave-1 migration guidance.
§WASM Usage
This crate is a pure library (rlib-only) and is never built as a standalone WASM
artifact itself. For WASM/JS bindings, use the sibling pqc-sig-wasm crate.
§Encoding only
With default-features = false and no algorithm feature, the crate is the encoding
surface alone: SigAlgorithm, SigPublicKey, Signature, SignedMessage,
multicodec codes, W3C Multikey, base64url and JSON. No signature implementation is
compiled in, and CI checks the dependency graph to keep it that way. Use this when you
only carry keys and signatures between systems, for example in a DID tool or an SDK
whose signing happens elsewhere. The encodings are byte-identical to the full build’s.
pqc-sig = { version = "0.5", default-features = false, features = ["std"] }§no_std Support
This crate is no_std-compatible with alloc. Disable the std feature and keep the
algorithms you use:
pqc-sig = { version = "0.5", default-features = false, features = ["ml-dsa", "slh-dsa"] }§pqc-sig
Tier: Production · MSRV: 1.87 · License: MIT OR Apache-2.0 · Not independently audited, see the crate family
Post-quantum digital signatures — a standalone, WASM-compatible Rust library implementing NIST-standardized post-quantum signature algorithms.
Reading the badges: CI is the build-and-test signal — it should be green, and a red one means something is actually broken. wasm builds the core crate for
wasm32-unknown-unknown, runswasm-pack buildon the siblingpqc-sig-wasmcrate, and executes thetest-wasm.mjsfunctional suite under Node — see.github/workflows/wasm.yml. cargo-deny is the dependency-advisory signal — also expected green. It previously carried an accepted-and-documented exception (the optionalfndsafeature depended on the now-archivedpqcrypto-falcon); that dependency was migrated to a pure-Rust, actively-maintained replacement, so there is currently nothing accepted or suppressed — seeSECURITY.md.
Verification: 132 known-answer test vectors are published in
tests/vectors/ (ML-DSA-44/65/87 and 7 SLH-DSA parameter sets; keys and
sigVer cases sourced from NIST ACVP-Server,
signatures self-generated deterministically by this crate over that externally-sourced key
material — see tests/vectors/README.md for the exact,
per-vector provenance) and executed on every CI run via
tests/kat_tests.rs.
§What runs today vs. what is designed
Runs today:
-
ML-DSA-44/65/87 (FIPS 204) and all 12 SLH-DSA parameter sets (FIPS 205) — pure Rust,
no_std+alloc, directly compilable towasm32-unknown-unknown, keygen/sign/verify covered by integration tests. -
FN-DSA-512/1024 (Falcon, FIPS 206 draft), gated behind the
fndsafeature flag — pure Rust via thefn-dsacrate,no_std+alloc, directly compilable towasm32-unknown-unknown. No C compiler needed. -
Hybrid Ed25519 + ML-DSA-65, gated behind the
hybridfeature flag —HybridSignercombines a classical and a post-quantum signature; verification requires both to pass. For bridging classical deployments to PQC during migration. Pure Rust, WASM-compatible. -
SigPublicKey::to_multibase()/from_multibase()— W3C Multikey encoding for all 17 algorithms, including FN-DSA. ML-DSA/SLH-DSA use officially registered (draft-status) multicodec codes, independently verified against themultibaseandssi-multicodeccrates (seetests/ssi_interop_test.rs).⚠️ FN-DSA Multikey codes are provisional, not upstream-registered.
to_multibase()/from_multibase()encode FN-DSA-512/1024 public keys using a private-use multicodec code this project self-assigned (0x307000/0x307001), because multiformats/multicodec has no registered code for FN-DSA/Falcon as of this writing (checked against the live table; no open PR or issue for it either).SigAlgorithm::is_private_use_multicodec()flags this at runtime so callers can warn when they’re producing a non-standard Multikey. Interop scope: 0x307-controlled systems only — a generic third-party multicodec decoder will not recognize0x307000/0x307001as FN-DSA. Revisit trigger: if/whenmultiformats/multicodecmerges a real FN-DSA/Falcon code,multicodec_code()migrates to it in a follow-up minor release (tracked insrc/types.rs’sFN_DSA_PRIVATE_USE_BASEdoc comment, which has the full rationale). Seedocs/MULTICODEC.mdfor the full code table (registered vs. provisional status for every algorithm) and the upstream-registration tracking checklist.
§FN-DSA status
FN-DSA (Falcon) support is optional — enable it with the fndsa feature flag; it is
never compiled into the default build. FIPS 206
itself is a NIST draft standard, not yet finalized, and FN-DSA’s Multikey/DID Document
multicodec codes (0x307000/0x307001) are provisional, private-use codes this project
self-assigned — see docs/MULTICODEC.md for the full rationale and
tracking status. Never use FN-DSA as a default signature algorithm for interop purposes
— PRIMARY_ALGORITHM (ML-DSA-65) is the only algorithm this crate
designates as a default, and it uses a registered multicodec code.
- Standalone WASM/JS bindings for ML-DSA and SLH-DSA live in the sibling
pqc-sig-wasmcrate (see WASM Usage below) — this crate itself is a pure library (rlib-only) and is never the final linked WASM artifact. FN-DSA and the hybrid combiner have no WASM bindings yet (though both compile towasm32as a library dependency — see above). - A WIT Component Model interface at
wit/pqc-sig.witdescribing the ML-DSA and SLH-DSA interfaces for non-Rust WASM runtimes. - CI (
.github/workflows/ci.yml) builds and tests both default features and--all-featureson every push/PR, including against the packagedcargo packageartifact, thepqc-sig-wasmcdylib artifact, and a downstream-consumer fixture that would otherwise mask ano_std/std-leak regression (see Continuous Integration).
Designed, not yet implemented / provisional:
- FIPS 206 itself is a NIST draft standard, not yet finalized (see the FN-DSA badge above) — the implementation tracks the draft and may need breaking changes once it’s finalized.
- MSRV,
unsafecode policy, and docs.rs metadata are now stated below (see Build Requirements and Safety).
§Performance
Measured, not estimated. Every figure in BENCHMARKS.md
is generated by benches/signatures.rs in the same commit, with the CPU,
toolchain and build profile stated beside it.
There are no deterministic stand-ins, no mock adapters and no stub backends anywhere in this crate, so there is no configuration in which a benchmark measures a hash instead of a lattice. FN-DSA sits behind a feature and the harness panics rather than skipping when it is off, because a run that quietly measures nothing still reports success.
Reproduce it:
cargo bench --features fndsa --bench signatures§When to choose FN-DSA over ML-DSA
Both are NIST post-quantum signature standards; the choice is a tradeoff, not a strict upgrade in either direction:
| ML-DSA-65 (recommended default) | FN-DSA-512 | |
|---|---|---|
| Public key | 1952 bytes | 897 bytes |
| Signature | ≤3309 bytes | ≤666 bytes |
| WASM-compatible (as a library dependency) | ✅ Yes (pure Rust) | ✅ Yes (pure Rust, via fn-dsa) |
| Dependency | Pure Rust (ml-dsa) | Pure Rust (fn-dsa) |
| Multikey encoding | Officially registered multicodec | Provisional 0x307 private-use code |
| Side-channel history | None known | Falcon’s reference floating-point sampling has a documented history of non-constant-time implementations in some revisions (see SECURITY.md) |
Choose FN-DSA when: signature/key size is the binding constraint (e.g. bandwidth-limited transport, on-chain storage, high-frequency signing where payload size dominates) and you can accept the current lack of upstream Multikey standardization (see the provisional-codes callout above).
Choose ML-DSA when: Multikey interop with third-party (non-0x307) multicodec tooling
matters — which is the common case, hence ML-DSA-65 remaining this crate’s
PRIMARY_ALGORITHM.
§What’s new in 0.5.0: encoding without the algorithms
ML-DSA and SLH-DSA are now features, ml-dsa and slh-dsa, both on by default. With
default-features = false and neither enabled, pqc-sig is just the key and signature
types and their encodings: multicodec codes, W3C Multikey, base64url and JSON. No
signature implementation is compiled in. See Encoding only.
This is a breaking change for one kind of consumer: if you use default-features = false,
add the algorithms you use, e.g. features = ["ml-dsa", "slh-dsa"]. Default-feature users
see no change, and every encoding is byte-identical to 0.4.2
(tests/encoding_vectors.rs).
§What’s new in 0.4.0 — Domain separation & pre-hash
Every algorithm family (ML-DSA, SLH-DSA, and — behind fndsa — FN-DSA) now exposes a
sign_ctx/verify_ctx trio (FIPS 204/205/206 context strings) and a
sign_prehash/verify_prehash trio (HashML-DSA/HashSLH-DSA for large payloads),
plus the hybrid feature can now bridge an existing Ed25519 identity. See
docs/SAGP_NOTES.md for the SAGP-specific policy this maps onto.
Domain-separated signing — bind a signature to a specific purpose
(examples/domain_separation.rs):
let sig = keypair.sign_ctx(&mut OsRng, b"8gentz-agent-v1", msg)?;
MlDsa65Keypair::verify_ctx(&pk, b"8gentz-agent-v1", msg, &sig)?;Pre-hash signing for large payloads — sign only a digest, never the whole payload
twice (examples/prehash_large_payload.rs):
let digest = Sha512::digest(&module_bytes);
let sig = keypair.sign_prehash_deterministic(b"8gentz-module-v1", PreHash::Sha512, &digest)?;
MlDsa65Keypair::verify_prehash(&pk, b"8gentz-module-v1", PreHash::Sha512, &digest, &sig)?;Hybrid bridge for an existing classical identity — wrap an already-deployed Ed25519
key instead of generating a new one
(examples/hybrid_bridge.rs):
let signer = HybridSigner::from_ed25519_secret(&mut OsRng, &legacy_ed25519_seed)?;
let sig = signer.sign_ctx(&mut OsRng, b"8gentz-agent-v1", msg)?;§Release
| Version | Date | Artifacts |
|---|---|---|
| v0.5.1 | 2026-10-23 | crates.io |
| v0.5.0 | 2026-09-24 | crates.io |
| v0.4.0 | 2026-09-09 | crates.io |
| v0.3.0 | 2026-09-02 | crates.io |
| v0.2.1 | 2026-09-01 | crates.io |
| v0.2.0 | 2026-09-01 | crates.io |
| v0.1.0 | 2026-08-01 | pqc-sig-v0.1.0-wasm.zip |
§Algorithms
| Algorithm | Standard | Security Level | Public Key | Signature | WASM |
|---|---|---|---|---|---|
| ML-DSA-44 | FIPS 204 | 2 (128-bit) | 1312 B | 2420 B | ✅ |
| ML-DSA-65 | FIPS 204 | 3 (192-bit) | 1952 B | 3309 B | ✅ |
| ML-DSA-87 | FIPS 204 | 5 (256-bit) | 2592 B | 4627 B | ✅ |
| SLH-DSA-SHA2-128s | FIPS 205 | 1 | 32 B | 7856 B | ✅ |
| SLH-DSA-SHA2-128f | FIPS 205 | 1 | 32 B | 17088 B | ✅ |
| SLH-DSA-SHA2-192s | FIPS 205 | 3 | 48 B | 16224 B | ✅ |
| SLH-DSA-SHA2-192f | FIPS 205 | 3 | 48 B | 35664 B | ✅ |
| SLH-DSA-SHA2-256s | FIPS 205 | 5 | 64 B | 29792 B | ✅ |
| SLH-DSA-SHA2-256f | FIPS 205 | 5 | 64 B | 49856 B | ✅ |
| SLH-DSA-SHAKE-128s | FIPS 205 | 1 | 32 B | 7856 B | ✅ |
| SLH-DSA-SHAKE-128f | FIPS 205 | 1 | 32 B | 17088 B | ✅ |
| SLH-DSA-SHAKE-192s | FIPS 205 | 3 | 48 B | 16224 B | ✅ |
| SLH-DSA-SHAKE-192f | FIPS 205 | 3 | 48 B | 35664 B | ✅ |
| SLH-DSA-SHAKE-256s | FIPS 205 | 5 | 64 B | 29792 B | ✅ |
| SLH-DSA-SHAKE-256f | FIPS 205 | 5 | 64 B | 49856 B | ✅ |
| FN-DSA-512 (Falcon) | FIPS 206‡ | 1 | 897 B | 666 B | ✅* |
| FN-DSA-1024 (Falcon) | FIPS 206‡ | 5 | 1793 B | 1280 B | ✅* |
*FN-DSA is pure Rust and WASM-compatible. Enable with --features fndsa.
‡FIPS 206 (FN-DSA/Falcon) is not yet finalized; it is currently a draft standard pending ratification.
Recommended: ML-DSA-65 for general use (best balance of security and performance).
§Build Requirements
- Rust 1.87 or later (edition 2021). This is the crate’s declared
rust-version(MSRV). Themsrvjob inci.ymlchecks it on every push and PR. It proves that a set of dependency versions compatible with Rust 1.87 exists and builds, with a pinned Rust 1.87.0 toolchain. It resolves that set with Cargo’s MSRV-aware fallback, so it does not prove that a plain resolve on 1.87 works. See the note below. The MSRV is set by a dependency, not by this crate’s own code:ctutils0.4.4, the first release without RUSTSEC-2026-0385, needs Rust 1.87. No nightly features are used. The MSRV promise coverscargo build/cargo checkonly, notcargo test. A couple of this crate’s dev-only interop-test dependencies need a newer toolchain, but dev-dependencies are never pulled in by downstream consumers.
Why not Rust 1.85 or 1.86 any more? Before 0.5.1 the MSRV was 1.85.
ml-dsaandslh-dsaboth depend onctutils, and RUSTSEC-2026-0385 (2026-10-23) covers everyctutilsbefore 0.4.4. Its constant-time helpers could compile to branches on secret values, which can leak those values through timing. The fixed releases, 0.4.4 and 0.4.5, need Rust 1.87. The newestctutilsthat builds on 1.85 is 0.4.2, which is affected. So a 1.85 or 1.86 build can only be made with an affectedctutils. pqc-sig now requiresctutils0.4.4 or later, so no resolver can choose an affected version.On Rust 1.87? No
Cargo.lockships with a library, so Cargo picks the newest compatible versions. With default features that includessha20.11.1, viaslh-dsa, which needs Rust 1.88. A plaincargo buildon 1.87 then fails with “rustc 1.87.0 is not supported by the following package”. If you only use ML-DSA (default-features = false, features = ["ml-dsa"]), you don’t usesha2and a plain build works. Otherwise either of these gives you a 1.87 build:
Resolve with the MSRV-aware fallback, once, before you build:
CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback cargo generate-lockfileSet
resolver = "3"in your workspace or packageCargo.toml. Resolver 3 uses the fallback by default.Either way Cargo picks
sha20.11.0 andctutils0.4.5. Both are safe:sha20.11.0 has no RustSec advisory, and thectutilsfloor keeps the fallback on a fixed release.
- No C compiler, credentials, network services, or local files outside the repo are needed
to build or test this crate — every optional feature (
fndsa,hybrid) is pure Rust.
§Safety
This crate’s own code contains zero unsafe, enforced by #![forbid(unsafe_code)] in
src/lib.rs — this is true of the default build and every combination of this
crate’s own features, and there is no C FFI anywhere in the dependency tree (the fndsa
feature dropped its C FFI dependency — see release notes). It is not
true of the dependency graph as a whole in the stronger sense of “no unsafe anywhere”: the
optional fndsa feature (fn-dsa) and hybrid feature (curve25519-dalek, via
ed25519-dalek) both use unsafe internally for architecture-specific performance paths
(AVX2 intrinsics, native floating point) — safe Rust, not FFI, and gated behind non-default
features. The default build has zero unsafe in its entire dependency tree.
§Continuous Integration
.github/workflows/ci.yml runs on every push and pull request, on
a fresh GitHub-hosted runner with no dependency or build caching — every run is a genuine
clean-room build.
Eight jobs:
- Default features (build + test) —
cargo build/cargo testwith default features. The crate’s supported surface; must always pass. --all-features(build + test) — same, with every optional algorithm feature enabled.- Packaged artifact (default features) — builds and tests the actual packaged
.crateoutput (whatcargo add pqc-sigships), not just the working tree — catches.gitignore/package-excludemistakes that only surface for someone installing the published crate. - Packaged artifact (
--all-features) — same, with every optional feature enabled. - MSRV (Rust 1.87.0, build): builds against the pinned MSRV toolchain, not
stable. It builds but does not test. See Build Requirements. It first resolves dependencies withCARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback, so it proves that a 1.87-compatible dependency set exists and builds. It fails if this crate’s code needs a newer Rust, or if no 1.87-compatible set can be found. pqc-sig-wasm(wasm32 cdylib artifact) — builds the real standalone WASM artifact,cargo build --target wasm32-unknown-unknown --no-default-featuresinpqc-sig-wasm/. This is the job that would have caught the crate-split defect this repo used to have — every other job only ever built the root crate by itself.- Downstream consumer (no_std + external std leak, wasm32) — builds
tests/downstream-consumer-fixture, a minimal crate depending onpqc-sigwithdefault-features = falsealongside an independentgetrandomdependency that leaks realstdviawasm-bindgenonwasm32regardless ofpqc-sig’s ownno_std-ness. A second build ofpqc-sigalone proves nothing here — only a consumer’s build graph exposes this class of regression. - Encoding only (no signature implementation) — builds with
--no-default-features --features stdand fails ifml-dsa,slh-dsa,fn-dsaored25519-dalekappears in the dependency graph. The same check runs against the default build first and must findml-dsathere, so a check that can’t fail doesn’t pass silently. Then runs the encoding tests, including the 0.4.2 byte-identity vectors, and clippy with warnings denied, in that configuration.
§Dependency scanning
.github/workflows/cargo-deny.yml runs
cargo-deny against deny.toml
— advisories, bans, licenses and sources — on every push to main, every pull request, and
weekly on Mondays at 06:00 UTC. A failing scheduled run opens or updates a tracking issue
labelled security / cargo-deny.
It is a separate workflow from ci.yml on purpose, and it is not a required status
check — that stays true even now that the advisories check passes cleanly, since a future
advisory landing on any dependency shouldn’t block merges by default; it should surface
visibly instead. Any accepted advisory would be listed by RUSTSEC ID in
SECURITY.md — not hidden with an ignore
list, and not papered over with continue-on-error.
§Quick Start
[dependencies]
pqc-sig = "0.5"use pqc_sig::fips204::MlDsa65Keypair;
use rand::rngs::OsRng;
// Generate a keypair
let keypair = MlDsa65Keypair::generate(&mut OsRng).unwrap();
let pk = keypair.public_key();
// Sign a message
let message = b"Hello, post-quantum world!";
let signature = keypair.sign(&mut OsRng, message).unwrap();
// Verify the signature
MlDsa65Keypair::verify(&pk, message, &signature).unwrap();§WASM Usage
pqc-sig itself is a pure library (rlib-only) and is never built as a standalone WASM
artifact — Cargo builds every declared crate-type for a package regardless of what a
consumer needs, so a cdylib declaration here would force the #[global_allocator]/
#[panic_handler] lang-item question onto every consumer, including ones that already have
their own std. The sibling pqc-sig-wasm crate exists specifically to be
that final artifact.
Build the WASM binary via the sibling crate:
powershell -ExecutionPolicy Bypass -File build-wasm.ps1§JavaScript/TypeScript
import init, {
// ML-DSA
WasmMlDsa44Keypair, WasmMlDsa65Keypair, WasmMlDsa87Keypair,
ml_dsa_44_verify, ml_dsa_65_verify, ml_dsa_87_verify,
// SLH-DSA (all 12 variants)
WasmSlhDsaSha2_128sKeypair, WasmSlhDsaSha2_128fKeypair,
WasmSlhDsaSha2_192sKeypair, WasmSlhDsaSha2_192fKeypair,
WasmSlhDsaSha2_256sKeypair, WasmSlhDsaSha2_256fKeypair,
WasmSlhDsaShake128sKeypair, WasmSlhDsaShake128fKeypair,
WasmSlhDsaShake192sKeypair, WasmSlhDsaShake192fKeypair,
WasmSlhDsaShake256sKeypair, WasmSlhDsaShake256fKeypair,
slh_dsa_sha2_128s_verify, slh_dsa_sha2_128f_verify,
slh_dsa_sha2_192s_verify, slh_dsa_sha2_192f_verify,
slh_dsa_sha2_256s_verify, slh_dsa_sha2_256f_verify,
slh_dsa_shake_128s_verify, slh_dsa_shake_128f_verify,
slh_dsa_shake_192s_verify, slh_dsa_shake_192f_verify,
slh_dsa_shake_256s_verify, slh_dsa_shake_256f_verify,
// Utilities
pqc_sig_version,
} from './pqc_sig.js';
await init();
// Generate keypair
const keypair = new WasmMlDsa65Keypair();
const pubKeyBytes = keypair.public_key_bytes();
// Sign
const message = new TextEncoder().encode("Hello, post-quantum world!");
const signature = keypair.sign(message);
// Verify
const valid = ml_dsa_65_verify(pubKeyBytes, message, signature);
console.log("Valid:", valid); // true
console.log("Version:", pqc_sig_version()); // e.g. "0.4.0"§Hybrid Signatures
The opt-in hybrid feature adds an Ed25519 + ML-DSA-65 combiner for bridging classical
deployments to PQC during migration: HybridSigner produces both signatures, and
verification requires both to pass — an attacker must break both primitives to forge one.
pqc-sig = { version = "0.5", features = ["hybrid"] }// Not run as a doctest: this README is included as the crate's top-level doc comment
// (`#![doc = include_str!("../README.md")]`), which compiles unconditionally — the
// `hybrid` feature isn't guaranteed enabled there. See `src/hybrid.rs` for the
// feature-gated, actually-tested version of this example.
use rand::rngs::OsRng;
use pqc_sig::hybrid::HybridSigner;
let signer = HybridSigner::generate(&mut OsRng).unwrap();
let pk = signer.public_key();
let message = b"Hello, hybrid world!";
let signature = signer.sign(message).unwrap();
HybridSigner::verify(message, &signature, &pk).unwrap();§WASM Component Model
The wit/pqc-sig.wit file defines the WIT (WebAssembly Interface Types) interface for this library, enabling use as a WASM Component with any compliant runtime.
Package: x307:pqc-sig@0.4.0
§WIT Interface Summary
package x307:pqc-sig@0.4.0;
interface types { ... } // sig-algorithm enum, sig-error variant
interface ml-dsa { ... } // ML-DSA-44/65/87 keypair resource + sign/verify
interface slh-dsa { ... } // SLH-DSA all 12 parameter sets
interface pqc-sig { ... } // Unified dispatch interface
world pqc-sig-world {
export ml-dsa;
export slh-dsa;
export pqc-sig;
}§Using with wasmtime (CLI)
# Run a component that imports pqc-sig
wasmtime run --component my-app.wasm§Using with jco (Node.js / Browser)
# Transpile the component for browser/Node.js use
npx jco transpile pqc_sig_bg.wasm -o dist-jco/
# Then import in your JS/TS project:
import { mlDsa, slhDsa } from './dist-jco/pqc_sig.js';§WIT File Location
The WIT interface is at wit/pqc-sig.wit and is also included in the WASM release artifact (dist/pqc-sig.wit, produced by build.ps1; historically shipped as pqc-sig-v0.1.0-wasm.zip in the v0.1.0 release — see the Release table).
§Encoding only
For code that carries keys and signatures but never signs or verifies (a DID tool, a resolver, an SDK whose signing happens elsewhere), turn the algorithms off:
pqc-sig = { version = "0.5", default-features = false, features = ["std"] }You get SigAlgorithm, SigPublicKey, Signature, SignedMessage, multicodec codes,
W3C Multikey (to_multibase/from_multibase), base64url and JSON, with no signature
implementation in your dependency graph. CI enforces that. The encodings are
byte-identical to the full build’s.
§no_std Support
This crate is no_std-compatible with alloc. Disable the std feature and keep the
algorithms you use:
pqc-sig = { version = "0.5", default-features = false, features = ["ml-dsa", "slh-dsa"] }§Features
| Feature | Description |
|---|---|
std (default) | Enable std-dependent trait impls |
ml-dsa (default) | ML-DSA-44/65/87 (FIPS 204) keypairs, signing and verification |
slh-dsa (default) | The twelve SLH-DSA parameter sets (FIPS 205) |
fndsa | Enable FN-DSA/Falcon (pure Rust, WASM-compatible) |
hybrid | Enable the Ed25519 + ML-DSA-65 hybrid combiner (pure Rust, WASM-compatible). Implies ml-dsa |
§Algorithm Selection Guide
| Use Case | Recommended Algorithm |
|---|---|
| General purpose (balanced) | ML-DSA-65 (FIPS 204) |
| Maximum security | ML-DSA-87 (FIPS 204) |
| Minimum key size | ML-DSA-44 (FIPS 204) |
| WASM module integrity | SLH-DSA-SHA2-128s (FIPS 205) |
| Compact signatures | FN-DSA-512 (FIPS 206) |
| Audit/long-term archival | ML-DSA-87 (FIPS 204) |
| Bridging a classical deployment to PQC | Hybrid Ed25519 + ML-DSA-65 (hybrid feature) |
§Security Notes
- PQC-only by default — classical cryptography (Ed25519) is opt-in only via the
hybridfeature, for migration bridging - No KEM — signatures only (see
pqc-kemfor key encapsulation) - Caller-provided RNG — no
OsRnghardcoding in library code - Zeroize on drop — secret keys are automatically zeroed when dropped
- All operations return
SigResult<T>— never panics
§Architecture
pqc-sig/
├── src/
│ ├── lib.rs — crate root, re-exports, no_std gate
│ ├── error.rs — SigError enum, SigResult alias
│ ├── types.rs — wire types (SigPublicKey, SigSecretKey, Signature, SignedMessage)
│ ├── fips204/ — ML-DSA (FIPS 204): ML-DSA-44/65/87
│ ├── fips205/ — SLH-DSA (FIPS 205): 12 parameter sets
│ ├── fips206/ — FN-DSA (FIPS 206): Falcon-512/1024 (feature = "fndsa")
│ └── hybrid.rs — Ed25519 + ML-DSA-65 combiner (feature = "hybrid")
├── pqc-sig-wasm/ — sibling crate: the standalone WASM cdylib artifact + JS bindings
├── tests/ — integration tests
│ └── downstream-consumer-fixture/ — regression fixture for the no_std/std-leak defect
└── wit/ — WIT interface for WASM Component Model§Release Notes
§v0.1.0 (2026-08-01) — Initial Release
- 17 algorithms implemented: ML-DSA-44/65/87, all 12 SLH-DSA variants, FN-DSA-512/1024
- 105 tests passing with
--features fndsa(86 without Falcon) - WASM-compatible: ML-DSA (all 3 variants) + SLH-DSA (all 12 variants) compile to
wasm32-unknown-unknown - WIT Component Model interface at
wit/pqc-sig.wit - Standalone crate — no workspace coupling,
no_std+alloc - Caller-provided RNG, secret keys zeroized on drop
See release-notes.md for full details, and CHANGELOG.md for the authoritative Keep-a-Changelog-format record.
§Stability and support
This project ships 0.x. See STABILITY.md for what counts as a breaking
change, deprecation notice, release cadence, and support posture. It also has the
support table: 0.5.x is current, and 0.4.x gets
security patches until 2027-05-31. No version is ever yanked.
§Security
See SECURITY.md to report a vulnerability. It also lists any known,
accepted dependency advisories (currently none — see
SECURITY.md) and the dependency-scanning
setup.
§Contributing
See CONTRIBUTING.md, including the current external-contribution
posture.
§License
MIT OR Apache-2.0 — see LICENSE-MIT and LICENSE-APACHE.
§Maintainer
Ed Johnson
§The 0x307 crate family
pqc-sig is one of six open-source crates from 0x307, held to one
audit and stability standard.
| Crate | Tier | What it does |
|---|---|---|
| pqc-sig (this crate) | Production | ML-DSA, SLH-DSA and FN-DSA signatures (FIPS 204/205/206) |
| pqc-kem | Production | ML-KEM (FIPS 203), the X25519 + ML-KEM-768 hybrid, X-Wing, and sealed boxes |
| aethel-core | Production | Post-quantum anonymous identity: a separate identifier per context, context-bound ML-DSA signing |
| aethel-sdk | Preview | The SDK over aethel-core, and the place to start |
| aethel-vault | Preview | Agent-held wallet: policy-gated x402 / EIP-3009 signing with ML-DSA-65 spend records |
| pqc-privacy | Lab | Research bundle, kept off every identity and payment path |
Production crates are thin, standards-bound libraries meant to be depended on today. Preview crates work and are published, with APIs still settling. Lab crates are research, never on an identity or payment path.
Runnable examples: 0x307/examples, one program per crate, pinned to the published versions.
Audit status: None of these crates has been independently audited, and none holds a CMVP / FIPS 140-3 validation. “FIPS 203/204/205/206” means the algorithms follow those standards, not that the code is certified. Known issues for this crate are in SECURITY.md. Versioning and support: STABILITY.md.
Re-exports§
pub use error::SigError;pub use error::SigResult;pub use types::SigAlgorithm;pub use types::SigPublicKey;pub use types::SigSecretKey;pub use types::Signature;pub use types::SignedMessage;pub use prehash::PreHash;pub use fips204::MlDsa44Keypair;pub use fips204::MlDsa65Keypair;pub use fips204::MlDsa87Keypair;pub use fips205::SlhDsaSha2_128sKeypair;pub use fips205::SlhDsaSha2_128fKeypair;pub use fips205::SlhDsaSha2_192sKeypair;pub use fips205::SlhDsaSha2_192fKeypair;pub use fips205::SlhDsaSha2_256sKeypair;pub use fips205::SlhDsaSha2_256fKeypair;pub use fips205::SlhDsaShake128sKeypair;pub use fips205::SlhDsaShake128fKeypair;pub use fips205::SlhDsaShake192sKeypair;pub use fips205::SlhDsaShake192fKeypair;pub use fips205::SlhDsaShake256sKeypair;pub use fips205::SlhDsaShake256fKeypair;pub use fips206::FnDsa512Keypair;pub use fips206::FnDsa1024Keypair;pub use hybrid::HybridPublicKey;pub use hybrid::HybridSignature;pub use hybrid::HybridSigner;
Modules§
- error
- Error types for all signature operations.
Unified error type for the
pqc-sigcrate. - fips204
- ML-DSA (NIST FIPS 204) — pure Rust, WASM-native. Requires the
ml-dsafeature (default). FIPS 204 — ML-DSA (Module Lattice Digital Signature Algorithm) - fips205
- SLH-DSA (NIST FIPS 205) — pure Rust, WASM-native. Requires the
slh-dsafeature (default). FIPS 205 — SLH-DSA (Stateless Hash-Based Digital Signature Algorithm) - fips206
- FN-DSA / Falcon (NIST FIPS 206) — requires
fndsafeature, pure Rust, WASM-compatible. FIPS 206 — FN-DSA (Falcon) Digital Signature Algorithm - hybrid
- Hybrid classical + post-quantum signatures (Ed25519 + ML-DSA-65) — requires
hybridfeature. Hybrid classical + post-quantum signatures: Ed25519 + ML-DSA-65. - prehash
- Pre-hash signing support (
HashML-DSA/HashSLH-DSA/FN-DSA pre-hashed mode) shared bysign_prehash/sign_prehash_deterministic/verify_prehashacross all algorithm families. See the crate-level “Pre-hash signing” docs above. Pre-hash signing support for HashML-DSA (FIPS 204 §5.4) / HashSLH-DSA (FIPS 205 §10.2.2), plus the equivalent native pre-hashed mode of FN-DSA (FIPS 206 draft). - types
- Wire types: public keys, secret keys, signatures, algorithm identifiers. Wire types for PQC signature public keys, secret keys, and signatures.
Constants§
- AUDIT_
ALGORITHM - Audit signature algorithm (ML-DSA-87, FIPS 204 Level 5).
- COMPACT_
ALGORITHM - Compact signature algorithm (FN-DSA-512, FIPS 206).
- MAX_
CONTEXT_ LEN - Maximum length, in bytes, of a FIPS 204/205/206 domain-separation context string.
- PRIMARY_
ALGORITHM - Primary signature algorithm (ML-DSA-65, FIPS 204 Level 3).
- VERSION
- Crate version string.
- WASM_
INTEGRITY_ ALGORITHM - WASM integrity algorithm (SLH-DSA-SHA2-128s, FIPS 205).