A Case for WebAssembly (WASM) and Rust in Zero-Knowledge Authentication #
Introduction #
While working on a project that implemented zero-knowledge proof authentication, I encountered an interesting challenge.
A zero-knowledge password proof (ZKPP) allows someone to prove they know a password without revealing it.
In this case, during signup, based on a password, the client generates an RSA key pair and sends the public key to the server. The private key and password are never sent—they remain on the client.
During login, the client regenerates the RSA key pair using the password. The server then sends a random challenge (binary array), which the client signs with the RSA private key. The resulting signature is sent back to the server, which verifies it using the stored public key.
This approach requires deterministic RSA key generation based on a seed derived from the password.
The Challenge #
The browser’s SubtleCrypto
API was the first option considered for key generation. Here’s an example of generating an RSA key pair using this API:
const keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]), // Equivalent to 65537
hash: { name: "SHA-256" } // Hashing algorithm
},
true, // Whether the key is extractable (can be exported)
["sign", "verify"] // Key usages
);
// Export the public key
const publicKey = await window.crypto.subtle.exportKey("spki", keyPair.publicKey);
console.log("Public Key:", bufferToBase64(publicKey));
// Export the private key
const privateKey = await window.crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
console.log("Private Key:", bufferToBase64(privateKey));
For more details, see [https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey](MDN’s SubtleCrypto) documentation.
However, this API lacks support for seeded key generation, making it unsuitable for deterministic RSA key pairs.
Enter WASM and Rust #
To address this limitation, we turned to Rust, which supports deterministic RSA key generation via libraries like rsa and rand. Combining Rust with WebAssembly (WASM) allowed us to expose this functionality to the browser.
Example: RSA Key Generation with Rust #
Using Rust, we can generate a deterministic RSA key pair as follows:
use rand::{SeedableRng, rngs::StdRng};
use rsa::{RsaPrivateKey, RsaPublicKey};
fn generate_keypair(seed: [u8; 32], bit_size: usize) -> RsaPrivateKey {
let rng = StdRng::from_seed(seed);
RsaPrivateKey::new(&mut rng.clone(), bit_size).expect("Failed to generate key pair")
}
WASM Integration #
We exposed the Rust functionality to JavaScript using WASM:
use rand::{SeedableRng, rngs::StdRng};
use rsa::{RsaPrivateKey, RsaPublicKey};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn generate_keypair(bit_size: usize, seed: &[u8]) -> Vec<u8> {
if seed.len() != 32 {
panic!("Seed length must be 32 bytes");
}
let mut fixed_seed = [0u8; 32];
fixed_seed.copy_from_slice(seed);
let rng = StdRng::from_seed(fixed_seed);
let private_key = RsaPrivateKey::new(&mut rng.clone(), bit_size).expect("Failed to generate key pair");
private_key.to_pkcs8().expect("Failed to encode key").to_vec()
}
By compiling this code into WASM, the deterministic key generation function can now be called from JavaScript.
Conclusion #
Modern browser APIs, such as SubtleCrypto
, are powerful but insufficient for implementing deterministic RSA key pairs required by zero-knowledge password proofs (ZKPP). By leveraging Rust and WebAssembly, we successfully introduced deterministic key generation to the frontend.
This combination showcases the flexibility of WASM and Rust in enabling advanced cryptographic operations directly within the browser, providing performance and security benefits for web applications.
References #
https://en.wikipedia.org/wiki/Zero-knowledge_password_proof
https://www.wired.com/story/zero-knowledge-proofs/
https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey