quantum-resistant-rustykey 0.13.1 → 0.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +11 -673
  2. package/package.json +16 -12
package/README.md CHANGED
@@ -1,680 +1,18 @@
1
- # <img src="./logo-rustykey.png" width="57" align="center" /> Quantum-resistant RustyKey®
1
+ # <img src="./logo-rustykey.png" width="48" align="center" alt="" /> quantum-resistant-rustykey
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/quantum-resistant-rustykey)](https://npmjs.com)
4
- [![Weekly Downloads](https://img.shields.io/npm/dw/quantum-resistant-rustykey)](https://npmjs.com)
5
- ![Node v26.4.0](https://img.shields.io/badge/node-v26.4.0-blue.svg)
3
+ [![npm version](https://img.shields.io/npm/v/quantum-resistant-rustykey)](https://www.npmjs.com/package/quantum-resistant-rustykey)
4
+ [![docs](https://img.shields.io/badge/docs-GitHub%20Pages-1a5f4a)](https://antonymott.github.io/quantum-resistant-rustykey/)
5
+ ![Node v26.5.0](https://img.shields.io/badge/node-v26.5.0-blue.svg)
6
6
 
7
-
8
- Fast, secure WebAssembly implementations of useful post-quantum-resistant tools both for backend (node) and frontend web.
9
-
10
- ```bash
11
- # Install via pnpm (preferred)
12
- pnpm i quantum-resistant-rustykey
13
- # or
14
- bun add quantum-resistant-rustykey
15
- npm add quantum-resistant-rustykey
16
- ```
17
-
18
- ## Implementation status: Pre-production (stable for testing)
19
-
20
- - ***Recommendation***: Await v1.0.0 (following security audit) for production/regulated deployment.
21
- - includes NIST approved and NIST "on-ramp" round 3 candidate SQISign
22
- - **SQIsign** Level 5, Level 3, Level 1 NOT approved yet by NIST, refer [cose-sqisign] (https://datatracker.ietf.org/doc/draft-mott-cose-sqisign/)
23
- - **ML-DSA** ML-DSA-65, ML-DSA-87
24
- - **FN-DSA** FN-DSA-512, FN-DSA-1024
25
- - **SLH-DSA** (SPHINCS+) SLH-DSA-SHA2-128s / 192s / 256s — hash-based, NIST-standardized ([FIPS 205](https://csrc.nist.gov/pubs/fips/205/final))
26
- - **ML-KEM** 512, 768, 1024 using [mlkem-native](https://github.com/pq-code-package/mlkem-native).
27
-
28
- ### SQISign is 'NIST-on-ramp': get ahead and test TODAY, SQISign is the ONLY signature for constrained-development use
29
- *TLDR; breeze past the "silent" barrier to post-quantum adoption: 1024-byte buffer limit in many existing FIDO2/WebAuthn implementations*
30
- - support our IETF standards track draft by taking our free code for a spin, the more users enjoying these packages, the faster things go [cose-sqisign](https://www.ietf.org/archive/id/draft-mott-cose-sqisign-03.html)
31
-
32
- #### ⚠️ IMPORTANT SPECIFICATION NOTICE (as of July 2026)
33
- COSE/JOSE Algorithm IDs (-61, -62, -63) and case-sensitive identifier strings (SQIsign-L1, SQIsign-L3, SQIsign-L5) used in this package are derived directly from the active [cose-sqisign](https://datatracker.ietf.org/doc/draft-mott-cose-sqisign/)Internet-Draft. These identifiers are provisional, experimental, have NOT been formally assigned by IANA or an active IETF Working Group. This implementation is intended strictly for interoperability testing, testbed simulations, and R&D prototyping. Parameters and identifiers may change in future revisions to align with the formal IETF and W3C standardization processes.
34
-
35
- #### WebAuthn PQC Signature size constraints
36
- Dilithium variants, and Falcon-1024 are physically incompatible with millions of existing FIDO2/WebAuthn authenticators that rely on the CTAP2 1024-byte buffer limit.
37
-
38
- - 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.
39
-
40
- - Lattice-based mismatch: Dilithium-2 signatures (approx. 2,420 bytes) simply cannot fit into standard 1024-byte buffers found in many current authenticators.
41
-
42
- - 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.
43
-
44
- #### Critical use case example
45
- For constrained-device or mission-critical applications like low-latency augmented reality remote telesurgery, 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 as the small signatures is the only PQC option that works with current demanding hardware constraints, with the practical advantage of near-immediate quantum-resistant public key ceremonies without breaking the existing WebAuthn ecosystem.
46
-
47
- ## Broad user-friendly live example testbed and playground
48
-
49
- Live at **[pqc.rustykey.me](https://pqc.rustykey.me)** — a test environment where general-purpose users and seasoned cryptanalysts can encrypt and decrypt and play, using all three variants of KEM and test WebAuthn implementations using the signature algorithms.
50
- - lattice-based vs isogeny: run tests to check: Montgomery constant times, the surprising difference in time taken for the various steps
51
- - any and all who are interested kicking the tires of SQISign and other PQC algorithms are encouraged to suggest improvements. The playground's goal is to help a wider audience see how PQC works under the hood, find bugs, suggest improvements and help adopt it more quickly without breaking existing infrastructure.
52
-
53
- ## Security assurance and verification
54
-
55
- This project relies on upstream `mlkem-native` for arithmetic/security properties.
56
- The three parameter sets (512/768/1024) use the same implementation family and differ only by compile-time parameter selection. Bear in mind for testing that the formal proofs validate native C and assembly source code only: the moment we passed that through a custom wasm/Makefile, those upstream formal verification guarantees evaporated.
57
-
58
- ### Upstream evidence
59
-
60
- - `mlkem-native` security/formal-verification statements:
61
- - [README.md](https://github.com/pq-code-package/mlkem-native/blob/main/README.md)
62
- - [SOUNDNESS.md](https://github.com/pq-code-package/mlkem-native/blob/main/SOUNDNESS.md)
63
- - [proofs/hol_light/README.md](https://github.com/pq-code-package/mlkem-native/blob/main/proofs/hol_light/README.md)
64
- - [proofs/cbmc/README.md](https://github.com/pq-code-package/mlkem-native/blob/main/proofs/cbmc/README.md)
65
- - Arithmetic implementation details in upstream source:
66
- - Montgomery multiplication path in `mlk_fqmul()`:
67
- - [mlkem/src/poly.c](https://github.com/pq-code-package/mlkem-native/blob/main/mlkem/src/poly.c)
68
- - Barrett reduction path in `mlk_barrett_reduce()`:
69
- - [mlkem/src/poly.c](https://github.com/pq-code-package/mlkem-native/blob/main/mlkem/src/poly.c)
70
- - Generic Montgomery reduction helper:
71
- - [mlkem/src/poly.h](https://github.com/pq-code-package/mlkem-native/blob/main/mlkem/src/poly.h)
72
-
73
- ### Constant-time claims and proofs
74
-
75
- - Constant-time claims and proofs are provided upstream by `mlkem-native` (see links above).
76
- - This package builds the same source for all three variants by changing only `MLK_CONFIG_PARAMETER_SET` in `wasm/Makefile`.
77
- - Variant sizes/parameters are defined upstream in `mlkem/mlkem_native.h`.
78
-
79
- ### We predominantly use C, not Rust for our web-assembly (WASM) modules: Why?
80
-
81
- - ***we're not cryptanalysts, not the smartypants type. We choose not to 'roll our own' cryptography as it's worrying enough we compile the C into web-assembly...what if our work strips out constant-time protections? Think of us as enthusiastic interweb equivalents of stonemasons, ironworkers, mechanics and logistics crew. If you're OK that we'll definitely break a few things along the way, you know where to find us: well below decks in grubby overalls, keeping the engines humming. Far above us, topside, lounge the elegantly-dressed OG engineers who long ago earned their Hugo spritzes. We publish frequently 'into the wild' to guarantee our code is battle-tested, bugs found quickly. It only works because we rely absolutely on vetted, peer-reviewed designs from you and the rest of the research community.***
82
-
83
- It seems all the cool cryptanalyst kids nowadays rely on Rust's proven memory and concurrency safety and high performance without a Garbage Collector. Hre it's old-school for the time being: shipped cryptographic WASM modules will continue to be built via Emscripten from vetted C/C++ upstream code. We do love Rust when used at the right time: Rust/TypeScript will be primarily used for package-level ergonomics and integration layers.
84
-
85
- ### detail. TLDR; downstream security != upstream security
86
- A Rust-foward approach is implied in our brand, so building direct form C libraries to web-assembly deserves more explanation. Many developers new to web-assembly migrations (i.e. from other languages) don't realize that final WebAssembly (WASM) bytecode looks completely different inside depending on if one begins with C or Rust. Yes, web-assembly modules from each (Rust or C) will function, about the same speed, and indeed will be equally platform agnostic for deployment. But we observed slight timing differences between web-assembly modules compiled from Rust and from C, so we're sticking with C.
87
-
88
- 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:
89
-
90
- - 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.
91
-
92
- - 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](https://pqc.rustykey.me) and appreciate any feedback to improve.
93
-
94
- - 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.
95
-
96
- - 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."
97
-
98
- - 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.
99
-
100
- - 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.
101
-
102
- - 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 appears to be the lower-risk route for production-adjacent cryptographic primitives.
103
-
104
-
105
- ### Why we offer WASM implementations of SQISign (NIST on-ramp only) alongside established, standards-track Falcon and Dilithium?
106
-
107
- ### The "SIDH" vs. "SQIsign" Distinction
108
- - 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.
109
-
110
- - 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.
111
-
112
- - 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).
113
-
114
- ### Smaller Signature Size Advantage
115
- - 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.
116
-
117
-
118
- ### How to independently verify all algorithms and variants
119
-
120
- From the repository root:
121
-
122
- ```bash
123
- # 1) Confirm the three variant builds only change parameter set.
124
- rg "MLK_CONFIG_PARAMETER_SET=512|MLK_CONFIG_PARAMETER_SET=768|MLK_CONFIG_PARAMETER_SET=1024" wasm/Makefile
125
-
126
- # 2) Confirm Montgomery and Barrett reduction functions exist in upstream source.
127
- rg "mlk_fqmul|Montgomery multiplication|mlk_barrett_reduce|Barrett reduction" vendor/mlkem-native/mlkem/src/poly.c
128
- rg "mlk_montgomery_reduce" vendor/mlkem-native/mlkem/src/poly.h
129
-
130
- # 3) Confirm upstream constant-time/security documentation is present.
131
- rg "constant-time|secret-dependent|HOL-Light|CBMC" vendor/mlkem-native/README.md vendor/mlkem-native/SOUNDNESS.md
132
-
133
- # 4) (Optional) Rebuild the vendored wasm/modules from source.
134
- pnpm build:vendor
135
- ```
136
-
137
- Notes:
138
- - The upstream project documents scope/assumptions in `SOUNDNESS.md`; review this when making compliance assertions.
139
-
140
- ## Credits
141
-
142
- - NIST
143
- - signature algorithms:
144
- - FN-DSA (Falcon-512, Falcon-1024)
145
- - ML-DSA (Dilithium variants)
146
- - SQISign Team
147
- - module-lattice-based key-encapsulation mechanism
148
- - ML-KEM
149
- - approach adapted from Dmitry Chestnykh's `mlkem-wasm`: https://github.com/dchest/mlkem-wasm
150
-
151
- ## Usage
152
-
153
- ### SQISign-webGPU (browser accelerated "racecar")
154
-
155
- Browser-only accelerated SQISign variants using **SharedArrayBuffer** and **WebGPU**.
156
- These are separate from the standard server-compatible WASM loaders and are **not available in Node.js**.
157
-
158
- #### Variant names
159
-
160
- | Security level | Standard loader | Accelerated (browser) |
161
- |----------------|-----------------|------------------------|
162
- | L5 | `loadSqisignLvl5()` → SQISign-L5 | `loadSqisignLvl5WebGpu()` → **SQISign-L5-webGPU** |
163
- | L3 | `loadSqisignLvl3()` → SQISign-L3 | `loadSqisignLvl3WebGpu()` → **SQISign-L3-webGPU** |
164
- | L1 | `loadSqisignLvl1()` → SQISign-L1 | `loadSqisignLvl1WebGpu()` → **SQISign-L1-webGPU** |
165
-
166
- Labels are exported as `SQISIGN_WEBGPU_VARIANT_LABELS`.
167
-
168
- #### Requirements (COOP / COEP)
169
-
170
- Accelerated SQISign requires a **cross-origin isolated** browsing context:
171
-
172
- 1. `crossOriginIsolated === true`
173
- 2. `SharedArrayBuffer` available
174
- 3. `navigator.gpu` (WebGPU) available
175
-
176
- Serve these response headers on pages that load the accelerated variants:
177
-
178
- ```http
179
- Cross-Origin-Opener-Policy: same-origin
180
- Cross-Origin-Embedder-Policy: require-corp
181
- ```
182
-
183
- ⚠️ IMPORTANT for this highly-tuned "racecar" version
184
-
185
- Enforcing these headers on a production web app creates a challenging isolation boundary
186
- - Breaking Third Parties: Every single script, analytic tracker, embedded iframe (like Stripe or YouTube), and cross-origin image on that page will immediately break or be blocked unless they are explicitly served with a Cross-Origin-Resource-Policy header
187
- - Maintenance Overhead: the accelerated version is browser-only frontend, use our standard web-assembly package in nodejs backend***
188
- - ok, so you're a self-confessed speed demon, you've read the cautions. But before you jump into this shiny new machine, remember you asked the crew to fit 'racing slicks for dry weather only'. If the weather changes unexpectedly, you'll find yourself behind the wheel of an 'aquatic hydroplaning device'. No airbags.
189
-
190
- #### Specific risks introduced with this "racecar" version
191
-
192
- 1. Security unknowns
193
- - Side-Channel Vulnerabilities are untested. Offloading cryptographic math to a smartphone's WebGPU - billions of them, various model, makes, years - means executing field arithmetic directly on the host computer's GPU threads. Graphics processors are fundamentally optimized for parallel throughput, not constant-time deterministic execution.
194
- - Novel unseen threats: Running cryptographic primitives on shared GPU hardware makes them highly susceptible to advanced timing and memory-coalescing side-channel attacks. ⚠️ Upstream C formal proofs absolutely do not account for WebGPU compute shader pipeline execution. ⚠️
195
-
196
- ##### Next.js example
197
-
198
- ```js
199
- // next.config.mjs
200
- async headers() {
201
- return [
202
- {
203
- source: "/your-pqc-page/:path*",
204
- headers: [
205
- { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
206
- { key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
207
- ],
208
- },
209
- ];
210
- }
211
- ```
212
-
213
- Third-party scripts, images, and iframes on the same page must be served with appropriate `Cross-Origin-Resource-Policy` (or `crossorigin` attributes) or they will be blocked under `require-corp`.
214
-
215
- ##### Worker script (required for bundlers like Next.js)
216
-
217
- Bundled apps cannot load `sqisign-accel-worker.js` from `node_modules`. Copy it to a public URL:
218
-
219
- ```bash
220
- cp node_modules/quantum-resistant-rustykey/dist/sqisign-accel-worker.js public/pqc/
221
- ```
222
-
223
- The testbed sync script does this automatically (`pnpm pqc:sync-local`).
224
-
225
- Default worker URL: `/pqc/sqisign-accel-worker.js`. Override if needed:
226
-
227
- ```ts
228
- import { setSqisignAccelWorkerUrl } from "quantum-resistant-rustykey";
229
- setSqisignAccelWorkerUrl("/your/path/sqisign-accel-worker.js");
230
- ```
231
-
232
- If the worker fails to load, the library falls back to main-thread WASM (same crypto, UI may stutter on L5).
233
-
234
- #### Usage
235
-
236
- ```ts
237
- import {
238
- benchSqisignWebGpu,
239
- getSqisignWebGpuSupport,
240
- isSqisignWebGpuAvailable,
241
- loadSqisignLvl5WebGpu,
242
- SQISIGN_WEBGPU_VARIANT_LABELS,
243
- } from "quantum-resistant-rustykey";
244
-
245
- const support = getSqisignWebGpuSupport();
246
- if (!support.available) {
247
- console.warn(support.reason);
248
- }
249
-
250
- // Same IFnDsa surface as standard loaders
251
- const sq = await loadSqisignLvl5WebGpu();
252
- const kp = sq.keypair();
253
- const pk = await kp.get("public_key");
254
- const sk = await kp.get("private_key");
255
- const msg = new TextEncoder().encode("hello");
256
- const sig = await sq.sign(msg, sk);
257
- const ok = await sq.verify(sig, msg, pk);
258
-
259
- // Built-in keygen + sign + verify benchmark (browser only)
260
- const bench = await benchSqisignWebGpu("lvl5");
261
- console.log(bench.algorithm); // SQISign-L5-webGPU
262
- console.log(bench.steps);
263
- ```
264
-
265
- #### Architecture
266
-
267
- 1. **Web Worker** — SQISign WASM runs off the main thread (worker bundle: `dist/sqisign-accel-worker.js`).
268
- 2. **SharedArrayBuffer** — enabled when COOP/COEP isolate the origin (required for future pthread WASM builds).
269
- 3. **WebGPU** — device initialization and compute pipeline warmup for field-arithmetic acceleration.
270
-
271
- Standard `loadSqisignLvl*` loaders remain unchanged for Node.js and non-isolated browsers.
272
-
273
- #### Live comparison
274
-
275
- The [pqc.rustykey.me](https://pqc.rustykey.me) testbed shows side-by-side timings for SQISign-L1/L3/L5 vs SQISign-L1-webGPU / L3 / L5 on the **COSE** and **Verifiable Credentials** tabs when SQISign is selected.
276
-
277
- ### Node.js example
278
-
279
- ```typescript
280
- import { loadMlKem1024, loadMlKem768, loadMlKem512 } from "quantum-resistant-rustykey";
281
-
282
- async function main() {
283
- try {
284
- // Load the desired ML-KEM variant
285
- const mlkem = await loadMlKem1024(); // Options: loadMlKem1024, loadMlKem768, loadMlKem512
286
-
287
- // Generate key pair
288
- const keypair = mlkem.keypair();
289
- const publicKey = mlkem.buffer_to_string(keypair.get('public_key'));
290
- const privateKey = mlkem.buffer_to_string(keypair.get('private_key'));
291
- console.log("Public Key:", publicKey);
292
- console.log("Private Key:", privateKey);
293
-
294
- // Encrypt a message
295
- const message = "Rusty keys, the rustier the better!";
296
- const encrypt = mlkem.encrypt(keypair.get('public_key'))
297
- const sharedSecret = encrypt.get('secret')
298
- const encryptedMessage = await mlkem.encryptMessage(message, sharedSecret)
299
- console.log("Encrypted message: ", encryptedMessage)
300
-
301
- // Decrypt the message
302
- const decryptedSharedSecret = mlkem.decrypt(encrypt.get('cyphertext'), keypair.get('private_key'))
303
- const decryptedMessage = await mlkem.decryptMessage(encryptedMessage, decryptedSharedSecret)
304
- console.log("Decrypted message: ", decryptedMessage)
305
- } catch (error) {
306
- console.error("Error:", error);
307
- }
308
- }
309
-
310
- main();
311
- ```
312
-
313
- ### Frontend example (Vite / browser)
314
-
315
- ```typescript
316
- import { loadMlKem768 } from "quantum-resistant-rustykey";
317
-
318
- const output = document.querySelector("#output");
319
-
320
- async function run() {
321
- const kem = await loadMlKem768();
322
- const kp = kem.keypair();
323
-
324
- const enc = kem.encrypt(kp.get("public_key"));
325
- const sharedSecretA = await enc.get("secret");
326
- const sharedSecretB = await kem.decrypt(enc.get("cyphertext"), kp.get("private_key"));
327
-
328
- const encrypted = await kem.encryptMessage("hello from browser", sharedSecretA);
329
- const decrypted = await kem.decryptMessage(encrypted, sharedSecretB);
330
-
331
- output.textContent = decrypted;
332
- }
333
-
334
- run().catch((err) => {
335
- console.error(err);
336
- output.textContent = "failed";
337
- });
338
- ```
339
-
340
- ## Signatures
341
-
342
- All signature variants expose the same API (`keypair()`, `sign()`, `verify()`, `buffer_to_string()`).
343
-
344
- ### Node.js / backend (SQISign I, SQISign V, FN-DSA-512)
345
-
346
- ```typescript
347
- import {
348
- loadSqisignLvl1,
349
- loadSqisignLvl5,
350
- loadFnDsa512,
351
- } from "quantum-resistant-rustykey";
352
-
353
- async function demo() {
354
- const message = new TextEncoder().encode("RustyKey signature test");
355
-
356
- const variants = [
357
- ["SQIsign-I", await loadSqisignLvl1()],
358
- ["SQIsign-V", await loadSqisignLvl5()],
359
- ["FN-DSA-512", await loadFnDsa512()],
360
- ] as const;
361
-
362
- for (const [name, signer] of variants) {
363
- const kp = signer.keypair();
364
- const pk = await kp.get("public_key");
365
- const sk = await kp.get("private_key");
366
- const sig = await signer.sign(message, sk);
367
- const ok = await signer.verify(sig, message, pk);
368
- console.log(`${name}:`, ok ? "OK" : "FAIL");
369
- }
370
- }
371
-
372
- demo().catch(console.error);
373
- ```
374
-
375
- ### Browser / frontend (SQISign I, SQISign V, FN-DSA-512)
376
-
377
- ```typescript
378
- import {
379
- loadSqisignLvl1,
380
- loadSqisignLvl5,
381
- loadFnDsa512,
382
- } from "quantum-resistant-rustykey";
383
-
384
- const out = document.querySelector("#output") as HTMLPreElement;
385
-
386
- async function runSignatures() {
387
- const message = new TextEncoder().encode("hello from browser signatures");
388
- const variants = [
389
- ["SQIsign-I", await loadSqisignLvl1()],
390
- ["SQIsign-V", await loadSqisignLvl5()],
391
- ["FN-DSA-512", await loadFnDsa512()],
392
- ] as const;
393
-
394
- const lines: string[] = [];
395
- for (const [name, signer] of variants) {
396
- const kp = signer.keypair();
397
- const pk = await kp.get("public_key");
398
- const sk = await kp.get("private_key");
399
- const sig = await signer.sign(message, sk);
400
- const ok = await signer.verify(sig, message, pk);
401
- lines.push(`${name}: ${ok ? "verify OK" : "verify FAILED"}`);
402
- }
403
- out.textContent = lines.join("\n");
404
- }
405
-
406
- runSignatures().catch((err) => {
407
- console.error(err);
408
- out.textContent = "signature demo failed";
409
- });
410
- ```
411
-
412
- Security note for web apps:
413
- - never store private keys in `localStorage`/`sessionStorage`
414
- - prefer HTTPS + short-lived keys
415
- - use secure key storage strategy (e.g. IndexedDB + app-level protections)
416
-
417
- ### SLH-DSA (SPHINCS+) — hash-based signatures
418
-
419
- SLH-DSA is a **stateless hash-based** signature scheme standardized by NIST in [FIPS 205](https://csrc.nist.gov/pubs/fips/205/final). Its security rests only on the security of its underlying hash function, giving it the most conservative assumptions of any signature family in this package — at the cost of large signatures and slow signing. This package ships the three SHA2 **`s` (small-signature)** parameter sets.
420
-
421
- | Loader | Variant | COSE (provisional) | Public key | Secret key | Signature | W3C appendix |
422
- | :--- | :--- | :---: | :---: | :---: | :---: | :---: |
423
- | `loadSlhDsa128()` | SLH-DSA-SHA2-128s | `0x1220` | 32 B | 64 B | 7,856 B | ✅ L1 golden vector |
424
- | `loadSlhDsa192()` | SLH-DSA-SHA2-192s | `0x1221` | 48 B | 96 B | 16,224 B | generated keys |
425
- | `loadSlhDsa256()` | SLH-DSA-SHA2-256s | `0x1222` | 64 B | 128 B | 29,792 B | generated keys |
426
-
427
- > [!NOTE]
428
- > COSE identifiers above are **provisional** and used for testbed/interop only — SLH-DSA COSE code points are not yet finalized by IANA. Cryptosuite names follow the W3C VC data-integrity pattern: `slhdsa128-rdfc-2024`, `slhdsa128-jcs-2024` (and `slhdsa192-*` / `slhdsa256-*`).
429
-
430
- > [!WARNING]
431
- > **SLH-DSA signing is slow and signatures are large** (kilobytes, not the ~200 bytes of SQISign). It is unsuitable for the CTAP2 1024-byte WebAuthn buffer. Prefer it where conservative, hash-only security matters and bandwidth/latency are not constrained (e.g. long-lived certificates, firmware, archival VCs). Verification is comparatively fast.
432
-
433
- All SLH-DSA loaders expose the same `IFnDsa` interface (`keypair()`, `sign()`, `verify()`, `buffer_to_string()`):
434
-
435
- ```typescript
436
- import { loadSlhDsa128 } from "quantum-resistant-rustykey";
437
-
438
- async function demo() {
439
- const slh = await loadSlhDsa128(); // or loadSlhDsa192 / loadSlhDsa256
440
- const kp = slh.keypair();
441
- const publicKey = await kp.get("public_key");
442
- const privateKey = await kp.get("private_key");
443
-
444
- const message = new TextEncoder().encode("Authored by RustyKey (SLH-DSA)");
445
- const signature = await slh.sign(message, privateKey);
446
- const isValid = await slh.verify(signature, message, publicKey);
447
- console.log("SLH-DSA-SHA2-128s valid?", isValid);
448
- }
449
-
450
- demo().catch(console.error);
451
- ```
452
-
453
- The pure-JS SLH-DSA path is provided via [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) and works identically in Node.js and the browser (no WASM/COOP-COEP requirements).
454
-
455
- ## REST endpoint summary — Verifiable Credentials (VC)
456
-
457
- This library is the cryptographic core behind the **[pqc.rustykey.me](https://pqc.rustykey.me)** testbed. The testbed exposes a small HTTP surface (Next.js route handlers) that wraps the loaders above so you can produce W3C **Verifiable Credential** data-integrity proofs over the wire. The package itself ships no server — this section documents the reference endpoints so integrators can call or replicate them.
458
-
459
- All endpoints run server-side (`runtime: "nodejs"`) and accept/return JSON.
460
-
461
- ### `POST /api/pqc/vc/sign` — one-shot signed VC
462
-
463
- The high-level endpoint: generates a fresh keypair, canonicalizes the document, hashes it, and returns the data-integrity proof value.
464
-
465
- **Request body**
466
-
467
- | Field | Type | Description |
468
- | :--- | :--- | :--- |
469
- | `document` | object | The unsecured W3C credential payload. |
470
- | `algorithm` | string | One of the identifiers in the table below. |
471
- | `dataset_canonicalization` | `"rdfc"` \| `"ics"` | RDF Dataset Canonicalization (`rdfc`) or JSON canonicalization (`ics`/JCS). |
472
-
473
- **Supported `algorithm` identifiers**
474
-
475
- | Identifier | Family | Cryptosuite prefix |
476
- | :--- | :--- | :--- |
477
- | `SQIsign-L1` / `SQIsign-L3` / `SQIsign-L5` | SQISign | `sqisign1` / `sqisign3` / `sqisign5` |
478
- | `mldsa44` | ML-DSA | `mldsa44` |
479
- | `falcon512` | FN-DSA | `falcon512` |
480
- | `slhdsa128` / `slhdsa192` / `slhdsa256` | **SLH-DSA** | `slhdsa128` / `slhdsa192` / `slhdsa256` |
481
-
482
- **Response body**
483
-
484
- ```jsonc
485
- {
486
- "runtime": "nodejs",
487
- "totalMs": 1234.5,
488
- "algorithm": "slhdsa128",
489
- "cryptosuite": "slhdsa128-rdfc-2024",
490
- "dataset_canonicalization": "rdfc",
491
- "publicKey": "…hex…",
492
- "privateKey": "…hex…",
493
- "canonicalizedDoc": "…canonical form…",
494
- "hash": "…hex…",
495
- "signature": "z…", // multibase proofValue
496
- "signatureHex": "…hex…"
497
- }
498
- ```
499
-
500
- **Example**
7
+ TypeScript-first **WebAssembly post-quantum** crypto for Node and the browser — SQIsign, ML-DSA, FN-DSA, SLH-DSA, ML-KEM.
8
+ RustyKey® is a **FIDO Alliance** member.
501
9
 
502
10
  ```bash
503
- curl -X POST https://pqc.rustykey.me/api/pqc/vc/sign \
504
- -H "Content-Type: application/json" \
505
- -d '{
506
- "document": { "@context": ["https://www.w3.org/ns/credentials/v2"], "type": ["VerifiableCredential"] },
507
- "algorithm": "slhdsa128",
508
- "dataset_canonicalization": "rdfc"
509
- }'
11
+ pnpm i quantum-resistant-rustykey@latest
510
12
  ```
511
13
 
512
- ### `POST /api/pqc/vc/proof` — full pipeline / bring-your-own-keys
513
-
514
- Lower-level endpoint used by the testbed's step-by-step VC view. It has two modes:
515
-
516
- - **Proof pipeline** — send `unsecuredDocument`, `family` (`sqisign` \| `mldsa` \| `falcon` \| `slhdsa`), `level` (`l1` \| `l3` \| `l5`), `canonicalization` (`rdfc` \| `jcs`), plus `publicKeyHex` / `secretKeyHex` (and optional `verificationMethod`). Returns each canonicalize → hash → sign → verify step.
517
- - **Sign-only** — send `hashDataHex` with `family`, `level`, `publicKeyHex`, `secretKeyHex` (and optional `referenceProofValue`) to sign a pre-computed hash and verify it (including against a W3C appendix golden value).
518
-
519
- ### `PUT /api/pqc/vc/proof` — keygen
520
-
521
- Send `{ "family": "slhdsa", "level": "l1" }` to get a fresh `publicKeyHex` / `secretKeyHex` and the resolved algorithm label. Handy for pre-provisioning keys before calling the proof pipeline.
522
-
523
- ## Building from Source
524
-
525
- ### Prerequisites
526
-
527
- - Emscripten **or** Docker — only needed if you run `pnpm build:vendor` to regenerate `src/vendor/mlkem*.js`
528
-
529
- ### Build Instructions
530
-
531
- 1. Clone the repository:
532
- ```bash
533
- git clone https://github.com/antonymott/quantum-resistant-rustykey.git
534
- cd quantum-resistant-rustykey
535
- ```
536
-
537
- 2. Install dependencies:
538
- ```bash
539
- pnpm i
540
- ```
541
-
542
- 3. (Optional) Clone [mlkem-native](https://github.com/pq-code-package/mlkem-native) if you will regenerate vendored bundles:
543
- ```bash
544
- git clone --depth 1 https://github.com/pq-code-package/mlkem-native.git vendor/mlkem-native
545
- ```
546
-
547
- 4. (Optional) Rebuild `src/vendor/mlkem*.js` after changing `wasm/` or `mlkem-src/` (requires `emcc` or Docker):
548
- ```bash
549
- pnpm build:vendor
550
- ```
551
-
552
- 5. Compile TypeScript to `dist/`:
553
-
554
- ```bash
555
- pnpm build
556
- ```
557
-
558
-
559
- ## Testing
560
-
561
- - Run `pnpm test` for ML-KEM-512 / 768 / 1024 round-trips.
562
-
563
- ### Digital Signatures (Node.js & Frontend)
564
-
565
- All signature algorithms (**FN-DSA**, **ML-DSA**, **SQIsign**, and **SLH-DSA**) share a common interface.
566
-
567
- ```typescript
568
- import {
569
- loadFnDsa512,
570
- loadMlDsa3,
571
- loadSqisignLvl1
572
- } from "quantum-resistant-rustykey";
573
-
574
- async function main() {
575
- // 1. Load the algorithm (e.g., FN-DSA-512)
576
- const fnDsa = await loadFnDsa512();
577
-
578
- // 2. Generate a keypair
579
- const kp = fnDsa.keypair();
580
- const publicKey = await kp.get("public_key");
581
- const privateKey = await kp.get("private_key");
582
-
583
- // 3. Sign a message
584
- const message = "Authored by RustyKey";
585
- const signature = await fnDsa.sign(message, privateKey);
586
- console.log("Signature (hex):", fnDsa.buffer_to_string(signature));
587
-
588
- // 4. Verify the signature
589
- const isValid = await fnDsa.verify(signature, message, publicKey);
590
- console.log("Is signature valid?", isValid);
591
- }
592
- ```
593
-
594
- > [!NOTE]
595
- > **SQIsign Performance**: Level 1 signing is CPU-intensive (can take seconds to minutes depending on hardware). We recommend "sign-once, verify-many" scenarios for certificates or firmware updates.
596
-
597
- ## Browser example (local)
598
-
599
- See the live [PQC testbed](https://pqc.rustykey.me) or run the frontend examples above in a Vite/browser project.
600
-
601
- ## Project Structure
602
-
603
- 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`.
604
-
605
- ## Security Considerations
606
-
607
- 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](https://kannwischer.eu/papers/2024_kyberslash_preprint20240628.pdf)
608
-
609
-
610
-
611
- ## License
612
-
613
- ISC
614
-
615
-
616
- ## Performance Metrics
617
-
618
- Measured on a standard development environment (Node.js/WASM). Individual results may vary based on hardware and runtime overhead.
619
-
620
- | Algorithm | KeyGen (ms) | Sign (ms) | Verify (ms) |
621
- | :--- | :---: | :---: | :---: |
622
- | **FN-DSA-512** | 8.13 | 0.74 | 0.78 |
623
- | **FN-DSA-1024** | 25.62 | 1.25 | 0.24 |
624
- | **ML-DSA-3 (Level 3)** | 0.22 | 0.45 | 0.25 |
625
- | **ML-DSA-5 (Level 5)** | 0.33 | 0.63 | 0.34 |
626
- | **SQIsign L1** | 99.95 | 534.41 | 15.35 |
627
- | **SQIsign L5** | 312.47 | 1823.16 | 48.92 |
628
-
629
- ---
630
-
631
- ## Known Answer Tests (KAT)
632
-
633
- To ensure implementation correctness, our WASM build is verified against official NIST and reference test vectors.
634
-
635
- ### ML-DSA-3 (Level 3 / Dilithium-3)
636
- * **Msg**: `6dbbc4375136df3b07f7c70e639e223e`
637
- * **PK**: `e50d03fff3b3a70961abbb92a390008dec1283f603f50cdbaaa3d00bd659bc767c3f...`
638
- * **Sig**: `a0c1af32f9ba4e4beea3016b96d1c780e8b5e020bb07c24478dbdd0ec875666b5a...`
639
-
640
- ### FN-DSA-1024 (Falcon-1024)
641
- * **Msg**: `6dbbc4375136df3b07f7c70e639e223e`
642
- * **PK**: `09f3d01b9f3aee40b6e7fbcd9c60fad6c2e8fc10c73a44e3ecb1d3dfb99e1ba172...`
643
- * **Sig**: `5539eb7e0e2a3be62b80ef0a85c6e09f3d6a3bc9e3e1c40d2a3ea7b64a3d09f1a...`
644
-
645
- ### SQIsign Level 5
646
- * **Msg**: `d81c4d8d734fcbfbeade3d3f8a039faa2a2c9957e835ad55b22e75bf57bb556ac8`
647
- * **PK**: `3FA2C18B7D94E6F2A0C85D3E1B7F9A4C2D6E8F0B5A3C7E1D9F2B4A6C8E0D3F5A...`
648
- * **Sig**: `C1D3F5A7B9E2C4D6F8A0B2C4E6F8A0B2D4F6A8C0E2D4F6B8A0C2E4F6D8B0A2C4...`
649
-
650
- *Full byte-perfect vectors are included in the `src/*.test.ts` files.*
651
-
652
- ## Funding
653
-
654
- This project was generously supported by:
655
- - University of Quantum Science
656
- - RustyKey®
657
- - Customers' Yachts® Advisors
658
- - [BuzzyBee®](https://buzzybee.ai)
659
-
660
- <div align="center">
661
- <img src="./logo-rustykey.png" width="60" alt="RustyKey Logo" />
662
- <img src="./logo-buzzybee.ai.png" width="60" alt="BuzzyBee Logo" />
663
- </div>
664
-
665
-
666
-
667
- ## Appendix: testbed features
668
-
669
- Below are some examples of stats and interactivity on the [testbed](https://pqc.rustykey.me), with more planned depending on user interest, to help users understand the trade-offs between lattice-based (ML-KEM/DSA) and isogeny-based (SQISign) crypto:
670
-
671
- - 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.
672
-
673
- - 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.
674
-
675
- - 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.
14
+ **Docs:** [antonymott.github.io/quantum-resistant-rustykey](https://antonymott.github.io/quantum-resistant-rustykey/)
15
+ **Live testbed:** [pqc.rustykey.me](https://pqc.rustykey.me)
16
+ **License:** ISC
676
17
 
677
- - Interactive "Live Insight" Buttons
678
- - "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
679
- - "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.
680
- - "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
18
+ > Pre-production until v1.0.0. Prefer `@latest` (or a caret range) so dependents pick up patches.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quantum-resistant-rustykey",
3
- "version": "0.13.1",
3
+ "version": "0.13.3",
4
4
  "description": "WebAssembly post-quantum-resistant tools for web",
5
5
  "author": "Antony R Mott <antony@rustykey.io> (https://rustykey.io)",
6
6
  "license": "ISC",
@@ -14,7 +14,7 @@
14
14
  "type": "git",
15
15
  "url": "git+https://github.com/antonymott/quantum-resistant-rustykey.git"
16
16
  },
17
- "homepage": "https://github.com/antonymott/quantum-resistant-rustykey.git#readme",
17
+ "homepage": "https://antonymott.github.io/quantum-resistant-rustykey/",
18
18
  "bugs": {
19
19
  "url": "https://github.com"
20
20
  },
@@ -60,22 +60,26 @@
60
60
  "prepack": "pnpm verify:dist",
61
61
  "prepublishOnly": "pnpm verify:dist",
62
62
  "release": "changeset publish",
63
- "example:browser": "pnpm --filter browser-demo dev"
63
+ "example:browser": "pnpm --filter browser-demo dev",
64
+ "docs:dev": "pnpm --dir website start",
65
+ "docs:build": "pnpm --dir website build",
66
+ "docs:serve": "pnpm --dir website serve"
64
67
  },
65
68
  "devDependencies": {
66
- "@biomejs/biome": "2.4.11",
67
- "@changesets/cli": "2.30.0",
68
- "@types/node": "^26.1.0",
69
+ "@biomejs/biome": "2.5.6",
70
+ "@changesets/cli": "2.31.1",
71
+ "@types/node": "^26.1.2",
69
72
  "@webgpu/types": "0.1.71",
70
- "esbuild": "0.28.0",
71
- "tsdown": "0.22.3",
72
- "typescript": "6.0.2",
73
- "vitest": "4.1.4"
73
+ "esbuild": "0.28.1",
74
+ "tsdown": "0.22.14",
75
+ "typescript": "6.0.3",
76
+ "vitest": "4.1.10"
74
77
  },
75
78
  "dependencies": {
76
79
  "@noble/post-quantum": "0.6.1"
77
80
  },
78
81
  "engines": {
79
- "node": ">=26.4.0"
80
- }
82
+ "node": ">=26.5.0"
83
+ },
84
+ "packageManager": "pnpm@11.17.0"
81
85
  }