Skip to content

antonymott/quantum-resistant-rustykey

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

57 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Quantum-resistant RustyKey®

Fast, secure WebAssembly implementations of useful post-quantum-resistant tools both for backend (node) and frontend web.

npm version Weekly Downloads Node v25.9.0

npm i quantum-resistant-rustykey

Implementation status: Pre-production (stable for testing)

  • Recommendation: Await v1.0.0 (following security audit) for production/regulated deployment.
  • includes NIST approved as well as riskier NIST 'on-ramp' variants eg SQISign
  • ML-DSA (ML-DSA-65, ML-DSA-87)
  • FN-DSA (FN-DSA-512, FN-DSA-1024)
  • SQIsign (Level 1, Level 3, Level 5)
  • ML-KEM (512, 768, 1024) using mlkem-native.

NOTE: Why we support SQISign when it is 'NIST-on-ramp' only

TLDR; to help hurdle the "silent" barrier to post-quantum adoption: 1024-byte buffer limit in many existing FIDO2/WebAuthn implementations

  • please see our IETF standards track draft for inclusion of SQISign cose-sqisign

WebAuthn PQC Signature size constraints

Dilithium variants, and Falcon-1024 are physical incompatibile with millions of existing FIDO2/WebAuthn authenticators that rely on the CTAP2 1024-byte buffer limit.

  • CTAP2 protocol, which allows browsers to talk to security keys, often operates within tight memory constraints to maintain the speed and low-power requirements of embedded devices.

  • Lattice-based mismatch: Dilithium-2 signatures (approx. 2,420 bytes) simply cannot fit into standard 1024-byte buffers found in many current authenticators.

  • At roughly 204 bytes, SQIsign is currently the only candidate that offers NIST-level (more accurately NIST-on-ramp-level) security safely within the 1024-byte limit alongside its necessary metadata.

Critical use case example

For mission-critical applications like low-latency augmented reality remote telesurgery, where ultra-low latency and hardware-rooted trust are non-negotiable. RustyKey® who financially support this repo and the npm package, required a WASM port of SQIsign specifically because the 204-byte signatures are the only PQC option that worked within their current hardware constraints, enabling immediate quantum-resistant public key ceremonies without breaking the existing WebAuthn ecosystem.

Broad user-friendly live example testbed and playground (coming May 2026)

  • live test environment where general purpose users together with seasoned cryptanalysts, can encrypt and descrypt and play, using all 3 varients of KEM and a test WebAuthn implementations using the signature algorithms
  • lattice-based vs isogeny: run tests to check: Montgomery constant times, the surprising difference in time taken for the various steps
  • all are encouraged to suggest improvements, and help a wider audience see how PQC works under the hood and adopt it more quickly and without breaking existing infrastructure.

Security assurance and verification

This project relies on upstream mlkem-native for arithmetic/security properties. The three parameter sets (512/768/1024) use the same implementation family and differ only by compile-time parameter selection.

Upstream evidence

What this means for 512/768/1024

  • Constant-time claims and proofs are provided upstream by mlkem-native (see links above).
  • This package builds the same source for all three variants by changing only MLK_CONFIG_PARAMETER_SET in wasm/Makefile.
  • Variant sizes/parameters are defined upstream in mlkem/mlkem_native.h.

Why we mix C => emscripten with Rust => wasm-bindgen for web-assembly module creation

Current status in this repository: the shipped cryptographic WASM modules are built via Emscripten from vetted C/C++ upstream code, while Rust/TypeScript is primarily used for package-level ergonomics and integration layers.

Increasingly, developers favor Rust => wasm-bindgen over C => emscripten for Rust's superior compile-time memory safety...and leaning on Rust is implied in our brand! RustyKey® current dual approach is a way to balance performance, security-vetted logic, and web compatibility. Some technical factors may make C => emscripten approach acceptable and, in some cases, preferable for post-quantum cryptography:

  • upstream Reliability: Many NIST-standardized PQC algorithms (like ML-KEM) have highly optimized, audited, and "constant-time" reference implementations written in C. Using C => Emscripten allows RustyKey® to port these vetted "upstream" sources directly, reducing the risk of introducing new implementation bugs during a full rewrite into Rust.

  • Constant-Time Guarantees: web-assembly is particularly opaque. In cryptography, protection against side-channel attacks (like timing attacks) is often more critical than general-purpose memory safety. Using audited C code that is already proven to be constant-time may be a safer WASM route than a new Rust implementation that might inadvertently introduce timing leaks. We encourage realtime constant time checks in our testbed site and appreciate any feedback to improve.

  • Toolchain Maturity: Emscripten is a mature leader in the WASM ecosystem (sometimes...bloated!). For projects needing to bridge legacy or specialized C libraries with the web, emscripten provides a stable environment that can, when optimized, outperform wasm-bindgen in raw execution speed for specific linear memory access patterns.

  • Verification Portability: security claims often live with the upstream C implementation (proof scripts, constant-time analyses, side-channel patches). Keeping that code path in WASM preserves traceability between "what was reviewed" and "what is shipped."

  • Rust Still Adds Value Around the Core: Rust/TypeScript remain excellent for orchestration layers (API ergonomics, input validation, lifecycle safety, integration code). In practice this means "safe glue + vetted primitive core" rather than forcing a full cryptographic rewrite too early.

  • Practical Side-Channel Discipline in Rust is non-trivial: Rust memory safety does not automatically guarantee constant-time behavior. Extra care is still required around branching, indexing, optimizer behavior, allocations, and panic paths, especially when targeting wasm32.

  • Long-term Strategy: once a Rust implementation reaches parity in test vectors, profiling, and side-channel review, migrating selected modules can reduce FFI complexity. Until then, Emscripten can be the lower-risk route for production-adjacent cryptographic primitives.

Why we offer WASM implementations of SQISign (NIST on-ramp only) alongside established, standards-track Falcon and Dilithium?

The "SIDH" vs. "SQIsign" Distinction

  • the algorithm that was spectacularly broken in 2022 was SIDH. The attack (the Castryck-Decru attack) exploited specific "auxiliary points", for example revealing torsion point information.

  • SQIsign is fundamentally different from SIDH, and likely structurally resistant to this specific attack because it does not appear to reveal torsion point information. Instead, SQIsign security relies on the Deuring correspondence — a mathematical link between supersingular elliptic curves and quaternion algebras — rather than the specific isogeny problem with auxiliary points used by SIDH.

  • To date (mid-2026), SQIsign remains structurally sound against the specific attacks that broke SIDH, which is why NIST accepted SQIsign onto the "on-ramp" (the Round 4/Additional Signatures track).

Smaller Signature Size Advantage

  • SQISign has smaller signatures: Short Quaternion Isogeny Signatures. This repo and associated npm package is primarily a WASM-based project targeting web or mobile, where signature size is a massive bottleneck for bandwidth.

How users can independently verify all algorithms and variants

From the repository root:

# 1) Confirm the three variant builds only change parameter set.
rg "MLK_CONFIG_PARAMETER_SET=512|MLK_CONFIG_PARAMETER_SET=768|MLK_CONFIG_PARAMETER_SET=1024" wasm/Makefile

# 2) Confirm Montgomery and Barrett reduction functions exist in upstream source.
rg "mlk_fqmul|Montgomery multiplication|mlk_barrett_reduce|Barrett reduction" vendor/mlkem-native/mlkem/src/poly.c
rg "mlk_montgomery_reduce" vendor/mlkem-native/mlkem/src/poly.h

# 3) Confirm upstream constant-time/security documentation is present.
rg "constant-time|secret-dependent|HOL-Light|CBMC" vendor/mlkem-native/README.md vendor/mlkem-native/SOUNDNESS.md

# 4) (Optional) Rebuild the vendored wasm/modules from source.
pnpm build:vendor

Notes:

  • The upstream project documents scope/assumptions in SOUNDNESS.md; review this when making compliance assertions.

Credits

  • NIST
  • signature algorithms:
    • FN-DSA (Falcon-512, Falcon-1024)
    • ML-DSA (Dilithium variants)
    • SQISign
  • module-lattice-based key-encapsulation mechanism

Installation

Install via pnpm, npm or yarn:

pnpm install quantum-resistant-rustykey
# or
npm install quantum-resistant-rustykey
# or
yarn add quantum-resistant-rustykey

Usage

Node.js example

import { loadMlKem1024, loadMlKem768, loadMlKem512 } from "quantum-resistant-rustykey";

async function main() {
  try {
    // Load the desired ML-KEM variant
    const mlkem = await loadMlKem1024(); // Options: loadMlKem1024, loadMlKem768, loadMlKem512

    // Generate key pair
    const keypair = mlkem.keypair();
    const publicKey = mlkem.buffer_to_string(keypair.get('public_key'));
    const privateKey = mlkem.buffer_to_string(keypair.get('private_key'));
    console.log("Public Key:", publicKey);
    console.log("Private Key:", privateKey);

    // Encrypt a message
    const message = "Rusty keys, the rustier the better!";
    const encrypt = mlkem.encrypt(keypair.get('public_key'))
    const sharedSecret = encrypt.get('secret')
    const encryptedMessage = await mlkem.encryptMessage(message, sharedSecret)
    console.log("Encrypted message: ", encryptedMessage)

    // Decrypt the message
    const decryptedSharedSecret = mlkem.decrypt(encrypt.get('cyphertext'), keypair.get('private_key'))
    const decryptedMessage = await mlkem.decryptMessage(encryptedMessage, decryptedSharedSecret)
    console.log("Decrypted message: ", decryptedMessage)
  } catch (error) {
    console.error("Error:", error);
  }
}

main();

Frontend example (Vite / browser)

import { loadMlKem768 } from "quantum-resistant-rustykey";

const output = document.querySelector("#output");

async function run() {
  const kem = await loadMlKem768();
  const kp = kem.keypair();

  const enc = kem.encrypt(kp.get("public_key"));
  const sharedSecretA = await enc.get("secret");
  const sharedSecretB = await kem.decrypt(enc.get("cyphertext"), kp.get("private_key"));

  const encrypted = await kem.encryptMessage("hello from browser", sharedSecretA);
  const decrypted = await kem.decryptMessage(encrypted, sharedSecretB);

  output.textContent = decrypted;
}

run().catch((err) => {
  console.error(err);
  output.textContent = "failed";
});

Signatures

All signature variants expose the same API (keypair(), sign(), verify(), buffer_to_string()).

Node.js / backend (SQISign I, SQISign V, FN-DSA-512)

import {
  loadSqisignLvl1,
  loadSqisignLvl5,
  loadFnDsa512,
} from "quantum-resistant-rustykey";

async function demo() {
  const message = new TextEncoder().encode("RustyKey signature test");

  const variants = [
    ["SQIsign-I", await loadSqisignLvl1()],
    ["SQIsign-V", await loadSqisignLvl5()],
    ["FN-DSA-512", await loadFnDsa512()],
  ] as const;

  for (const [name, signer] of variants) {
    const kp = signer.keypair();
    const pk = await kp.get("public_key");
    const sk = await kp.get("private_key");
    const sig = await signer.sign(message, sk);
    const ok = await signer.verify(sig, message, pk);
    console.log(`${name}:`, ok ? "OK" : "FAIL");
  }
}

demo().catch(console.error);

Browser / frontend (SQISign I, SQISign V, FN-DSA-512)

import {
  loadSqisignLvl1,
  loadSqisignLvl5,
  loadFnDsa512,
} from "quantum-resistant-rustykey";

const out = document.querySelector("#output") as HTMLPreElement;

async function runSignatures() {
  const message = new TextEncoder().encode("hello from browser signatures");
  const variants = [
    ["SQIsign-I", await loadSqisignLvl1()],
    ["SQIsign-V", await loadSqisignLvl5()],
    ["FN-DSA-512", await loadFnDsa512()],
  ] as const;

  const lines: string[] = [];
  for (const [name, signer] of variants) {
    const kp = signer.keypair();
    const pk = await kp.get("public_key");
    const sk = await kp.get("private_key");
    const sig = await signer.sign(message, sk);
    const ok = await signer.verify(sig, message, pk);
    lines.push(`${name}: ${ok ? "verify OK" : "verify FAILED"}`);
  }
  out.textContent = lines.join("\n");
}

runSignatures().catch((err) => {
  console.error(err);
  out.textContent = "signature demo failed";
});

Security note for web apps:

  • never store private keys in localStorage/sessionStorage
  • prefer HTTPS + short-lived keys
  • use secure key storage strategy (e.g. IndexedDB + app-level protections)

Building from Source

Prerequisites

  • Node.js >= 25.9.0
  • pnpm (or npm)
  • Emscripten or Docker — only needed if you run pnpm build:vendor to regenerate src/vendor/mlkem*.js

Build Instructions

  1. Clone the repository:
git clone https://github.com/antonymott/quantum-resistant-rustykey.git
cd quantum-resistant-rustykey
  1. Install dependencies:
pnpm i
  1. (Optional) Clone mlkem-native if you will regenerate vendored bundles:
git clone --depth 1 https://github.com/pq-code-package/mlkem-native.git vendor/mlkem-native
  1. (Optional) Rebuild src/vendor/mlkem*.js after changing wasm/ or mlkem-src/ (requires emcc or Docker):
pnpm build:vendor
  1. Compile TypeScript to dist/:
pnpm build

Testing

  • Run pnpm test for ML-KEM-512 / 768 / 1024 round-trips.

Digital Signatures (Node.js & Frontend)

All signature algorithms (FN-DSA, ML-DSA, and SQIsign) share a common interface.

import { 
  loadFnDsa512, 
  loadMlDsa3, 
  loadSqisignLvl1 
} from "quantum-resistant-rustykey";

async function main() {
  // 1. Load the algorithm (e.g., FN-DSA-512)
  const fnDsa = await loadFnDsa512();

  // 2. Generate a keypair
  const kp = fnDsa.keypair();
  const publicKey = await kp.get("public_key");
  const privateKey = await kp.get("private_key");

  // 3. Sign a message
  const message = "Authored by RustyKey";
  const signature = await fnDsa.sign(message, privateKey);
  console.log("Signature (hex):", fnDsa.buffer_to_string(signature));

  // 4. Verify the signature
  const isValid = await fnDsa.verify(signature, message, publicKey);
  console.log("Is signature valid?", isValid);
}

Note

SQIsign Performance: Level 1 signing is extremely CPU-intensive (can take seconds to minutes depending on hardware). It is recommended for "sign-once, verify-many" scenarios like certificates or firmware updates.

Browser example (local)

Project Structure

ML-KEM logic comes from mlkem-native (C), compiled with Emscripten under wasm/, wrapped by TypeScript in mlkem-src/, then bundled into src/vendor/mlkem*.js.

Security Considerations

This implementation includes patches to withstand side-channel attacks. For more information about the security improvements, see: RaspberryPi recovers secret keys from NIST winner implementation...within minutes

Contributing

  • Please make pull requests tested to work on Bun and previous Node.js versions
  • Follow the existing code style and testing practices
  • Include tests for new features
  • Update documentation as needed

License

ISC

Performance Metrics

Measured on a standard development environment (Node.js/WASM). Individual results may vary based on hardware and runtime overhead.

Algorithm KeyGen (ms) Sign (ms) Verify (ms)
FN-DSA-512 8.13 0.74 0.78
FN-DSA-1024 25.62 1.25 0.24
ML-DSA-3 (Level 3) 0.22 0.45 0.25
ML-DSA-5 (Level 5) 0.33 0.63 0.34
SQIsign L1 99.95 534.41 15.35

Known Answer Tests (KAT)

To ensure implementation correctness, our WASM build is verified against official NIST and reference test vectors.

ML-DSA-3 (Level 3 / Dilithium-3)

  • Msg: 6dbbc4375136df3b07f7c70e639e223e
  • PK: e50d03fff3b3a70961abbb92a390008dec1283f603f50cdbaaa3d00bd659bc767c3f...
  • Sig: a0c1af32f9ba4e4beea3016b96d1c780e8b5e020bb07c24478dbdd0ec875666b5a...

ML-DSA-5 (Level 5 / Dilithium-5)

  • Msg: 6dbbc4375136df3b07f7c70e639e223e
  • PK: bc89b367d4288f47c71a74679d0fcffbe041de41b5da2f5fc66d8e28c589949404...
  • Sig: 47dc5764266841c1af3073fcead6a13d372979e6cca0b2952b349915f54ef66312...

SQIsign Level 1

  • Msg: d81c4d8d734fcbfbeade3d3f8a039faa2a2c9957e835ad55b22e75bf57bb556ac8
  • PK: 07CCD21425136F6E865E497D2D4D208F0054AD81372066E817480787AAF7B2029...
  • Sig: 84228651f271b0f39f2f19f2e8718f31ed3365ac9e5cb303afe663d0cfc11f0455...

Full byte-perfect vectors are included in the src/*.test.ts files.

Funding

This project was generously supported by:

  • University of Quantum Science
  • RustyKey®
  • Customers' Yachts® Advisors
  • BuzzyBee®
RustyKey Logo BuzzyBee Logo

How we work (aka Conduct)

"You are very welcome to our house: It must appear in other ways than words!" - W. Shakespeare

  • do you think of yourself as total n00b...or seasoned and cynical Cryptologic Scientist. WELCOME one and all!
  • consider helping us build a friendly, safe and welcoming environment for all, regardless of level of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic.
  • please avoid aliases or nicknames that might detract from a friendly, safe and welcoming environment.
  • getting annoyed? First try being kind and courteous: someone may simply have had a bad day.
  • people have differences of opinion, usually every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer.
  • go light on unstructured critique, encourage others!
  • if you feel you have been or are being harassed or made uncomfortable by a community member, contact BuzzyBee® our friendly multi-LLM on the chat widget on our testbed site

Appendix (WIP) testbed 'coming soon' features

Below our some examples of stats and interactivity we plan to add to the testbed depending on user-interest that will help users understand the trade-offs between lattice-based (ML-KEM/DSA) and isogeny-based (SQISign) crypto:

  • Memory Peak (Heap Usage): WASM runs in a linear memory space. Tracking performance.memory.usedJSHeapSize (in supported browsers) or monitoring the WASM instance’s memory growth is vital, especially for ML-DSA (Dilithium), which can be memory-intensive.

  • Serialized Payload Size: Explicitly display the "Over-the-wire" size for public keys and signatures. Seeing a 204-byte SQISign signature next to a 2,420-byte Dilithium-2 signature makes the WebAuthn buffer issue immediately obvious.

  • WASM Instantiation Time: Measure how long it takes to compile and initialize the module. This is a "hidden" latency cost in web apps that users often overlook.

  • Interactive "Live Insight" Buttons

    • "Simulate CTAP2 Limit": A toggle that "clips" the buffer at 1024 bytes. If the user tries to run Dilithium, it throws a visual error, while SQISign passes—demonstrating that "silent barrier" they mentioned
    • "Throttle CPU": An option to simulate mobile/embedded performance (standard in Chrome DevTools, but great as a one-click button). This highlights how SQISign is great for size but potentially slower on verification time compared to Falcon or Dilithium.
    • "Batch Verification Run": A button to run 100 signatures in a loop. This generates a jitter chart to show if the Montgomery constant-time implementation stays flat or fluctuates under load

About

Fast, secure WASM implementations of NIST and NIST 'on-ramp' post-quantum-resistant tools

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors