Skip to main content

Crate pqc_sig

Crate pqc_sig 

Source
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-dsa feature (default)

  • SLH-DSA (NIST FIPS 205) — Stateless hash-based, pure Rust, WASM-native. slh-dsa feature (default)

    • SHA2 variants: 128s/128f, 192s/192f, 256s/256f
    • SHAKE variants: 128s/128f, 192s/192f, 256s/256f
  • FN-DSA (NIST FIPS 206 / Falcon) — requires fndsa feature, pure Rust, WASM-compatible

  • Hybrid Ed25519 + ML-DSA-65 — requires hybrid feature; 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.

CI wasm cargo-deny crates.io docs.rs MSRV License: MIT OR Apache-2.0 FIPS 204 FIPS 205 FIPS 206

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, runs wasm-pack build on the sibling pqc-sig-wasm crate, and executes the test-wasm.mjs functional 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 optional fndsa feature depended on the now-archived pqcrypto-falcon); that dependency was migrated to a pure-Rust, actively-maintained replacement, so there is currently nothing accepted or suppressed — see SECURITY.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 to wasm32-unknown-unknown, keygen/sign/verify covered by integration tests.

  • FN-DSA-512/1024 (Falcon, FIPS 206 draft), gated behind the fndsa feature flag — pure Rust via the fn-dsa crate, no_std + alloc, directly compilable to wasm32-unknown-unknown. No C compiler needed.

  • Hybrid Ed25519 + ML-DSA-65, gated behind the hybrid feature flag — HybridSigner combines 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 the multibase and ssi-multicodec crates (see tests/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 recognize 0x307000/0x307001 as FN-DSA. Revisit trigger: if/when multiformats/multicodec merges a real FN-DSA/Falcon code, multicodec_code() migrates to it in a follow-up minor release (tracked in src/types.rs’s FN_DSA_PRIVATE_USE_BASE doc comment, which has the full rationale). See docs/MULTICODEC.md for 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-wasm crate (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 to wasm32 as a library dependency — see above).
  • A WIT Component Model interface at wit/pqc-sig.wit describing 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-features on every push/PR, including against the packaged cargo package artifact, the pqc-sig-wasm cdylib artifact, and a downstream-consumer fixture that would otherwise mask a no_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, unsafe code 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 key1952 bytes897 bytes
Signature≤3309 bytes≤666 bytes
WASM-compatible (as a library dependency)✅ Yes (pure Rust)✅ Yes (pure Rust, via fn-dsa)
DependencyPure Rust (ml-dsa)Pure Rust (fn-dsa)
Multikey encodingOfficially registered multicodecProvisional 0x307 private-use code
Side-channel historyNone knownFalcon’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

VersionDateArtifacts
v0.5.12026-10-23crates.io
v0.5.02026-09-24crates.io
v0.4.02026-09-09crates.io
v0.3.02026-09-02crates.io
v0.2.12026-09-01crates.io
v0.2.02026-09-01crates.io
v0.1.02026-08-01pqc-sig-v0.1.0-wasm.zip

§Algorithms

AlgorithmStandardSecurity LevelPublic KeySignatureWASM
ML-DSA-44FIPS 2042 (128-bit)1312 B2420 B✅
ML-DSA-65FIPS 2043 (192-bit)1952 B3309 B✅
ML-DSA-87FIPS 2045 (256-bit)2592 B4627 B✅
SLH-DSA-SHA2-128sFIPS 205132 B7856 B✅
SLH-DSA-SHA2-128fFIPS 205132 B17088 B✅
SLH-DSA-SHA2-192sFIPS 205348 B16224 B✅
SLH-DSA-SHA2-192fFIPS 205348 B35664 B✅
SLH-DSA-SHA2-256sFIPS 205564 B29792 B✅
SLH-DSA-SHA2-256fFIPS 205564 B49856 B✅
SLH-DSA-SHAKE-128sFIPS 205132 B7856 B✅
SLH-DSA-SHAKE-128fFIPS 205132 B17088 B✅
SLH-DSA-SHAKE-192sFIPS 205348 B16224 B✅
SLH-DSA-SHAKE-192fFIPS 205348 B35664 B✅
SLH-DSA-SHAKE-256sFIPS 205564 B29792 B✅
SLH-DSA-SHAKE-256fFIPS 205564 B49856 B✅
FN-DSA-512 (Falcon)FIPS 206‡1897 B666 B✅*
FN-DSA-1024 (Falcon)FIPS 206‡51793 B1280 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). The msrv job in ci.yml checks 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: ctutils 0.4.4, the first release without RUSTSEC-2026-0385, needs Rust 1.87. No nightly features are used. The MSRV promise covers cargo build/cargo check only, not cargo 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-dsa and slh-dsa both depend on ctutils, and RUSTSEC-2026-0385 (2026-10-23) covers every ctutils before 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 newest ctutils that 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 affected ctutils. pqc-sig now requires ctutils 0.4.4 or later, so no resolver can choose an affected version.

On Rust 1.87? No Cargo.lock ships with a library, so Cargo picks the newest compatible versions. With default features that includes sha2 0.11.1, via slh-dsa, which needs Rust 1.88. A plain cargo build on 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 use sha2 and a plain build works. Otherwise either of these gives you a 1.87 build:

  1. Resolve with the MSRV-aware fallback, once, before you build:

    CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback cargo generate-lockfile
  2. Set resolver = "3" in your workspace or package Cargo.toml. Resolver 3 uses the fallback by default.

Either way Cargo picks sha2 0.11.0 and ctutils 0.4.5. Both are safe: sha2 0.11.0 has no RustSec advisory, and the ctutils floor 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 test with 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 .crate output (what cargo add pqc-sig ships), not just the working tree — catches .gitignore/package-exclude mistakes 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 with CARGO_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-features in pqc-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 on pqc-sig with default-features = false alongside an independent getrandom dependency that leaks real std via wasm-bindgen on wasm32 regardless of pqc-sig’s own no_std-ness. A second build of pqc-sig alone 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 std and fails if ml-dsa, slh-dsa, fn-dsa or ed25519-dalek appears in the dependency graph. The same check runs against the default build first and must find ml-dsa there, 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

FeatureDescription
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)
fndsaEnable FN-DSA/Falcon (pure Rust, WASM-compatible)
hybridEnable the Ed25519 + ML-DSA-65 hybrid combiner (pure Rust, WASM-compatible). Implies ml-dsa

§Algorithm Selection Guide

Use CaseRecommended Algorithm
General purpose (balanced)ML-DSA-65 (FIPS 204)
Maximum securityML-DSA-87 (FIPS 204)
Minimum key sizeML-DSA-44 (FIPS 204)
WASM module integritySLH-DSA-SHA2-128s (FIPS 205)
Compact signaturesFN-DSA-512 (FIPS 206)
Audit/long-term archivalML-DSA-87 (FIPS 204)
Bridging a classical deployment to PQCHybrid Ed25519 + ML-DSA-65 (hybrid feature)

§Security Notes

  • PQC-only by default — classical cryptography (Ed25519) is opt-in only via the hybrid feature, for migration bridging
  • No KEM — signatures only (see pqc-kem for key encapsulation)
  • Caller-provided RNG — no OsRng hardcoding 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.

CrateTierWhat it does
pqc-sig (this crate)ProductionML-DSA, SLH-DSA and FN-DSA signatures (FIPS 204/205/206)
pqc-kemProductionML-KEM (FIPS 203), the X25519 + ML-KEM-768 hybrid, X-Wing, and sealed boxes
aethel-coreProductionPost-quantum anonymous identity: a separate identifier per context, context-bound ML-DSA signing
aethel-sdkPreviewThe SDK over aethel-core, and the place to start
aethel-vaultPreviewAgent-held wallet: policy-gated x402 / EIP-3009 signing with ML-DSA-65 spend records
pqc-privacyLabResearch 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-sig crate.
fips204
ML-DSA (NIST FIPS 204) — pure Rust, WASM-native. Requires the ml-dsa feature (default). FIPS 204 — ML-DSA (Module Lattice Digital Signature Algorithm)
fips205
SLH-DSA (NIST FIPS 205) — pure Rust, WASM-native. Requires the slh-dsa feature (default). FIPS 205 — SLH-DSA (Stateless Hash-Based Digital Signature Algorithm)
fips206
FN-DSA / Falcon (NIST FIPS 206) — requires fndsa feature, pure Rust, WASM-compatible. FIPS 206 — FN-DSA (Falcon) Digital Signature Algorithm
hybrid
Hybrid classical + post-quantum signatures (Ed25519 + ML-DSA-65) — requires hybrid feature. Hybrid classical + post-quantum signatures: Ed25519 + ML-DSA-65.
prehash
Pre-hash signing support (HashML-DSA/HashSLH-DSA/FN-DSA pre-hashed mode) shared by sign_prehash/sign_prehash_deterministic/verify_prehash across 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).