pqc-sig 0.4.2

Post-quantum digital signatures: ML-DSA (FIPS 204), SLH-DSA (FIPS 205), FN-DSA (FIPS 206) — standalone WASM-compatible
Documentation
# pqc-sig Release Notes

## v0.4.1 (2026-09-23)

Adds `BENCHMARKS.md`: measured cost of every operation the crate offers, on a
dedicated-core Intel Xeon Platinum 8488C. **No library code changes** — `src/`
is identical to 0.4.0, so there is nothing to migrate and no behavior to
re-verify.

Signing figures are ±20%, because signing cost depends on the key and each
benchmark run generates a fresh one. Key generation and verification are not
affected. See `CHANGELOG.md` for detail.

## v0.4.0 (2026-09-09)

Closes five stakeholder-identified adoption gaps (S-1..S-5) blocking SAGP/8gentz-fabric
integration. **Purely additive — no breaking changes, no migration required.**

### Highlights

**S-1: Domain-separated signing.** Every ML-DSA and SLH-DSA keypair (all 15 native FIPS
algorithms) now exposes `sign_ctx`/`sign_ctx_deterministic`/`verify_ctx`, using the FIPS
204/205 `ctx` byte-string parameter natively rather than the hard-coded empty context this
crate previously baked in. This is what lets an agent cryptographically bind a
signature to a specific protocol/relationship (e.g. `"8gentz-agent-v1"` vs.
`"8gentz-fabric-v1"`) so a signature produced for one context can't be replayed as valid in
another — a prerequisite for any multi-tenant or multi-protocol SAGP deployment. FN-DSA gets
the same capability via `fn_dsa::DomainContext`. See
[`examples/domain_separation.rs`](examples/domain_separation.rs).

**S-2: Hybrid bridge for existing classical identities.** `HybridSigner` can now be
constructed from an *already-deployed* Ed25519 secret (`from_ed25519_secret`) or restored
from persisted key material (`from_secret_key_bytes`/`secret_key()`), instead of only
`generate()`-ing a fresh keypair. This removes the practical migration blocker for a fleet
that already has classical identities: agents can add a post-quantum ML-DSA-65 signature
alongside their existing Ed25519 key without re-provisioning identity. `sign_ctx`/
`verify_ctx` bring domain separation to the hybrid combiner too. See
[`examples/hybrid_bridge.rs`](examples/hybrid_bridge.rs) and
[`docs/SAGP_NOTES.md`](docs/SAGP_NOTES.md) for the recommended `PRIMARY_ALGORITHM` (ML-DSA-65)
and Wave-1 accept-hybrid rollout guidance. `hybrid` remains an opt-in feature, off by default.

**S-3: FN-DSA multicodec status, documented.** No code changed here — what changed is
clarity. [`docs/MULTICODEC.md`](docs/MULTICODEC.md) lays out, for all 17 algorithms, exactly
which multicodec codes are upstream-registered (ML-DSA, SLH-DSA) versus this project's own
provisional private-use reservation (FN-DSA `0x307000`/`0x307001`), with a tracking checklist
for when/if multiformats/multicodec registers a real code. Stakeholders integrating FN-DSA
Multikeys into cross-org tooling now have a single authoritative reference for interop scope
instead of having to reverse-engineer it from source comments.

**S-4: Pre-hash signing for large payloads (HashML-DSA / HashSLH-DSA).** `sign_prehash`/
`sign_prehash_deterministic`/`verify_prehash`, spec-conformant with FIPS 204 §5.4 Alg 4/5 and
FIPS 205 §10.2.2 Alg 23/25, let a caller hash a large artifact (a container image, a model
checkpoint, an 8gentz module bundle) themselves and sign only the digest — this crate never
buffers the full payload for signing purposes. Digest strength is enforced against the target
algorithm's security level (`SigError::PreHashTooWeak`), so e.g. SHA-256 is rejected for
ML-DSA-65/87 rather than silently under-securing the signature. See
[`examples/prehash_large_payload.rs`](examples/prehash_large_payload.rs).

**S-5: External, independently-checkable proof.** 132 known-answer test vectors, with key
material sourced from NIST's own ACVP-Server (not self-generated) — and 15 of them verbatim
NIST ACVP `ML-DSA-sigVer` cases, signature and expected result included — are now published under
[`tests/vectors/`](tests/vectors/), executed on every `cargo test`, and shipped inside the
packaged crate so downstream consumers can verify this implementation offline without
depending on any network service. A new WASM CI workflow
([`.github/workflows/wasm.yml`](.github/workflows/wasm.yml)) additionally proves the compiled
WASM artifact behaves identically to the native build, on every push.

### Upgrading from 0.3.x

No code changes are required. Every addition in this release is either:

- a new opt-in method on an existing type (`sign_ctx`, `sign_prehash`, and friends) — existing
  calls to `sign`/`verify` are untouched and byte-identical to before;
- a new opt-in constructor on `HybridSigner` (`from_ed25519_secret`, `from_secret_key_bytes`)
  — `HybridSigner::generate()` still works exactly as before, and `hybrid` is still not a
  default feature (`features = ["hybrid"]` is still required to use any of it); or
- documentation/tooling only (`docs/MULTICODEC.md`, `docs/SAGP_NOTES.md`, KAT vectors, CI).

If you build `pqc-sig-wasm` yourself: it is now version `0.4.0` and exposes 60 new JS
bindings (`sign_ctx`/`sign_prehash` on all 15 bound keypairs, plus free-function
`{alg}_verify_ctx`/`{alg}_verify_prehash`). `verify_ctx`/`verify_prehash` return
`Result<(), JsValue>` rather than `bool` — if you call these *new* functions from JS, check
for a thrown/rejected error rather than a falsy return; the pre-existing `{alg}_verify`
functions are unchanged. If you consume `wit/pqc-sig.wit`, the package name is now
`x307:pqc-sig@0.4.0` (was `@0.1.0`, stale).

### Known limitations

- ML-DSA `sign(rng, ..)`/`sign_ctx`/`sign_prehash` remain **deterministic**: the
  caller-provided `rng` is accepted but currently ignored. Hedged/randomized ML-DSA signing
  needs a `rand_core` 0.6→0.10 adapter for the upstream `ml-dsa` crate — this is a
  pre-existing limitation (`TODO(hedging)`), not new in this release.
- SLH-DSA cannot stream a message by construction — the full message must be buffered before
  signing/verifying. Pre-hash signing (S-4) is the recommended mitigation for large payloads.
- SLH-DSA's published KATs have ACVP-sourced *keys* but self-generated *signatures* — the
  upstream ACVP `SLH-DSA-sigGen`/`sigVer` files (30–38 MB) were not curated in this pass. See
  [`tests/vectors/README.md`]tests/vectors/README.md for the exact per-vector provenance and
  what's planned next.
- Published KATs cover **7 of the 12 SLH-DSA parameter sets** (all six SHA2 sets plus
  `SHAKE-128s`). `SHAKE-128f`, `SHAKE-192s/f` and `SHAKE-256s/f` have no published vectors in
  this pass — SLH-DSA signing wall-time was the constraint.
- Test coverage is uneven across the SLH-DSA SHAKE sets: **`SHAKE-192s` and `SHAKE-192f` have
  no automated tests**, and `SHAKE-256f` is covered only by a Multikey encoding check that never
  signs or verifies. All three are exposed API and are thin wrappers over the same upstream
  `slh-dsa` generics as the covered sets, but that wiring is not currently asserted by a test.
- Of the 132 published vectors, **15 are verbatim NIST ACVP `ML-DSA-sigVer` cases** (5 each for
  ML-DSA-44/65/87). The other 117 pair ACVP-derived *key material* with signatures this crate
  generated itself, making them round-trip/self-consistency tests rather than independent
  cross-validation. See [`tests/vectors/README.md`]tests/vectors/README.md for per-vector
  provenance.
- `pqc-sig-wasm` still does not bind FN-DSA or `hybrid` (unchanged from 0.3.x) — only ML-DSA
  and SLH-DSA are exposed to WASM/JS.
- The hybrid bridge's Ed25519-half context framing (`0x00 ‖ len(ctx) ‖ ctx ‖ msg`) is a
  crate-defined convention, not a cross-implementation standard.

### Test Coverage

- **201 tests passing with `--all-features`, 0 failed, 4 ignored** (up from the `0.3.1`
  baseline of 106) — unit tests: 49 (including 9 new pre-hash tests, 10 new hybrid tests, 2
  new ctx tests); `ml_dsa` integration: 40; `slh_dsa` integration: 46; `fn_dsa` integration:
  27; `kat_tests`: 11; `multibase_tests`: 13; `ssi_interop_test`: 7; doctests: 8 passed / 4
  ignored.
- **132 published KAT vectors** across 10 files under [`tests/vectors/`]tests/vectors/
  — covering 7 of 12 SLH-DSA parameter sets, of which 15 vectors are verbatim NIST ACVP
  sigVer cases — executed by `kat_tests`, byte-reproducible via
  [`examples/gen_kat_vectors.rs`]examples/gen_kat_vectors.rs.
- **22/22 WASM functional tests** via `node test-wasm.mjs` against the `wasm-pack`-built
  `pqc-sig-wasm` artifact.

### Files of interest

- [`src/ctx.rs`]src/ctx.rs, [`src/prehash.rs`]src/prehash.rs, [`src/hybrid.rs`]src/hybrid.rs
- [`examples/domain_separation.rs`]examples/domain_separation.rs,
  [`examples/prehash_large_payload.rs`]examples/prehash_large_payload.rs,
  [`examples/hybrid_bridge.rs`]examples/hybrid_bridge.rs,
  [`examples/gen_kat_vectors.rs`]examples/gen_kat_vectors.rs
- [`docs/SAGP_NOTES.md`]docs/SAGP_NOTES.md, [`docs/MULTICODEC.md`]docs/MULTICODEC.md
- [`tests/vectors/README.md`]tests/vectors/README.md, [`tests/kat_tests.rs`]tests/kat_tests.rs
- [`wit/pqc-sig.wit`]wit/pqc-sig.wit, [`.github/workflows/wasm.yml`].github/workflows/wasm.yml

## v0.2.1 (2026-09-01)

Docs-only fix: README.md and `src/lib.rs`'s doc-comment examples still pinned `pqc-sig =
"0.1"` after the 0.2.0 release. Caught reviewing the published crates.io page — crates.io
READMEs are immutable per version, so this needed a new release rather than an edit.
Bumped all six version pins to `"0.2"`. No code or behavior change.

## v0.2.0 (2026-09-01)

### BREAKING: `SigPublicKey::to_multibase()` / `from_multibase()` now emit W3C Multikeys

`to_multibase()` previously base58btc-encoded raw public key bytes with no multicodec prefix,
which is not a valid [W3C Multikey](https://www.w3.org/TR/controller-document/#multikey) and
would be rejected by any spec-conformant verifier (e.g. the `ssi` crate).

- Output now carries a [multicodec]https://github.com/multiformats/multicodec varint prefix
  identifying the key type, per the Multikey spec, for ML-DSA (FIPS 204) and SLH-DSA (FIPS 205)
  keys.
- `to_multibase()` now returns `SigResult<String>` instead of `String` — callers must handle
  the `Result`.
- `from_multibase()` now verifies the embedded multicodec code matches the `algorithm` argument
  and returns an error on mismatch (previously it trusted the caller-supplied algorithm blindly).
- **FN-DSA (FIPS 206 / Falcon) is not supported** by `to_multibase()`/`from_multibase()`: no
  multicodec code is registered for it yet upstream. Both functions return `SigError` for
  `FnDsa512`/`FnDsa1024`. This will be revisited once a code is registered.
- Multibase strings produced by the old code are **not** valid Multikeys and will fail to decode
  under `from_multibase()`. There is no migration path for old-format strings other than
  re-encoding from the raw key bytes.
- Committed test vectors: `tests/multibase_tests.rs`.
- Independently verified: `tests/ssi_interop_test.rs` decodes our output with `multibase`
  (multiformats/rust-multibase) and `ssi-multicodec` (spruceid/ssi, dev-dependency only) and
  confirms the recovered multicodec code and key bytes match what we encoded, for ML-DSA-44/65/87
  and two SLH-DSA parameter sets.

### Added: Hybrid Ed25519 + ML-DSA-65 combiner (`hybrid` feature)

`HybridSigner` produces both a classical Ed25519 and a post-quantum ML-DSA-65 signature on
`sign()`; `verify()` requires both to pass, with the failing side identifiable
(`SigError::HybridClassicalFailed` / `HybridPqcFailed`). For bridging classical deployments
to PQC during migration. Pure Rust (`ed25519-dalek`), WASM-compatible, `no_std` + `alloc`.

### Changed: `fndsa` feature migrated from `pqcrypto-falcon` to `fn-dsa` (pure Rust)

Resolves RUSTSEC-2026-0165/0163/0162 (see [`SECURITY.md`](SECURITY.md)) and drops the C-FFI /
non-WASM limitation — `cargo build --target wasm32-unknown-unknown --features fndsa` now
succeeds, and no C compiler is needed to build any feature of this crate.

- `FnDsa512Keypair::generate` / `FnDsa1024Keypair::generate` now take a caller-provided RNG
  (`generate(&mut rng)`), matching this crate's RNG convention everywhere else. Previously used
  `pqcrypto-falcon`'s internal RNG.
- `FnDsa512Keypair::sign` / `FnDsa1024Keypair::sign` now take a caller-provided RNG
  (`sign(&mut rng, message)`) — FN-DSA signing is randomized, unlike ML-DSA.
- FN-DSA secret key wire size changed: 1345 bytes for FN-DSA-512 (was 1281), 2369 bytes for
  FN-DSA-1024 (was 2305). This is `fn-dsa`'s own encoded signing-key format, not the raw NIST
  secret-key size — implementation-defined, the same posture this crate already takes for the
  ML-DSA seed encoding. Public key and signature sizes are unchanged.
- FN-DSA signatures are now a fixed length (666 / 1280 bytes, zero-padded) rather than
  variable-length up to that size.

### Test Coverage

- **106 tests passing with `--features fndsa,hybrid`** (79 with neither feature, 99 with
  `fndsa` only, 86 with `hybrid` only).
- `cargo deny check` (advisories, bans, licenses, sources) passes cleanly with `--all-features`
  — no accepted or ignored findings.
- Verified against live `wasm32-unknown-unknown` builds: `--features fndsa`,
  `--features wasm,fndsa,hybrid`.

## v0.1.0 (2026-08-01)

### Initial Release

**Post-quantum digital signatures** — standalone, WASM-compatible Rust library.

### Algorithms

| Algorithm | Standard | Status |
|-----------|----------|--------|
| ML-DSA-44 | FIPS 204 | ✅ Implemented |
| ML-DSA-65 | FIPS 204 | ✅ Implemented |
| ML-DSA-87 | FIPS 204 | ✅ Implemented |
| SLH-DSA-SHA2-128s | FIPS 205 | ✅ Implemented |
| SLH-DSA-SHA2-128f | FIPS 205 | ✅ Implemented |
| SLH-DSA-SHA2-192s | FIPS 205 | ✅ Implemented |
| SLH-DSA-SHA2-192f | FIPS 205 | ✅ Implemented |
| SLH-DSA-SHA2-256s | FIPS 205 | ✅ Implemented |
| SLH-DSA-SHA2-256f | FIPS 205 | ✅ Implemented |
| SLH-DSA-SHAKE-128s | FIPS 205 | ✅ Implemented |
| SLH-DSA-SHAKE-128f | FIPS 205 | ✅ Implemented |
| SLH-DSA-SHAKE-192s | FIPS 205 | ✅ Implemented |
| SLH-DSA-SHAKE-192f | FIPS 205 | ✅ Implemented |
| SLH-DSA-SHAKE-256s | FIPS 205 | ✅ Implemented |
| SLH-DSA-SHAKE-256f | FIPS 205 | ✅ Implemented |
| FN-DSA-512 (Falcon) | FIPS 206 (draft) | ✅ Implemented (feature = "fndsa") |
| FN-DSA-1024 (Falcon) | FIPS 206 (draft) | ✅ Implemented (feature = "fndsa") |

### Test Coverage

- **85 tests passing** (66 without Falcon, 85 with `--features fndsa`)
- ML-DSA integration tests: 23
- SLH-DSA integration tests: 20
- FN-DSA integration tests: 15 (feature-gated)
- Unit/smoke tests: 22
- Doc-tests: 5

### WASM Artifacts

- `pqc_sig_bg.wasm` — compiled WebAssembly binary
- `pqc_sig.js` — JavaScript ESM glue module
- `pqc_sig.d.ts` — TypeScript type definitions
- `pqc-sig.wit` — WIT Component Model interface

### Dependencies

- `ml-dsa 0.1.1` — FIPS 204 (pure Rust, RustCrypto)
- `slh-dsa 0.2.0-rc.5` — FIPS 205 (pure Rust, RustCrypto)
- `pqcrypto-falcon 0.3` — FIPS 206 (C FFI, feature-gated)

### Security Properties

- No classical cryptography (PQC-only)
- `no_std` + `alloc` compatible
- Caller-provided RNG (no `OsRng` hardcoding)
- Secret keys zeroized on drop
- All operations return `SigResult<T>` (never panics)