pqc-sig
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. cargo-deny is the dependency-advisory signal — also expected green. It previously carried an accepted-and-documented exception (the optional
fndsafeature 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.
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). -
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).
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.
Release
| Version | Date | Artifacts |
|---|---|---|
| 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.85 or later (edition 2021) — this is the crate's declared
rust-version(MSRV), enforced by themsrvjob inci.yml, which builds the working tree with a pinned Rust 1.85.0 toolchain on every push/PR. The MSRV is driven by a transitive dependency in the default build (ablock-bufferversion pulled in viaslh-dsa's hashing chain requires theedition2024Cargo feature, stabilized in Rust 1.85.0) — not by this crate's own code. 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. - 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.
Seven 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.85.0, build) — builds (not tests — see
Build Requirements) against the pinned MSRV toolchain, not
stable. Fails the moment any code or dependency bump relies on a newer language feature than the declaredrust-version. 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.
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
[]
= "0.3"
use MlDsa65Keypair;
use OsRng;
// Generate a keypair
let keypair = generate.unwrap;
let pk = keypair.public_key;
// Sign a message
let message = b"Hello, post-quantum world!";
let signature = keypair.sign.unwrap;
// Verify the signature
verify.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 from './pqc_sig.js';
await ;
// Generate keypair
const keypair = ;
const pubKeyBytes = keypair.;
// Sign
const message = .;
const signature = keypair.;
// Verify
const valid = ;
console.log; // true
console.log; // "0.1.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.
= { = "0.3", = ["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 OsRng;
use HybridSigner;
let signer = generate.unwrap;
let pk = signer.public_key;
let message = b"Hello, hybrid world!";
let signature = signer.sign.unwrap;
verify.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.1.0
WIT Interface Summary
package x307:pqc-sig@0.1.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
Using with jco (Node.js / Browser)
# Transpile the component for browser/Node.js use
# Then import in your JS/TS project:
;
WIT File Location
The WIT interface is at wit/pqc-sig.wit and is also included in the WASM release artifact (pqc-sig-v0.1.0-wasm.zip).
no_std Support
This crate is no_std-compatible with alloc. Disable the std feature:
= { = "0.3", = false }
Features
| Feature | Description |
|---|---|
std (default) |
Enable std-dependent trait impls |
fndsa |
Enable FN-DSA/Falcon (pure Rust, WASM-compatible) |
hybrid |
Enable the Ed25519 + ML-DSA-65 hybrid combiner (pure Rust, WASM-compatible) |
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.
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