WASM Ed25519 (RFC 8032) — Tier-2 fallback for Ed25519 when crypto.subtle lacks it.

Module wasmEd25519 | Source packages/front/fw/src/crypto/wasm/ed25519.js | Deps wasmRuntime | Worker-safe yes

WASM-backed Ed25519 digital signatures (RFC 8032): keygen, sign, and verify. The binary is the libsodium ref10 closure framed by the @awacloud/fw-wasm-crypto package's arena allocator, SHA-512 seam, and staged-entropy RNG seam. Signatures are deterministic (RFC 8032 §5.1 — no nonce randomness).

This is the Tier-2 fallback for environments where crypto.subtle is unavailable (non-secure-context, locked-down workers). In secure contexts prefer ../webcrypto/ed25519.md, which is hardware-accelerated. The pure-JS ../pkc/ed25519.md remains the universal default.

The binary ships scalar-only (ed25519.scalar.wasm; simd: false in targets.json; ref10 has no simd128 lane). The { variant: 'scalar' } option is passed explicitly to wasmRuntime.load because the runtime's selectVariant() defaults to simd and has no automatic fallback.

Resolve

const wasmEd25519 = runtime.resolve('wasmEd25519');
// Returns: { isAvailable, keygen, sign, verify }

API

Method Signature Returns
isAvailable () => boolean true when WebAssembly is present
keygen (seed?: Uint8Array) => Promise<{publicKey: Uint8Array, privateKey: Uint8Array}|false> Fresh Ed25519 key pair
sign (privateKey: Uint8Array, message: Uint8Array) => Promise<Uint8Array|false> 64-byte detached signature
verify (publicKey: Uint8Array, signature: Uint8Array, message: Uint8Array) => Promise<boolean> true / false (no-throw)

Encodings

Item Encoding Length
seed raw 32-byte secret (optional; drawn from crypto.getRandomValues if omitted) 32 bytes
publicKey compressed Ed25519 point (RFC 8032 encoding) 32 bytes
privateKey expanded sk = seed ‖ publicKey — identical to pkc/ed25519 format 64 bytes
signature detached Ed25519 signature R ‖ s 64 bytes

The privateKey encoding is the libsodium "expanded sk" (seed concatenated with the compressed public key), which matches the pure-JS pkc/ed25519 module exactly, enabling full cross-tier signature interoperability.

All methods resolve false (or verify → false) when:

  • a key / seed / signature has the wrong byte length, or an argument is not a Uint8Array
  • WebAssembly is unavailable, or the binary fails to load (fetch error, ABI mismatch)
  • the WASM entry returns a non-zero status (verify also returns false on a non-matching signature)

None of them ever reject.

Examples

const wasmEd25519 = runtime.resolve('wasmEd25519');

if (!wasmEd25519.isAvailable()) {
    // Fall back to webcrypto/ed25519 (secure contexts) or pure-JS pkc/ed25519.
}

// Generate a key pair (random seed drawn internally).
const { publicKey, privateKey } = await wasmEd25519.keygen();

// Sign and verify.
const enc = new TextEncoder();
const msg = enc.encode('hello');

const sig = await wasmEd25519.sign(privateKey, msg); // 64-byte R||s
const ok = await wasmEd25519.verify(publicKey, sig, msg);
// ok === true

// Deterministic keygen from a fixed seed (RFC 8032 §7.1 TEST 1).
function hexToBytes(h) {
    const o = new Uint8Array(h.length / 2);
    for (let i = 0; i < o.length; i++) o[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
    return o;
}
const seed = hexToBytes('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60');
const kp = await wasmEd25519.keygen(seed);
// kp.publicKey === d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a

Worker Usage

const worker = fw.createWorker(
    function ({ libs }) {
        // wasmRuntime fetches the colocated .wasm by name inside the worker —
        // no main-thread closure is serialized.
        libs.wasmEd25519.keygen().then((kp) => {
            self.postMessage(kp !== false);
        });
    },
    { dependencies: ['wasmEd25519'] }
);

Notes

  • Prefer WebCrypto in secure contexts: webcrypto/ed25519 is hardware-accelerated where crypto.subtle supports Ed25519 (Chrome 113+, Firefox 130+, Safari 17+). Use wasmEd25519 only when crypto.subtle is unavailable or Ed25519 is not yet in the browser's subtle.
  • RFC 8032 compliance: signatures are standard Ed25519 (RFC 8032 §5.1 / FIPS 186-5 §7.6) — interoperable with OpenSSL, libsodium, and all compliant implementations.
  • Cross-tier interoperability: the 64-byte privateKey format (seed ‖ publicKey) is identical between wasmEd25519 and pkc/ed25519. A key pair generated in one tier can be used directly in the other for both signing and verification.
  • Scalar-only: ed25519.simd.wasm is not shipped (ref10 has no simd128 lane). The { variant: 'scalar' } pin is mandatory; a default load would fail on the missing SIMD binary.
  • No-throw contract: all methods resolve to a value or false; they never reject. Input validation (lengths, instanceof Uint8Array) happens before any WASM call.
  • Output is always a fresh copy: readBytes copies out of WASM linear memory into a new Uint8Array. The caller owns the returned buffer.
  • RNG seam not used for keygen: the binary exports rng_stage/rng_reset (the ABI triple), but ed25519_keypair is SEED-EXPLICIT (crypto_sign_ed25519_seed_keypair) — it does not use the rng seam. The seed is always supplied by the JS layer (from the caller or from crypto.getRandomValues).

See also