crypto.subtle は、Web プラットフォームの標準の暗号インターフェースです。同じ呼び出し(generateKey、importKey、sign、verify、exportKey)が Chromium、Firefox、WebKit でも、Node.js などのサーバーランタイムでも動作するため、ブラウザーとサーバーの両方で動かす必要があるコードには自然な選択肢です。使えるのはセキュアコンテキストだけで、通常の http のページでは crypto.subtle は undefined になります。
仕様は ECDSA 署名の形式を生の r‖s(P-256 では 64 バイト)に固定しており、チェックでも 4 つのエンジンすべてでそのとおりの結果になりました。これは JWS や JWT と一致するため ES256 トークンに変換は不要ですが、OpenSSL、Java の SHA256withECDSA、Go の SignASN1、node:crypto とは一致しません。これらはいずれも ASN.1 DER を使います。DER 署名を verify() に渡すとエラーではなく単に false が返るため、形式の不一致が偽造された署名のように見えます。DER ⇄ 生形式の変換ツールで変換するか、ECDSA 署名検証ツールで両方の形式を確認してください。
もう 1 つのよくあるつまずきの原因は鍵です。importKey("spki") が受け付けるのは DER のバイト列で、PEM テキストは受け付けません。"raw" は SEC1 の点を受け付けますが、圧縮形式は Node.js、Chromium、Firefox では受け入れられる一方、WebKit では拒否されます。移植性のあるコードでは非圧縮形式の点を渡してください。秘密鍵は PKCS#8 DER か JWK でしかインポートできません。OpenSSL がデフォルトで書き出す SEC1 の EC PRIVATE KEY ファイルは、JWK 変換ツールなどで先に PKCS#8 に変換する必要があります。
対応する曲線は P-256、P-384、P-521 だけです。secp256k1 と Brainpool はどのエンジンでも NotSupportedError になるため、Bitcoin や Ethereum の鍵には @noble/curves などのライブラリが必要です。Ed25519 は 4 つのエンジンすべてで動作しますが、Ed448 は Node.js だけです。ポスト量子暗号への対応はまちまちです。Node.js 24.22 は ML-DSA と ML-KEM を実験的機能の警告付きで実装し、Chromium 156 は ML-DSA と ML-KEM-768/1024 を受け付けますが ML-KEM-512 は受け付けません。Firefox 157 と WebKit 27.2 はどちらにも対応していません。利用する前に機能検出を行い、ポスト量子署名ラボでアルゴリズムを試してください。
サンプルは移植性のあるパターンを示しています。アルゴリズムオブジェクトでハッシュを指定し、外部の PEM 鍵は先に DER にデコードしてからインポートします。また、64 バイトの出力は DER でラップしないと OpenSSL 方式の検証コードでは読み取れないことに注意してください。