// WebCrypto imports binary keys: strip the PEM armour and decode the base64 bodyconst pemToBytes = (pem) => Uint8Array.from(atob(pem.replace(/-----[^-]+-----|\s+/g, "")), (c) => c.charCodeAt(0));const toHex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");async function signEd25519(privateKeyPem, message) { const key = await crypto.subtle.importKey( "pkcs8", pemToBytes(privateKeyPem), { name: "Ed25519" }, false, ["sign"], ); // no hash parameter: Ed25519 hashes internally return new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key, new TextEncoder().encode(message)));} async function verifyEd25519(publicKeyPem, message, signature) { const key = await crypto.subtle.importKey( "spki", pemToBytes(publicKeyPem), { name: "Ed25519" }, false, ["verify"], ); return crypto.subtle.verify({ name: "Ed25519" }, key, signature, new TextEncoder().encode(message));}// a bare 32-byte public key (JWK "x", libsodium, Solana) imports as "raw"const rawKey = await crypto.subtle.importKey("raw", rawPublicKey, { name: "Ed25519" }, true, ["verify"]);console.log("raw key valid:", await crypto.subtle.verify({ name: "Ed25519" }, rawKey, signature, new TextEncoder().encode("hello ed25519")));How it works
importKey("pkcs8", …)takes the DER bytes under aBEGIN PRIVATE KEYblock, so the helper strips the armour and decodes the base64 body;"spki"does the same forBEGIN PUBLIC KEY.- The algorithm object is
{ name: "Ed25519" }inimportKey,signandverifyalike. There is nohashmember: SHA-512 is part of Ed25519. signresolves to anArrayBufferof 64 bytes;verifyresolves totrueorfalse.- A public key that arrives as 32 raw bytes (a JWK
x, libsodium, Solana) imports directly with the"raw"format.
Gotchas
- Private keys cannot be imported as
"raw": a 32-byte seed with format"raw"and usage"sign"is rejected (SyntaxError: Unsupported key usage for Ed25519 keyin Node.js). Use PKCS#8, or a JWK{ kty: "OKP", crv: "Ed25519", d, x }. - Older browsers reject the algorithm name with
NotSupportedError(Chrome before 137, Firefox before 129). Feature-detect with atryaroundimportKeyand fall back to a library such as @noble/curves. - Sign bytes, not strings:
new TextEncoder().encode(text)gives the UTF-8 bytes other languages read from a file. A hex or base64 string signed as text is a different message. - The keys here are imported as non-extractable (
false). Passtrueif you needexportKeylater, for example to publish the public key as a JWK.