denoland/celldPublic

self-hosted, distributed Durable Objects

crypto.subtle.verify does not support Ed25519 (Secure Curves); X25519 unsupported

AK

`crypto.subtle.verify` rejects Ed25519 with `NotSupportedError: unsupported verify algorithm: ED25519`, and `X25519` is unsupported for `generateKey`/`deriveBits`. Both are part of the [Secure Curves](https://wicg.github.io/webcrypto-secure-curves/) set that Cloudflare Workers has supported since [April 2023](https://community.cloudflare.com/t/2023-4-28-workers-runtime-release-notes/502941), so a worker that verifies EdDSA JWTs runs on Workers but not on celld.

This looks like the remaining slice of #124, which was closed once RS256 and ES256 verified. I confirmed RS256/ES256/PS256/HS256 all verify correctly now; only the Secure Curves algorithms are left.

celld version: **0.5.0** (`celld dev`, `compatibility_date` 2026-09-01, `nodejs_compat`).

## Why it is awkward to hit

Ed25519 `generateKey`, `exportKey`, `importKey`, and **`sign` all succeed** — only `verify` throws. A worker that mints keys and signs tokens looks completely healthy, and the failure only appears at the point where an incoming token is checked. In my case (a [better-auth](https://github.com/better-auth/better-auth) agent-auth provider) the thrown error surfaced as a generic `invalid_jwt` / 401 from the library's own error mapping, so it read as a malformed or expired token rather than a missing runtime primitive. The database rows showed the signing key stored and active, which pointed further away from the real cause.

## Repro

Identical worker, deployed to both runtimes, probing generate/export/import/sign/verify per algorithm:

```ts async function probe(name: string, algo: any, signAlgo: any) { const row: Record<string, unknown> = { algorithm: name }; const kp = (await crypto.subtle.generateKey(algo, true, ["sign", "verify"])) as CryptoKeyPair; row.generateKey = "ok"; const jwk = await crypto.subtle.exportKey("jwk", kp.publicKey); const data = new TextEncoder().encode("probe"); const sig = await crypto.subtle.sign(signAlgo, kp.privateKey, data); row.sign = "ok"; try { row.verify = await crypto.subtle.verify(signAlgo, kp.publicKey, sig, data); } catch (e) { row.verify = String(e).split("\n")[0]; } try { const imported = await crypto.subtle.importKey("jwk", jwk, algo, true, ["verify"]); row.verify_via_importedKey = await crypto.subtle.verify(signAlgo, imported, sig, data); } catch (e) { row.verify_via_importedKey = String(e).split("\n")[0]; } return row; }

export default { async fetch() { return Response.json([ await probe("Ed25519", { name: "Ed25519" }, { name: "Ed25519" }), await probe("ECDSA P-256", { name: "ECDSA", namedCurve: "P-256" }, { name: "ECDSA", hash: "SHA-256" }), ]); }, }; ```

## Results

`celld dev` (0.5.0):

| algorithm | generateKey | exportKey | importKey(jwk) | sign | verify | |---|---|---|---|---|---| | **Ed25519** | ok | ok | ok | ok | **`NotSupportedError: unsupported verify algorithm: ED25519`** | | **NODE-ED25519** | ok | ok | `NotSupportedError: unsupported key import` | **`NotSupportedError: unsupported sign algorithm: NODE-ED25519`** | – | | ECDSA P-256 (ES256) | ok | ok | ok | ok | `true` | | RSASSA-PKCS1-v1_5 (RS256) | ok | ok | ok | ok | `true` | | RSA-PSS (PS256) | ok | ok | ok | ok | `true` | | HMAC SHA-256 (HS256) | ok | – | – | ok | `true` | | **X25519 (deriveBits)** | **`NotSupportedError: unsupported key algorithm: X25519`** | – | – | – | – |

Cloudflare Workers, same code, same `compatibility_date`:

| algorithm | generateKey | exportKey | importKey(jwk) | sign | verify | |---|---|---|---|---|---| | Ed25519 | ok | ok | ok | ok | `true` | | NODE-ED25519 | ok | ok | ok | ok | `true` | | ECDSA P-256 (ES256) | ok | ok | ok | ok | `true` | | RSASSA-PKCS1-v1_5 (RS256) | ok | ok | ok | ok | `true` | | RSA-PSS (PS256) | ok | ok | ok | ok | `true` | | HMAC SHA-256 (HS256) | ok | – | – | ok | `true` | | X25519 (deriveBits) | ok | – | – | `deriveBits: ok` | – |

`verify` and `verify_via_importedKey` agree in every row, so it is the algorithm that is unsupported, not a key-handle or import-path issue.

## Impact

EdDSA/Ed25519 is the default JWT algorithm for better-auth's JWT plugin and the only algorithm advertised by its agent-auth provider, so that whole class of app can be developed and signed locally but never verified locally. The practical workaround is to deploy to Workers to exercise any incoming-token path, which removes most of the value of the local runtime for auth work.

X25519 is listed separately above because it fails earlier, at `generateKey` — worth noting if Secure Curves gets addressed as one unit.

## Suggestion

If Ed25519 verification is not planned soon, it would help to surface it at `generateKey`/`importKey` rather than at `verify`. Letting keys be created and signatures produced, then failing only on verification, sends debugging toward the token and the key material instead of the runtime.

Happy to test a build.

  1. AK
    Author

    Still reproduces on **v0.5.1** (released 2026-09-19). Re-ran the same minimal worker against the freshly downloaded `celld-aarch64-apple-darwin` binary:

    ```json {"generateKey":"ok","sign":"ok","error":"NotSupportedError: unsupported verify algorithm: ED25519"} ```

    `generateKey` and `sign` still succeed and only `verify` throws, so the failure surfaces far from its cause \u2014 in our case as a generic `invalid_jwt` 401 during agent registration, with a perfectly valid Ed25519 key sitting in the database. The same worker binary returns `true` on deployed Workers.

    Noting it here since v0.5.1 fixed #204 (`KeyObject.toCryptoKey()`) and #209, which are adjacent enough that someone might reasonably assume this went with them.

  2. BA

    Thanks for the report, I just fixed it. It will be included in the next release.

  3. bartlomiejuclosedlast month on GitHub

  4. bartlomiejureferenced 81d1c52fb5belast month on GitHub

  5. bartlomiejureferenced 0aa22f091625last month on GitHub

  6. bartlomiejureferenced 46692d514ac3last month on GitHub

  7. ryreferenced def3ad02192dlast month on GitHub

  8. bartlomiejureferenced bab562b22376last month on GitHub

  9. bartlomiejureferenced af69bb4337c5last month on GitHub

  10. bartlomiejureferenced bad4649d01f0last month on GitHub