Пакет · jwt

Асимметричные ключи (RSA и ECDSA)

Когда сторона, проверяющая токен, не должна иметь возможности его подделать, используйте асимметричную подпись: приватный ключ подписывает, а свободно распространяемый публичный ключ проверяет. Это руководство охватывает RSA (RS*) и ECDSA (ES*), а также один дополнительный шаг, который стоит сделать, когда ключей больше одного, — kid.

Как выбирается ключ

kid (идентификатор ключа) — указатель в набор ключей. По RFC 7515 §4.1.4 он необязателен, и правило выбора одно для всех алгоритмов:

В заголовке В decode() передано Что произойдёт
kid есть ключ с таким идентификатором берётся именно он
kid есть такого идентификатора нет отказ — Key with 'kid'=… was not found
kid нет ровно один ключ берётся он
kid нет несколько ключей отказ — выбрать не из чего

Почему при неизвестном kid нет отката на другой ключ

Если заголовок называет ключ, берётся именно он — или ничего. Перебор ключей до совпадения позволил бы токену влиять на то, каким ключом его проверят, а это решение принимает приложение, а не тот, кто прислал токен.

По той же причине несколько ключей без kid — отказ, а не «возьмём первый»: иначе проверка зависела бы от порядка элементов массива.

kid нужен, когда ключей несколько: ротация, несколько эмитентов, JWKS. Для одного ключа третий аргумент PrivateKey можно не указывать — токен будет валиден и для этой библиотеки, и для любой другой.

RSA: подпись и проверка

Сгенерируйте пару (см. «Установка и требования»), затем:

sign-rsa.php
<?php

use Flytachi\Jwt\JWT;
use Flytachi\Jwt\Entity\JwtPayload;
use Flytachi\Jwt\Entity\PrivateKey;

$privateKey = openssl_pkey_get_private(file_get_contents('private.pem'));

$token = JWT::encode(
  new JwtPayload(['sub' => 'user-42', 'exp' => time() + 3600]),
  new PrivateKey($privateKey, 'RS256', 'rsa-key-1') // 3-й аргумент = kid
);

Проверьте публичным ключом, указав тот же kid:

verify-rsa.php
<?php

use Flytachi\Jwt\JWT;
use Flytachi\Jwt\Entity\PublicKey;

$publicKey = openssl_pkey_get_public(file_get_contents('public.pem'));

$payload = JWT::decode($token, [
  'rsa-key-1' => new PublicKey($publicKey, 'RS256'),
]);

echo $payload->getClaim('sub'); // user-42

ECDSA: меньшие ключи, тот же процесс

ECDSA даёт вам гораздо более короткие ключи и подписи при эквивалентной безопасности. Код идентичен, за исключением алгоритма и кривой:

php
use Flytachi\Jwt\JWT;
use Flytachi\Jwt\Entity\JwtPayload;
use Flytachi\Jwt\Entity\PrivateKey;
use Flytachi\Jwt\Entity\PublicKey;

$privateKey = openssl_pkey_get_private(file_get_contents('ec-private.pem'));
$token = JWT::encode(
  new JwtPayload(['sub' => 'user-42', 'exp' => time() + 3600]),
  new PrivateKey($privateKey, 'ES256', 'ec-key-1')
);

$publicKey = openssl_pkey_get_public(file_get_contents('ec-public.pem'));
$payload = JWT::decode($token, [
  'ec-key-1' => new PublicKey($publicKey, 'ES256'),
]);

Кривые соответствуют алгоритмам

ES256 требует ключ P-256, ES384 — ключ P-384, а ES512 — ключ P-521. Библиотека автоматически преобразует сырую подпись ECDSA в ASN.1 DER для OpenSSL — см. «Модель безопасности». Генерируйте кривые, как показано в «Установка и требования».

Публикуйте свои публичные ключи как JWKS

Поскольку публичный ключ безопасно публиковать, распространённый подход — предоставить его как JSON Web Key Set, который получают проверяющие стороны. Вы можете собрать одну запись из разобранного ключа или просто опубликовать стандартные поля, которые уже есть в ваших ключах:

json
{
"keys": [
  {
    "kty": "RSA",
    "alg": "RS256",
    "kid": "rsa-key-1",
    "use": "sig",
    "n": "0vx7agoebGcQ...",
    "e": "AQAB"
  }
]
}

Потребители затем проверяют токены, ни разу не касаясь вашего приватного ключа — см. «Проверка через JWKS».

Выбор между RSA и ECDSA

RSA (RS*) ECDSA (ES*)
Размер ключа (сопоставимая безопасность) 2048–4096 бит 256–521 бит
Размер подписи большой малый
Скорость подписи медленнее быстрее
Скорость проверки быстрая быстрая
Распространённость универсальна очень широкая

Выбирайте RSA для максимальной совместимости со старыми системами; выбирайте ECDSA для меньших токенов и более быстрого подписания. Оба варианта держат приватный ключ приватным.

Связанные материалы