Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,8 @@ jobs:
- name: Smoke test packed artifact
run: npm run test:pack

# TODO: Migrate to npm OIDC trusted publishing to eliminate NPM_TOKEN secret.
# Configure at npmjs.com: Settings > Publishing access > Trusted publishing.
# Once configured, remove the NODE_AUTH_TOKEN env var below.
# Uses npm OIDC trusted publishing — no long-lived NPM_TOKEN secret needed.
# Configured at npmjs.com: Settings > Publishing access > Trusted publishing.
# The id-token: write permission (line 9) enables the GitHub OIDC provider.
- name: Publish with provenance
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ temp/
# Documentation build
# -----------------------------------------------------------------------------
docs/_build/
docs/superpowers/
site/

# -----------------------------------------------------------------------------
Expand Down
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.0.0] - 2026-03-25

### Added

- 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
- Tests for the runtime integrity check (embedded hash correctness and tamper detection)
- `examples/quantum-safe-wallet.mjs`: end-to-end cryptocurrency wallet simulation demonstrating ML-DSA signature replacement with address-to-key binding validation

### Changed

- Version bumped to 1.0.0 — first stable release with a public API commitment
- Removed unused error codes (`DECAPSULATION_FAILED`, `VERIFICATION_FAILED`, `NOT_IMPLEMENTED`) from the public API to keep the 1.0.0 surface minimal
- Publish workflow migrated from long-lived `NPM_TOKEN` to OIDC trusted publishing for stronger supply chain security
- `picomatch` dev dependency updated to fix high-severity ReDoS vulnerability (CVE in transitive dep; not in published package)

### Fixed

- Socket.dev filesystem-access warning mitigated: the WASM binary is now verified against an embedded SHA-256 hash before `WebAssembly.Module` instantiation

## [0.7.0] - 2026-03-24

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

[Unreleased]: https://github.com/fzheng/fips-crypto/compare/v0.7.0...HEAD
[Unreleased]: https://github.com/fzheng/fips-crypto/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/fzheng/fips-crypto/compare/v0.7.0...v1.0.0
[0.7.0]: https://github.com/fzheng/fips-crypto/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/fzheng/fips-crypto/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/fzheng/fips-crypto/compare/v0.4.0...v0.5.0
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "2"
members = ["rust"]

[workspace.package]
version = "0.7.0"
version = "1.0.0"
edition = "2024"
license = "MIT"
authors = ["Feng Zheng <fzheng@users.noreply.github.com>"]
Expand Down
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,24 @@ High-performance post-quantum cryptography for JavaScript and TypeScript, powere
[![FIPS 205](https://img.shields.io/badge/FIPS%20205-SLH--DSA-blue)](https://csrc.nist.gov/pubs/fips/205/final)
[![provenance](https://img.shields.io/badge/provenance-sigstore-green)](https://www.npmjs.com/package/fips-crypto)

> **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.

## Why post-quantum cryptography matters

Quantum computers running Shor's algorithm will break the classical cryptography that secures today's systems:

- **ECDSA** (Bitcoin, Ethereum, TLS) &mdash; private keys derived from public keys
- **RSA** (HTTPS, email, code signing) &mdash; factored in polynomial time
- **ECDH/X25519** (key exchange) &mdash; same elliptic curve vulnerability

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.

## Why fips-crypto

- **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)
- **Rust + WebAssembly core** &mdash; constant-time-oriented critical paths with Rust-side zeroization of secret material
- **TypeScript-first** &mdash; full type definitions, explicit input validation, clear error codes
- **Tested and benchmarked** &mdash; 940+ tests, 99%+ coverage, cross-implementation compliance vectors
- **Tested and benchmarked** &mdash; 970+ tests, 99%+ coverage, cross-implementation compliance vectors
- **Flexible** &mdash; ESM, CommonJS, auto-init; Node.js CI-tested, browser-compatible via bundlers

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

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.
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:

- [Key Encapsulation](examples/key-encapsulation.mjs) — ML-KEM key exchange
- [Digital Signatures](examples/digital-signatures.mjs) — ML-DSA signing with context binding
- [Hash-Based Signatures](examples/hash-based-signatures.mjs) — SLH-DSA signing
- [Quantum-Safe Wallet](examples/quantum-safe-wallet.mjs) — ML-DSA signature replacement in a crypto-style workflow
- [CommonJS Usage](examples/commonjs-usage.cjs) — `require()` with auto-init

## Performance

Expand Down Expand Up @@ -239,7 +257,7 @@ Every build includes SHA-256 checksums for WASM artifacts. Provenance links each

## Validation and testing

- **940+** tests (746 JavaScript/TypeScript + 225 Rust)
- **970+** tests (748 JavaScript/TypeScript + 225 Rust)
- **99%+** coverage (statements, functions, branches, lines)
- Cross-implementation compliance vectors for all algorithm families
- Packed-artifact smoke tests in CI
Expand Down
17 changes: 9 additions & 8 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

| Version | Supported |
|---------|-----------|
| 0.6.x | Yes |
| 0.5.x | Yes |
| < 0.5 | No |
| 1.0.x | Yes |
| 0.7.x | Yes |
| < 0.7 | No |

## Reporting a Vulnerability

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

### What each verification layer protects against

| Threat | Checksums | Provenance |
|--------|-----------|------------|
| CDN/mirror corruption | Yes | No |
| Stolen npm token | No | Yes |
| Compromised CI environment | No | No |
| Threat | Runtime WASM check | Checksums | Provenance |
|--------|--------------------|-----------|------------|
| WASM binary tampered post-install | Yes | Yes | No |
| CDN/mirror corruption | Yes | Yes | No |
| Stolen npm token | No | No | Yes |
| Compromised CI environment | No | No | No |

For a detailed security model, see [docs/SECURITY-MODEL.md](docs/SECURITY-MODEL.md#checksums-vs-provenance-threat-boundaries).
37 changes: 31 additions & 6 deletions docs/SECURITY-MODEL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
# Security Model

> **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.

This document describes what fips-crypto protects against, how, and what it does not guarantee.

## Quantum Threat Context

Classical public-key cryptography (RSA, ECDSA, ECDH/X25519) is vulnerable to Shor's algorithm on a sufficiently powerful quantum computer. The specific threats:

| Classical algorithm | Used in | Quantum attack | fips-crypto replacement |
|---------------------|---------|----------------|------------------------|
| ECDSA (secp256k1) | Bitcoin/Ethereum transaction signing, TLS | Private key recovery from public key | ML-DSA (FIPS 204), SLH-DSA (FIPS 205) |
| RSA signing | HTTPS, S/MIME, code signing | Factorization in polynomial time | ML-DSA (FIPS 204), SLH-DSA (FIPS 205) |
| RSA encryption | Key transport, S/MIME encryption | Factorization in polynomial time | ML-KEM (FIPS 203) |
| ECDH / X25519 | TLS key exchange, Signal Protocol | Shared secret recovery | ML-KEM (FIPS 203) |
| AES-256, SHA-256 | Symmetric encryption, hashing | Grover's gives ~128-bit equivalent | **Not considered at risk** at current key sizes |

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.

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.

## Threat Model

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

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

### Runtime WASM integrity check

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.

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.

### npm Provenance

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.
Expand All @@ -124,9 +148,10 @@ npm audit signatures

**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.

| Threat | Checksums | Provenance |
|--------|-----------|------------|
| CDN/mirror corruption | Detects | No |
| Stolen npm token | No | Detects |
| Compromised CI environment | No | No |
| Malicious source commit | No | No |
| Threat | Runtime WASM check | Checksums | Provenance |
|--------|--------------------|-----------|------------|
| WASM binary tampered post-install | Detects | Detects | No |
| CDN/mirror corruption | Detects | Detects | No |
| Stolen npm token | No | No | Detects |
| Compromised CI environment | No | No | No |
| Malicious source commit | No | No | No |
172 changes: 172 additions & 0 deletions examples/quantum-safe-wallet.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* Quantum-Safe Signature Replacement Example
*
* Demonstrates how post-quantum signatures (ML-DSA) can replace the ECDSA
* signature primitive used in Bitcoin and other cryptocurrencies.
*
* NOTE: This is a simplified simulation of the signature layer only. A real
* blockchain migration would also require changes to address derivation,
* transaction serialization, fee economics, and consensus rules. This example
* focuses on the cryptographic primitive swap: ECDSA -> ML-DSA.
*
* Run: node examples/quantum-safe-wallet.mjs
*/

import { ml_dsa65 } from 'fips-crypto/auto';
import { createHash } from 'crypto';

// --- Helpers ---

function sha256(data) {
return createHash('sha256').update(data).digest('hex');
}

/** Derive an address from a public key (simplified: truncated SHA-256). */
function deriveAddress(publicKey) {
return sha256(publicKey).slice(0, 40);
}

function createTransaction(from, to, amount, nonce, timestamp) {
const tx = { from, to, amount, nonce, timestamp };
const encoded = new TextEncoder().encode(JSON.stringify(tx));
return { ...tx, hash: sha256(encoded), encoded };
}

/**
* Validate a signed transaction the way a blockchain node would:
* 1. Verify the signature against the signer's public key
* 2. Verify the signer's public key derives the claimed sender address
*/
async function validateTransaction(tx, signature, signerPublicKey) {
// Step 1: Signature must be valid
const sigValid = await ml_dsa65.verify(signerPublicKey, tx.encoded, signature);
if (!sigValid) return { valid: false, reason: 'invalid signature' };

// Step 2: Signer must own the sender address
const derivedAddr = deriveAddress(signerPublicKey);
if (derivedAddr !== tx.from) return { valid: false, reason: 'signer does not match sender address' };

return { valid: true, reason: 'ok' };
}

// =============================================================================
// Step 1: Create two wallets (Alice and Bob)
// =============================================================================

console.log('=== Quantum-Safe Signature Replacement Demo ===\n');

console.log('Creating wallets...');
const alice = await ml_dsa65.keygen();
const bob = await ml_dsa65.keygen();

const aliceAddr = deriveAddress(alice.publicKey);
const bobAddr = deriveAddress(bob.publicKey);

console.log(` Alice: 0x${aliceAddr}`);
console.log(` Public key: ${alice.publicKey.length} bytes (ML-DSA-65)`);
console.log(` Bob: 0x${bobAddr}`);
console.log(` Public key: ${bob.publicKey.length} bytes (ML-DSA-65)`);

// =============================================================================
// Step 2: Alice signs a transaction to send funds to Bob
// =============================================================================

console.log('\n--- Transaction Signing ---\n');

const tx1 = createTransaction(aliceAddr, bobAddr, 2.5, 1, 1711900000000);
console.log(`Transaction: Alice -> Bob, 2.5 coins`);
console.log(` TX hash: ${tx1.hash}`);

const sig1 = await ml_dsa65.sign(alice.secretKey, tx1.encoded);
console.log(` Signature: ${sig1.length} bytes (ML-DSA-65)`);
console.log(` (Bitcoin ECDSA would be ~71 bytes; ML-DSA-65 is ${sig1.length} bytes)`);
console.log(` (Tradeoff: larger signatures, but quantum-safe)`);

// =============================================================================
// Step 3: Validator verifies signature AND sender-address binding
// =============================================================================

console.log('\n--- Validator Verification ---\n');

const result1 = await validateTransaction(tx1, sig1, alice.publicKey);
console.log(` Signature valid: ${result1.valid}`);
console.log(` Address binding: signer key derives 0x${aliceAddr.slice(0, 8)}... = tx.from`);
console.log(' Transaction accepted into mempool');

// =============================================================================
// Step 4: Simulate a block with multiple transactions
// =============================================================================

console.log('\n--- Block Simulation ---\n');

const transactions = [
{ signer: alice, fromAddr: aliceAddr, toAddr: bobAddr, amount: 1.0, nonce: 2 },
{ signer: bob, fromAddr: bobAddr, toAddr: aliceAddr, amount: 0.3, nonce: 1 },
{ signer: alice, fromAddr: aliceAddr, toAddr: bobAddr, amount: 0.7, nonce: 3 },
];

const signedTxs = [];
for (const t of transactions) {
const tx = createTransaction(t.fromAddr, t.toAddr, t.amount, t.nonce, 1711900000000);
const sig = await ml_dsa65.sign(t.signer.secretKey, tx.encoded);
signedTxs.push({ tx, sig, signerPk: t.signer.publicKey });
}

console.log(`Block contains ${signedTxs.length} transactions:`);

let allValid = true;
for (const { tx, sig, signerPk } of signedTxs) {
const { valid, reason } = await validateTransaction(tx, sig, signerPk);
console.log(` ${tx.hash.slice(0, 16)}... ${tx.from.slice(0, 8)}->${tx.to.slice(0, 8)} ${tx.amount} coins [${valid ? 'VALID' : 'REJECTED: ' + reason}]`);
if (!valid) allValid = false;
}
console.log(`\nBlock validation: ${allValid ? 'ALL VALID' : 'REJECTED'}`);

// =============================================================================
// Step 5: Tamper detection — modified transaction is rejected
// =============================================================================

console.log('\n--- Tamper Detection ---\n');

// Sign the original transaction
const originalTx = createTransaction(aliceAddr, bobAddr, 2.5, 1, 1711900000000);
const originalSig = await ml_dsa65.sign(alice.secretKey, originalTx.encoded);

// Attacker rebuilds the transaction with a different amount but same metadata
const tamperedTx = createTransaction(aliceAddr, bobAddr, 2500, 1, 1711900000000);
console.log('Attacker changes amount from 2.5 to 2500 (same nonce, same timestamp)...');

const tamperedResult = await validateTransaction(tamperedTx, originalSig, alice.publicKey);
console.log(` Tampered TX valid: ${tamperedResult.valid} (${tamperedResult.reason})`);

// Original still verifies
const originalResult = await validateTransaction(originalTx, originalSig, alice.publicKey);
console.log(` Original TX valid: ${originalResult.valid}`);

// =============================================================================
// Step 6: Wrong signer is rejected by address binding
// =============================================================================

console.log('\n--- Address Binding Check ---\n');

// Bob tries to sign a transaction claiming to be from Alice's address
const forgedTx = createTransaction(aliceAddr, bobAddr, 100, 99, 1711900000000);
const forgedSig = await ml_dsa65.sign(bob.secretKey, forgedTx.encoded);
const forgedResult = await validateTransaction(forgedTx, forgedSig, bob.publicKey);
console.log('Bob signs a TX claiming to be from Alice...');
console.log(` Valid: ${forgedResult.valid} (${forgedResult.reason})`);

// =============================================================================
// Summary
// =============================================================================

console.log('\n=== Summary ===\n');
console.log('This demo shows ML-DSA-65 (FIPS 204) replacing the ECDSA signature');
console.log('primitive. In a real blockchain migration, additional protocol-level');
console.log('changes (address format, serialization, consensus rules) would also');
console.log('be needed.\n');
console.log('Key points:');
console.log(" - ECDSA is vulnerable to Shor's algorithm on a quantum computer");
console.log(' - ML-DSA resists all known quantum attacks');
console.log(' - Same keygen -> sign -> verify workflow');
console.log(` - Tradeoff: larger keys (${alice.publicKey.length}B vs 33B) and signatures (${sig1.length}B vs 71B)`);
Loading
Loading