crypto.subtle — стандартный криптографический интерфейс веб-платформы. Одни и те же вызовы — generateKey, importKey, sign, verify, exportKey — работают в Chromium, Firefox и WebKit, а также в серверных средах выполнения, например в Node.js, поэтому это естественный выбор для кода, который должен работать на обеих сторонах. Интерфейс доступен только в безопасных контекстах: на странице, открытой по обычному HTTP, crypto.subtle равен undefined.
Спецификация фиксирует формат подписи ECDSA как raw r‖s (64 байта на P-256), и проверка обнаружила именно его во всех четырёх движках. Это совпадает с JWS и JWT, поэтому токены ES256 не нужно преобразовывать, но не совпадает с OpenSSL, SHA256withECDSA в Java, SignASN1 в Go и node:crypto: все они работают с ASN.1 DER. Подпись в DER, переданная в verify(), даёт просто false, а не ошибку, поэтому несовпадение форматов выглядит как подделка; преобразуйте подпись конвертером DER ⇄ raw или проверьте обе формы в инструменте проверки подписей.
Второй частый источник проблем — ключи. importKey("spki") принимает байты DER и никогда не принимает текст PEM; "raw" принимает точку SEC1, причём Node.js, Chromium и Firefox принимают и сжатую форму, а WebKit её отклоняет, поэтому в переносимом коде передавайте несжатую точку. Закрытые ключи импортируются только как PKCS#8 DER или JWK: файлы SEC1 EC PRIVATE KEY, которые OpenSSL записывает по умолчанию, сначала нужно преобразовать в PKCS#8, например конвертером JWK.
Список кривых ограничен P-256, P-384 и P-521: secp256k1 и Brainpool везде приводят к NotSupportedError, поэтому для ключей Bitcoin и Ethereum нужна библиотека, например @noble/curves. Ed25519 работает во всех четырёх движках, 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.