Skip to content

Commit 24f9dd1

Browse files
authored
Merge pull request #6 from fzheng/dev
Release 1.0.0
2 parents bc927af + 1edd5e2 commit 24f9dd1

17 files changed

Lines changed: 494 additions & 167 deletions

.github/workflows/publish.yml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,8 @@ jobs:
5353
- name: Smoke test packed artifact
5454
run: npm run test:pack
5555

56-
# TODO: Migrate to npm OIDC trusted publishing to eliminate NPM_TOKEN secret.
57-
# Configure at npmjs.com: Settings > Publishing access > Trusted publishing.
58-
# Once configured, remove the NODE_AUTH_TOKEN env var below.
56+
# Uses npm OIDC trusted publishing — no long-lived NPM_TOKEN secret needed.
57+
# Configured at npmjs.com: Settings > Publishing access > Trusted publishing.
58+
# The id-token: write permission (line 9) enables the GitHub OIDC provider.
5959
- name: Publish with provenance
6060
run: npm publish --provenance --access public
61-
env:
62-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ temp/
157157
# Documentation build
158158
# -----------------------------------------------------------------------------
159159
docs/_build/
160+
docs/superpowers/
160161
site/
161162

162163
# -----------------------------------------------------------------------------

CHANGELOG.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.0.0] - 2026-03-25
11+
12+
### Added
13+
14+
- Runtime WASM integrity check: SHA-256 hash of the WASM binary is embedded in the Node.js loader at build time and verified before instantiation, blocking tampered binaries from executing
15+
- Tests for the runtime integrity check (embedded hash correctness and tamper detection)
16+
- `examples/quantum-safe-wallet.mjs`: end-to-end cryptocurrency wallet simulation demonstrating ML-DSA signature replacement with address-to-key binding validation
17+
18+
### Changed
19+
20+
- Version bumped to 1.0.0 — first stable release with a public API commitment
21+
- Removed unused error codes (`DECAPSULATION_FAILED`, `VERIFICATION_FAILED`, `NOT_IMPLEMENTED`) from the public API to keep the 1.0.0 surface minimal
22+
- Publish workflow migrated from long-lived `NPM_TOKEN` to OIDC trusted publishing for stronger supply chain security
23+
- `picomatch` dev dependency updated to fix high-severity ReDoS vulnerability (CVE in transitive dep; not in published package)
24+
25+
### Fixed
26+
27+
- Socket.dev filesystem-access warning mitigated: the WASM binary is now verified against an embedded SHA-256 hash before `WebAssembly.Module` instantiation
28+
1029
## [0.7.0] - 2026-03-24
1130

1231
### Added
@@ -196,7 +215,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
196215
- TypeScript/JavaScript wrappers with ESM and CJS support
197216
- SLH-DSA parameter set definitions (stubs)
198217

199-
[Unreleased]: https://github.com/fzheng/fips-crypto/compare/v0.7.0...HEAD
218+
[Unreleased]: https://github.com/fzheng/fips-crypto/compare/v1.0.0...HEAD
219+
[1.0.0]: https://github.com/fzheng/fips-crypto/compare/v0.7.0...v1.0.0
200220
[0.7.0]: https://github.com/fzheng/fips-crypto/compare/v0.6.0...v0.7.0
201221
[0.6.0]: https://github.com/fzheng/fips-crypto/compare/v0.5.0...v0.6.0
202222
[0.5.0]: https://github.com/fzheng/fips-crypto/compare/v0.4.0...v0.5.0

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ resolver = "2"
33
members = ["rust"]
44

55
[workspace.package]
6-
version = "0.7.0"
6+
version = "1.0.0"
77
edition = "2024"
88
license = "MIT"
99
authors = ["Feng Zheng <fzheng@users.noreply.github.com>"]

README.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,24 @@ High-performance post-quantum cryptography for JavaScript and TypeScript, powere
1212
[![FIPS 205](https://img.shields.io/badge/FIPS%20205-SLH--DSA-blue)](https://csrc.nist.gov/pubs/fips/205/final)
1313
[![provenance](https://img.shields.io/badge/provenance-sigstore-green)](https://www.npmjs.com/package/fips-crypto)
1414

15+
> **Note:** This package implements the algorithm specifications in FIPS 203, FIPS 204, and FIPS 205. It is **not** a FIPS 140-2 or FIPS 140-3 validated cryptographic module. If your compliance framework requires CMVP-validated modules, this library does not satisfy that requirement.
16+
17+
## Why post-quantum cryptography matters
18+
19+
Quantum computers running Shor's algorithm will break the classical cryptography that secures today's systems:
20+
21+
- **ECDSA** (Bitcoin, Ethereum, TLS) &mdash; private keys derived from public keys
22+
- **RSA** (HTTPS, email, code signing) &mdash; factored in polynomial time
23+
- **ECDH/X25519** (key exchange) &mdash; same elliptic curve vulnerability
24+
25+
NIST finalized three post-quantum standards in 2024 (FIPS 203, 204, 205) to replace these vulnerable primitives. fips-crypto brings all three to JavaScript. See the [quantum-safe wallet example](examples/quantum-safe-wallet.mjs) for a demo of replacing the ECDSA signature primitive in a cryptocurrency-style workflow.
26+
1527
## Why fips-crypto
1628

1729
- **Standards-focused** &mdash; implements NIST [FIPS 203](https://csrc.nist.gov/pubs/fips/203/final) (ML-KEM), [FIPS 204](https://csrc.nist.gov/pubs/fips/204/final) (ML-DSA), and [FIPS 205](https://csrc.nist.gov/pubs/fips/205/final) (SLH-DSA)
1830
- **Rust + WebAssembly core** &mdash; constant-time-oriented critical paths with Rust-side zeroization of secret material
1931
- **TypeScript-first** &mdash; full type definitions, explicit input validation, clear error codes
20-
- **Tested and benchmarked** &mdash; 940+ tests, 99%+ coverage, cross-implementation compliance vectors
32+
- **Tested and benchmarked** &mdash; 970+ tests, 99%+ coverage, cross-implementation compliance vectors
2133
- **Flexible** &mdash; ESM, CommonJS, auto-init; Node.js CI-tested, browser-compatible via bundlers
2234

2335
## Try it now
@@ -46,7 +58,13 @@ const slhSig = await slh_dsa_shake_192f.sign(slhKeys.secretKey, message);
4658
const slhValid = await slh_dsa_shake_192f.verify(slhKeys.publicKey, message, slhSig);
4759
```
4860

49-
See the [fips-crypto-demo](https://github.com/fzheng/fips-crypto-demo) for an interactive app, or browse the [examples/](examples/) folder for ready-to-run scripts.
61+
See the [fips-crypto-demo](https://github.com/fzheng/fips-crypto-demo) for an interactive app, or browse the [examples/](examples/) folder for ready-to-run scripts:
62+
63+
- [Key Encapsulation](examples/key-encapsulation.mjs) — ML-KEM key exchange
64+
- [Digital Signatures](examples/digital-signatures.mjs) — ML-DSA signing with context binding
65+
- [Hash-Based Signatures](examples/hash-based-signatures.mjs) — SLH-DSA signing
66+
- [Quantum-Safe Wallet](examples/quantum-safe-wallet.mjs) — ML-DSA signature replacement in a crypto-style workflow
67+
- [CommonJS Usage](examples/commonjs-usage.cjs)`require()` with auto-init
5068

5169
## Performance
5270

@@ -239,7 +257,7 @@ Every build includes SHA-256 checksums for WASM artifacts. Provenance links each
239257
240258
## Validation and testing
241259
242-
- **940+** tests (746 JavaScript/TypeScript + 225 Rust)
260+
- **970+** tests (748 JavaScript/TypeScript + 225 Rust)
243261
- **99%+** coverage (statements, functions, branches, lines)
244262
- Cross-implementation compliance vectors for all algorithm families
245263
- Packed-artifact smoke tests in CI

SECURITY.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
| Version | Supported |
66
|---------|-----------|
7-
| 0.6.x | Yes |
8-
| 0.5.x | Yes |
9-
| < 0.5 | No |
7+
| 1.0.x | Yes |
8+
| 0.7.x | Yes |
9+
| < 0.7 | No |
1010

1111
## Reporting a Vulnerability
1212

@@ -78,10 +78,11 @@ A successful result confirms the package was built and published by the GitHub A
7878

7979
### What each verification layer protects against
8080

81-
| Threat | Checksums | Provenance |
82-
|--------|-----------|------------|
83-
| CDN/mirror corruption | Yes | No |
84-
| Stolen npm token | No | Yes |
85-
| Compromised CI environment | No | No |
81+
| Threat | Runtime WASM check | Checksums | Provenance |
82+
|--------|--------------------|-----------|------------|
83+
| WASM binary tampered post-install | Yes | Yes | No |
84+
| CDN/mirror corruption | Yes | Yes | No |
85+
| Stolen npm token | No | No | Yes |
86+
| Compromised CI environment | No | No | No |
8687

8788
For a detailed security model, see [docs/SECURITY-MODEL.md](docs/SECURITY-MODEL.md#checksums-vs-provenance-threat-boundaries).

docs/SECURITY-MODEL.md

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
11
# Security Model
22

3+
> **Compliance scope:** fips-crypto implements the cryptographic algorithms specified in FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), and FIPS 205 (SLH-DSA). It has not undergone FIPS 140-2 or FIPS 140-3 CMVP validation. The "FIPS" in the package name refers to the algorithm standards implemented, not to module-level validation status.
4+
35
This document describes what fips-crypto protects against, how, and what it does not guarantee.
46

7+
## Quantum Threat Context
8+
9+
Classical public-key cryptography (RSA, ECDSA, ECDH/X25519) is vulnerable to Shor's algorithm on a sufficiently powerful quantum computer. The specific threats:
10+
11+
| Classical algorithm | Used in | Quantum attack | fips-crypto replacement |
12+
|---------------------|---------|----------------|------------------------|
13+
| ECDSA (secp256k1) | Bitcoin/Ethereum transaction signing, TLS | Private key recovery from public key | ML-DSA (FIPS 204), SLH-DSA (FIPS 205) |
14+
| RSA signing | HTTPS, S/MIME, code signing | Factorization in polynomial time | ML-DSA (FIPS 204), SLH-DSA (FIPS 205) |
15+
| RSA encryption | Key transport, S/MIME encryption | Factorization in polynomial time | ML-KEM (FIPS 203) |
16+
| ECDH / X25519 | TLS key exchange, Signal Protocol | Shared secret recovery | ML-KEM (FIPS 203) |
17+
| AES-256, SHA-256 | Symmetric encryption, hashing | Grover's gives ~128-bit equivalent | **Not considered at risk** at current key sizes |
18+
19+
Note: hash functions and symmetric ciphers are not known to be broken by quantum algorithms at standard key sizes, though future cryptanalytic advances cannot be ruled out. fips-crypto replaces the asymmetric primitives that are at risk, not hash functions.
20+
21+
See the [quantum-safe wallet example](../examples/quantum-safe-wallet.mjs) for a demonstration of replacing the ECDSA signature primitive in a cryptocurrency-style workflow. Note that a real blockchain migration would also require protocol-level changes (address derivation, serialization, consensus rules) beyond the cryptographic primitive swap.
22+
523
## Threat Model
624

725
fips-crypto is designed to protect against:
@@ -106,6 +124,12 @@ npm run verify:integrity
106124

107125
This compares the actual file hashes against the stored checksums. Any mismatch indicates tampering or corruption.
108126

127+
### Runtime WASM integrity check
128+
129+
The Node.js build (`pkg-node/`) includes a runtime integrity guard. At build time, the SHA-256 hash of the WASM binary is computed and embedded directly in the JS loader file. At module load time, before `new WebAssembly.Module()` is called, the loader recomputes the hash of the file it just read from disk and compares it against the embedded constant. If they differ, the module throws immediately instead of executing unknown code.
130+
131+
This protects against post-install tampering of the WASM binary (e.g., a compromised CDN or filesystem modification) without depending on a separate checksums file that could also be replaced.
132+
109133
### npm Provenance
110134

111135
Releases published via GitHub Actions use npm's `--provenance` flag, which creates a [Sigstore](https://www.sigstore.dev/) attestation linking the published package to the specific GitHub Actions workflow run, commit SHA, and repository. This is visible as a "Provenance" badge on the npm package page.
@@ -124,9 +148,10 @@ npm audit signatures
124148

125149
**Defense in depth**: Use both. Checksums catch accidental corruption and CDN issues. Provenance catches deliberate supply chain attacks on the publish step. Neither protects against a compromised source repository (e.g., a malicious commit merged to `main`). For that, rely on code review and branch protection rules.
126150

127-
| Threat | Checksums | Provenance |
128-
|--------|-----------|------------|
129-
| CDN/mirror corruption | Detects | No |
130-
| Stolen npm token | No | Detects |
131-
| Compromised CI environment | No | No |
132-
| Malicious source commit | No | No |
151+
| Threat | Runtime WASM check | Checksums | Provenance |
152+
|--------|--------------------|-----------|------------|
153+
| WASM binary tampered post-install | Detects | Detects | No |
154+
| CDN/mirror corruption | Detects | Detects | No |
155+
| Stolen npm token | No | No | Detects |
156+
| Compromised CI environment | No | No | No |
157+
| Malicious source commit | No | No | No |

examples/quantum-safe-wallet.mjs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
* Quantum-Safe Signature Replacement Example
3+
*
4+
* Demonstrates how post-quantum signatures (ML-DSA) can replace the ECDSA
5+
* signature primitive used in Bitcoin and other cryptocurrencies.
6+
*
7+
* NOTE: This is a simplified simulation of the signature layer only. A real
8+
* blockchain migration would also require changes to address derivation,
9+
* transaction serialization, fee economics, and consensus rules. This example
10+
* focuses on the cryptographic primitive swap: ECDSA -> ML-DSA.
11+
*
12+
* Run: node examples/quantum-safe-wallet.mjs
13+
*/
14+
15+
import { ml_dsa65 } from 'fips-crypto/auto';
16+
import { createHash } from 'crypto';
17+
18+
// --- Helpers ---
19+
20+
function sha256(data) {
21+
return createHash('sha256').update(data).digest('hex');
22+
}
23+
24+
/** Derive an address from a public key (simplified: truncated SHA-256). */
25+
function deriveAddress(publicKey) {
26+
return sha256(publicKey).slice(0, 40);
27+
}
28+
29+
function createTransaction(from, to, amount, nonce, timestamp) {
30+
const tx = { from, to, amount, nonce, timestamp };
31+
const encoded = new TextEncoder().encode(JSON.stringify(tx));
32+
return { ...tx, hash: sha256(encoded), encoded };
33+
}
34+
35+
/**
36+
* Validate a signed transaction the way a blockchain node would:
37+
* 1. Verify the signature against the signer's public key
38+
* 2. Verify the signer's public key derives the claimed sender address
39+
*/
40+
async function validateTransaction(tx, signature, signerPublicKey) {
41+
// Step 1: Signature must be valid
42+
const sigValid = await ml_dsa65.verify(signerPublicKey, tx.encoded, signature);
43+
if (!sigValid) return { valid: false, reason: 'invalid signature' };
44+
45+
// Step 2: Signer must own the sender address
46+
const derivedAddr = deriveAddress(signerPublicKey);
47+
if (derivedAddr !== tx.from) return { valid: false, reason: 'signer does not match sender address' };
48+
49+
return { valid: true, reason: 'ok' };
50+
}
51+
52+
// =============================================================================
53+
// Step 1: Create two wallets (Alice and Bob)
54+
// =============================================================================
55+
56+
console.log('=== Quantum-Safe Signature Replacement Demo ===\n');
57+
58+
console.log('Creating wallets...');
59+
const alice = await ml_dsa65.keygen();
60+
const bob = await ml_dsa65.keygen();
61+
62+
const aliceAddr = deriveAddress(alice.publicKey);
63+
const bobAddr = deriveAddress(bob.publicKey);
64+
65+
console.log(` Alice: 0x${aliceAddr}`);
66+
console.log(` Public key: ${alice.publicKey.length} bytes (ML-DSA-65)`);
67+
console.log(` Bob: 0x${bobAddr}`);
68+
console.log(` Public key: ${bob.publicKey.length} bytes (ML-DSA-65)`);
69+
70+
// =============================================================================
71+
// Step 2: Alice signs a transaction to send funds to Bob
72+
// =============================================================================
73+
74+
console.log('\n--- Transaction Signing ---\n');
75+
76+
const tx1 = createTransaction(aliceAddr, bobAddr, 2.5, 1, 1711900000000);
77+
console.log(`Transaction: Alice -> Bob, 2.5 coins`);
78+
console.log(` TX hash: ${tx1.hash}`);
79+
80+
const sig1 = await ml_dsa65.sign(alice.secretKey, tx1.encoded);
81+
console.log(` Signature: ${sig1.length} bytes (ML-DSA-65)`);
82+
console.log(` (Bitcoin ECDSA would be ~71 bytes; ML-DSA-65 is ${sig1.length} bytes)`);
83+
console.log(` (Tradeoff: larger signatures, but quantum-safe)`);
84+
85+
// =============================================================================
86+
// Step 3: Validator verifies signature AND sender-address binding
87+
// =============================================================================
88+
89+
console.log('\n--- Validator Verification ---\n');
90+
91+
const result1 = await validateTransaction(tx1, sig1, alice.publicKey);
92+
console.log(` Signature valid: ${result1.valid}`);
93+
console.log(` Address binding: signer key derives 0x${aliceAddr.slice(0, 8)}... = tx.from`);
94+
console.log(' Transaction accepted into mempool');
95+
96+
// =============================================================================
97+
// Step 4: Simulate a block with multiple transactions
98+
// =============================================================================
99+
100+
console.log('\n--- Block Simulation ---\n');
101+
102+
const transactions = [
103+
{ signer: alice, fromAddr: aliceAddr, toAddr: bobAddr, amount: 1.0, nonce: 2 },
104+
{ signer: bob, fromAddr: bobAddr, toAddr: aliceAddr, amount: 0.3, nonce: 1 },
105+
{ signer: alice, fromAddr: aliceAddr, toAddr: bobAddr, amount: 0.7, nonce: 3 },
106+
];
107+
108+
const signedTxs = [];
109+
for (const t of transactions) {
110+
const tx = createTransaction(t.fromAddr, t.toAddr, t.amount, t.nonce, 1711900000000);
111+
const sig = await ml_dsa65.sign(t.signer.secretKey, tx.encoded);
112+
signedTxs.push({ tx, sig, signerPk: t.signer.publicKey });
113+
}
114+
115+
console.log(`Block contains ${signedTxs.length} transactions:`);
116+
117+
let allValid = true;
118+
for (const { tx, sig, signerPk } of signedTxs) {
119+
const { valid, reason } = await validateTransaction(tx, sig, signerPk);
120+
console.log(` ${tx.hash.slice(0, 16)}... ${tx.from.slice(0, 8)}->${tx.to.slice(0, 8)} ${tx.amount} coins [${valid ? 'VALID' : 'REJECTED: ' + reason}]`);
121+
if (!valid) allValid = false;
122+
}
123+
console.log(`\nBlock validation: ${allValid ? 'ALL VALID' : 'REJECTED'}`);
124+
125+
// =============================================================================
126+
// Step 5: Tamper detection — modified transaction is rejected
127+
// =============================================================================
128+
129+
console.log('\n--- Tamper Detection ---\n');
130+
131+
// Sign the original transaction
132+
const originalTx = createTransaction(aliceAddr, bobAddr, 2.5, 1, 1711900000000);
133+
const originalSig = await ml_dsa65.sign(alice.secretKey, originalTx.encoded);
134+
135+
// Attacker rebuilds the transaction with a different amount but same metadata
136+
const tamperedTx = createTransaction(aliceAddr, bobAddr, 2500, 1, 1711900000000);
137+
console.log('Attacker changes amount from 2.5 to 2500 (same nonce, same timestamp)...');
138+
139+
const tamperedResult = await validateTransaction(tamperedTx, originalSig, alice.publicKey);
140+
console.log(` Tampered TX valid: ${tamperedResult.valid} (${tamperedResult.reason})`);
141+
142+
// Original still verifies
143+
const originalResult = await validateTransaction(originalTx, originalSig, alice.publicKey);
144+
console.log(` Original TX valid: ${originalResult.valid}`);
145+
146+
// =============================================================================
147+
// Step 6: Wrong signer is rejected by address binding
148+
// =============================================================================
149+
150+
console.log('\n--- Address Binding Check ---\n');
151+
152+
// Bob tries to sign a transaction claiming to be from Alice's address
153+
const forgedTx = createTransaction(aliceAddr, bobAddr, 100, 99, 1711900000000);
154+
const forgedSig = await ml_dsa65.sign(bob.secretKey, forgedTx.encoded);
155+
const forgedResult = await validateTransaction(forgedTx, forgedSig, bob.publicKey);
156+
console.log('Bob signs a TX claiming to be from Alice...');
157+
console.log(` Valid: ${forgedResult.valid} (${forgedResult.reason})`);
158+
159+
// =============================================================================
160+
// Summary
161+
// =============================================================================
162+
163+
console.log('\n=== Summary ===\n');
164+
console.log('This demo shows ML-DSA-65 (FIPS 204) replacing the ECDSA signature');
165+
console.log('primitive. In a real blockchain migration, additional protocol-level');
166+
console.log('changes (address format, serialization, consensus rules) would also');
167+
console.log('be needed.\n');
168+
console.log('Key points:');
169+
console.log(" - ECDSA is vulnerable to Shor's algorithm on a quantum computer");
170+
console.log(' - ML-DSA resists all known quantum attacks');
171+
console.log(' - Same keygen -> sign -> verify workflow');
172+
console.log(` - Tradeoff: larger keys (${alice.publicKey.length}B vs 33B) and signatures (${sig1.length}B vs 71B)`);

0 commit comments

Comments
 (0)