Сообщение об ошибке
signature verification failed
jose (JavaScript) — JWSSignatureVerificationFailed, code ERR_JWS_SIGNATURE_VERIFICATION_FAILED
Что это означает
jose успешно разобрала JWS и выполнила криптографическую проверку, но подпись не проходит проверку выбранным ключом. Структура, алгоритм и тип ключа были допустимы — просто не сошлась математика, а это указывает на ключ или на изменённые подписанные байты.
Почему это происходит
Не тот ключ для этого токена
частоУ проверяющей стороны не тот ключ, которым подписывал издатель: JWKS после ротации, где старого kid уже нет, устаревший ключ, жёстко прописанный в коде, или путаница с окружениями (проверяющая сторона из staging, токен из рабочей среды).
Подписанные байты изменились по пути от подписанта к проверяющей стороне
частоJWS подписывает в точности текст заголовка и полезной нагрузки (header.payload) в base64url. Всё, что перекодирует эти сегменты, — шлюз, который нормализует дополнение base64, повторная сериализация JSON, обрезанные при передаче символы — ломает проверку, хотя декодированная полезная нагрузка выглядит точно так же.
Самописный код подписи ES256 выдал DER
иногдаcrypto.sign() в Node по умолчанию выдаёт подписи ECDSA в формате ASN.1 DER, а RFC 7518 требует, чтобы подписи JWS для ES256 были в формате raw r‖s (ровно 64 байта). Токен, третий сегмент которого декодируется в ~70–72 байта, начинающихся с 0x30, подписан не в том формате.
Как исправить
- 1.
Выбирайте ключи по kid из JWKS издателя
Пусть jose сама выбирает подходящий ключ для каждого токена вместо закрепления одного ключа вручную; createRemoteJWKSet кеширует ключи и обновляет их при ротациях.
js import { createRemoteJWKSet, jwtVerify } from "jose"; const jwks = createRemoteJWKSet(new URL("https://issuer.example/.well-known/jwks.json")); const { payload } = await jwtVerify(token, jwks, { algorithms: ["ES256"], issuer: "https://issuer.example", }); - 2.
Если подписываете через node:crypto, выдавайте подпись в формате raw r‖s
В собственном коде подписи запрашивайте вывод в формате IEEE P1363, чтобы подпись была в 64-байтном формате, которого требует JWS, — или проверяйте результат своего кода подписи по длине третьего сегмента.
js import { sign } from "node:crypto"; const sig = sign("sha256", Buffer.from(signingInput), { key: privateKey, dsaEncoding: "ieee-p1363", // raw r||s, 64 bytes for P-256 }); - 3.
Проверьте, как выглядит сегмент подписи
Декодируйте третий сегмент токена: 64 байта означают raw r‖s (правильно для ES256), а ~70 байт, начинающихся с 0x30, — DER. Конвертер DER ⇄ raw ниже показывает значения r и s и преобразует подпись из одного формата в другой.