Как работать с JWE и NestedJWT?

JSON Web Encryption

Полезные ссылки:

  1. Спецификация rfc7516;
  2. Онлайн инструмент для работы с JWE;
  3. Библиотеки для работы с JWE: nimbus-jose-jwt, справочник openid;
  4. Информация для разработчиков на PHP: web-token.spomky-labs.com

JWE — разновидность JWT, при которой полезная нагрузка подлежит шифрованию.

Структурно JWE состоит из пяти частей:

  • header — заголовок;
  • encrypted key — зашифрованный ассиметричным алгоритмом ключ;
  • initialization vector — вектор инициализации;
  • ciphertext — зашифрованная симметричным алгоритмом полезная нагрузка;
  • authentication tag — тег аутентификации.

Каждая часть кодируется при помощи алгоритма base64url. Закодированные части соединяются друг с другом через знак «.»

BASE64URL(JOSE Header) + '.' +
BASE64URL(JWE Encrypted Key)  + '.' +
BASE64URL(JWE Initialization Vector) + '.' +
BASE64URL(JWE Ciphertext) + '.' +
BASE64URL(JWE Authentication Tag)

Формирование заголовка

Аналогично JWT, это JSON-объект, содержащий сведения о токене, который кодируется при помощи алгоритма base64url.

ПараметрОписаниеОбязательныйПример
algУказание алгоритма, с помощью которого зашифрован encrypted key. Рекомендуется использовать RSA-OAEP-256.ДаRSA-OAEP-256
kidИдентификатор ключа, с помощью которого выполнено ассиметричное шифрование. Значение должно совпадать со значением одноимённого параметра в публичном ключе с меткой «use»:«enc», опубликованным на jwks_endpoint.Даrsa1
encУказание алгоритма, с помощью которого зашифрован ciphertext. Рекомендуется использовать A256GCM.ДаA256GCM
typТип контента, которым является итоговый токен. Поддерживается только JWT.ДаJWT
ctyДополнительная информация, описывающая тип контента. Обязательно используется для NestedJWT со значением «JWT».НетJWT

Пример JSON Header:

{
  "alg": "RSA-OAEP-256",
  "kid": "rsa1",
  "enc": "A256GCM",
  "typ": "JWT"
}

Пример JWE Header:

eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJraWQiOiJyc2ExIiwiZW5jIjoiQTI1NkdDTSIsInR5cCI6IkpXVCJ9

Шифрование

В общем виде порядок шифрования состоит из следующих шагов:

  1. Сформировать json header;
  2. Сгенерировать случайное значение для encrypted key;
  3. Зашифровать сгенерированное значение публичным ключом получателя с использованием alg из json header;
  4. Сгенерировать initialization vector;
  5. Сгенерировать Authentication Tag;
  6. Зашифровать полезную нагрузку с помощью encrypted key, initialization vector и Authentication Tag с использованием enc из json header;
  7. Сформировать итоговый JWE из отдельно закодированных в base64url частей с разделителем «.».

Пример на PHP:

private function buildJWE(string $payload, string $keyEncryptionAlg, string $contentEncryptionAlg): string
{
    $jweBuilder = $this->jweBuilderFactory->create(
        array_keys($this->config['jwt']['key_encryption_algorithms']),
        array_keys($this->config['jwt']['content_encryption_algorithms']),
        array_keys($this->config['jwt']['compressions'])
    );

    $jwe = $jweBuilder->create()
        ->withPayload($payload)
        ->withSharedProtectedHeader([
            'alg' => $keyEncryptionAlg,
            'enc' => $contentEncryptionAlg,
        ])
        ->addRecipient(JWKFactory::createFromValues($this->getJWK()))
        ->build();

    $jweSerializer = $this->jweSerializerManagerFactory->create([EncCompactSerializer::NAME]);

    return $jweSerializer->serialize(EncCompactSerializer::NAME, $jwe);
}

Расшифровка

В общем виде порядок расшифровки состоит из следующих шагов:

  1. Декодировать из base64url значения элементов;
  2. Вычислить encrypted key — расшифровать приватным ключом с использованием alg из json header;
  3. Вычислить полезную нагрузку — расшифровать Ciphertext с использованием encrypted key, initialization vector и authentication tag с применением метода, указанного в enc из json header.

Пример на PHP:

private function getJWEPayload(): string
{
    // Our key.
    $encKey = $this->getJWKFromKey('private', 'enc');

    $jweLoader = $this->jweLoaderFactory->create(
        ['jwe_compact'],
        [$this->jwe->getSharedProtectedHeaderParameter('alg')],
        [$this->jwe->getSharedProtectedHeaderParameter('enc')],
        ['DEF']
    );

    $jwe = $jweLoader->loadAndDecryptWithKey($this->token, $encKey, $recipient);

    return $jwe->getPayload();
}

Nested JWT

Частный случай JWE с единственным отличием — в качестве полезной нагрузки выступает не JSON-объект, а предварительно сформированный и подписанный JWT. При этом исходный JSON-объект становится полезной нагрузкой вложенного JWT.