Верификация JWS

JWS (JSON Web Signature) представляет собой компактный формат передачи подписанных данных, основанный на JSON и стандартизированный в RFC 7515. В контексте веб-приложений он часто используется как основа JWT, где payload содержит полезную нагрузку, а подпись гарантирует целостность и подлинность сообщения.

JWS состоит из трёх частей, разделённых точками:

header.payload.signature

Каждая часть закодирована в Base64URL:

  • header — метаданные (алгоритм подписи, тип токена)
  • payload — полезные данные
  • signature — криптографическая подпись

Верификация JWS сводится к проверке того, что подпись действительно соответствует header и payload при использовании ожидаемого ключа и алгоритма.

Ключевой принцип: любые изменения header или payload должны приводить к невалидной подписи.

Jsrsasign и базовая модель проверки

Библиотека Jsrsasign предоставляет пространство имён KJUR.jws.JWS, где реализованы основные операции работы с JWS.

Основная функция верификации:

KJUR.jws.JWS.verify(token, key, options)

или более расширенные формы:

KJUR.jws.JWS.verifyJWT(token, key, {
  alg: ['RS256'],
  verifyAt: KJUR.jws.IntDate.getNow()
});

Внутри выполняется:

  • разбор токена
  • извлечение header
  • проверка алгоритма
  • пересчёт подписи
  • сравнение с переданной подписью

Алгоритмы подписи и их роль в проверке

Алгоритм, указанный в header (alg), определяет метод криптографической проверки.

HMAC (HS256, HS384, HS512)

Симметричная схема:

подпись = HMAC(secret, base64url(header) + "." + base64url(payload))

Проверка требует того же секрета.

Пример:

const token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";

const isValid = KJUR.jws.JWS.verify(token, "shared_secret", ["HS256"]);

Особенность: безопасность полностью зависит от секретного ключа.

RSA (RS256, RS384, RS512)

Асимметричная схема:

  • приватный ключ — для подписи
  • публичный ключ — для проверки
const pubKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----`;

const isValid = KJUR.jws.JWS.verify(token, pubKey, ["RS256"]);

Преимущество: публичный ключ безопасно распространяется.

ECDSA (ES256 и др.)

Использует эллиптические кривые:

const isValid = KJUR.jws.JWS.verify(token, pubKey, ["ES256"]);

Более компактные подписи при высокой криптостойкости.

Разбор структуры токена перед проверкой

Jsrsasign позволяет явно разобрать JWS:

const parsed = KJUR.jws.JWS.parse(token);

Результат содержит:

  • headerObj
  • payloadObj
  • signatureHex

Это полезно для предварительного анализа до криптографической проверки.

Проверка header и контроль алгоритма

Одним из критических этапов является проверка alg.

Header может содержать:

{
  "alg": "RS256",
  "typ": "JWT"
}

Jsrsasign позволяет ограничивать допустимые алгоритмы:

KJUR.jws.JWS.verify(token, pubKey, ["RS256"]);

Если алгоритм отличается — проверка не проходит.

Это защищает от атак подмены алгоритма.

Работа с JWT как частным случаем JWS

JWT фактически является JWS с JSON payload.

Проверка:

const isValid = KJUR.jws.JWS.verifyJWT(token, pubKey, {
  alg: ["RS256"]
});

Дополнительно можно проверять claims:

const result = KJUR.jws.JWS.verifyJWT(token, pubKey, {
  alg: ["RS256"],
  verifyAt: KJUR.jws.IntDate.getNow()
});

После верификации payload извлекается:

const payloadObj = KJUR.jws.JWS.parse(token).payloadObj;

JWKS и динамическая верификация ключей

В реальных системах ключи часто хранятся в JWKS (JSON Web Key Set).

Пример структуры:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "key1",
      "n": "...",
      "e": "AQAB"
    }
  ]
}

Процесс:

  1. Извлечение kid из header
  2. Поиск соответствующего ключа
  3. Формирование PEM
  4. Проверка подписи

Jsrsasign позволяет работать с ключами через KEYUTIL:

const keyObj = KEYUTIL.getKey(jwksKey);
const isValid = KJUR.jws.JWS.verify(token, keyObj, ["RS256"]);

Критические моменты безопасности

1. Жёсткая фиксация алгоритма

Нельзя доверять alg из токена без ограничения:

["RS256"] // допустимо

2. Запрет “none” алгоритма

Некоторые реализации допускают alg: none, что полностью отключает проверку подписи. В Jsrsasign это должно быть исключено через whitelist алгоритмов.

3. Проверка времени (exp, iat, nbf)

Подпись валидна криптографически, но токен может быть просрочен:

const payload = KJUR.jws.JWS.parse(token).payloadObj;

if (payload.exp < KJUR.jws.IntDate.getNow()) {
  // токен просрочен
}

4. Ошибки работы с ключами

Типичные проблемы:

  • использование публичного ключа для HS256
  • перепутанные PEM форматы
  • неверная кодировка Base64URL

Частые сценарии валидации

Проверка входящего JWT

function validate(token, publicKey) {
  const ok = KJUR.jws.JWS.verify(token, publicKey, ["RS256"]);
  if (!ok) return false;

  const payload = KJUR.jws.JWS.parse(token).payloadObj;

  return payload.exp > KJUR.jws.IntDate.getNow();
}

Проверка с несколькими алгоритмами

KJUR.jws.JWS.verify(token, pubKey, ["RS256", "ES256"]);

Разделение ответственности

  • криптографическая проверка
  • проверка бизнес-правил
  • проверка времени жизни
  • проверка issuer и audience

Внутренняя логика Jsrsasign при верификации

Процесс включает:

  1. Base64URL decode header

  2. Проверка alg

  3. Формирование signing input:

    base64url(header) + "." + base64url(payload)
  4. Декодирование signature

  5. Криптографическая проверка через выбранный алгоритм

  6. Возврат boolean результата

При несоответствии любого шага проверка завершается отрицательно.

Типовые ошибки при интеграции

  • использование JSON.parse до проверки подписи
  • доверие payload без проверки exp
  • игнорирование alg whitelist
  • хранение секретов на клиенте при HS256
  • отсутствие ротации ключей

Особенности работы с PEM и KEYUTIL

Jsrsasign использует утилиту преобразования ключей:

const pubKeyObj = KEYUTIL.getKey(pemString);

Далее ключ может использоваться напрямую:

KJUR.jws.JWS.verify(token, pubKeyObj, ["RS256"]);

Это упрощает работу с различными форматами ключей (PEM, JWK, DER).

Контроль целостности payload после верификации

После успешной проверки подписи payload можно считать неизменённым с момента подписания, однако логическая валидация остаётся обязательной:

  • соответствие роли пользователя
  • проверка scope
  • проверка audience (aud)
  • проверка issuer (iss)