Faster signatures for WebAssembly

I just released ed25519-wasm, a small Rust crate for Ed25519 signatures in WebAssembly. The static library can also be directly linked in applications written in other languages.

It’s backed by lib25519, compiled to WebAssembly using Zig, linked to Rust bindings, and exposes a simple API similar to ed25519-compact:

use ed25519_wasm::{KeyPair, Seed};

let key_pair = KeyPair::from_seed(Seed::generate());
let message = b"hello";
let signature = key_pair.sk.sign(message, None);

key_pair.pk.verify(message, &signature).unwrap();

The benchmark

The benchmark in the ed25519-wasm README was run with Wasmtime 45.0.2, using 128-byte messages.

The table reports the median time per operation and the relative speed compared to ed25519-wasm:

Algorithm / implementation Sign 128B Verify 128B Sign vs ed25519-wasm Verify vs ed25519-wasm
Ed25519 / ed25519-wasm 15.1 us 49.1 us 1.0x 1.0x
Ed25519 / libsodium wasm 21.4 us 53.1 us 1.4x slower 1.1x slower
ECDSA-P256/SHA-256 / RustCrypto p256 245 us 277 us 16.2x slower 5.6x slower
ML-DSA-44 / Zig std wasm 135.5 us 23.3 us 8.9x slower 2.1x faster
ML-DSA-44 / RustCrypto ml-dsa 706 us 91.7 us 46.6x slower 1.9x slower
RSA-2048 / RustCrypto rsa 3.21 ms 171 us 212x slower 3.5x slower
RSA-3072 / RustCrypto rsa 10.2 ms 394 us 671x slower 8.0x slower

Ed25519 beats everything hands down.

If you already use libsodium, including from Rust through something like libsodium-rs, that’s a perfectly reasonable option.

But ed25519-wasm is slightly faster: 15.1 microseconds to sign and 49.1 microseconds to verify a 128-byte message on Apple M4.

P-256 isn’t close.

ECDSA-P256/SHA-256 with RustCrypto’s p256 takes 245 microseconds to sign and 277 microseconds to verify. That’s 16.2 times slower for signing and 5.6 times slower for verification.

RSA is much worse.

RSA-2048 signing takes 3.21 milliseconds. RSA-3072 signing takes 10.2 milliseconds.

Compared to ed25519-wasm, that’s 212 times slower and 671 times slower.

Verification is less catastrophic, but still bad: 171 microseconds for RSA-2048 and 394 microseconds for RSA-3072.

RS256 is a huge waste of CPU cycles and money

Even when they are not forced to, people use JWT. Especially RS256.

In JWT’s jargon, RS256 means RSA with SHA-256. It’s common because old systems support it, cloud identity products support it, tutorials use it, and people copy what already works.

But if you’re verifying or signing JWT tokens in WebAssembly, avoid RS256.

For signing, it isn’t even close. RSA-2048 is already 212 times slower than ed25519-wasm. RSA-3072 is 671 times slower.

Even verification, the operation JWT consumers usually care about, is 3.5 times slower with RSA-2048 and 8 times slower with RSA-3072.

If this runs in a browser once, maybe you don’t care.

If this runs at the edge for every request, or in a WebAssembly function where CPU time is literally what you pay for, you should care a lot. It’s a terrible choice.

What about post-quantum signatures?

ML-DSA is interesting.

It’s post-quantum resistant, and in WebAssembly, its performance is close to native since it’s mostly based on scalar arithmetic.

The Zig standard library implementation of ML-DSA-44 compiled to WebAssembly signs in 135.5 microseconds and verifies in 23.3 microseconds in this benchmark.

This is actually faster than Ed25519!

But don’t generalize that to every ML-DSA implementation.

RustCrypto’s ml-dsa implementation is much slower, and signs in 706 microseconds and verifies in 91.7 microseconds. That makes signing 46.6 times slower than ed25519-wasm, and even verification is 1.9 times slower.

But with a fast implementation, ML-DSA is a wise choice for WebAssembly if you can afford the large keys and signatures.

Use Ed25519 or ML-DSA, forget RSA

For most WebAssembly applications that need signatures, Ed25519 should be your go-to algorithm.

It’s got small keys, small signatures, simple APIs, fast signing, fast verification, and mature implementations.

If you already ship libsodium to WebAssembly, keep using it. The WebAssembly build performs well, and libsodium remains a good default crypto toolbox.

But if the only thing you need is Ed25519 signatures from Rust targeting WebAssembly, ed25519-wasm is now the best option I know of.

It’s small. It’s direct. It’s backed by lib25519.

If you pay to run WebAssembly code, use this.