Pull to refresh
239.57
FirstVDS
Виртуальные серверы в ДЦ в Москве

Почему PASETO лучше для аутентификации, чем JWT

Reading time8 min
Views11K

В веб-разработке одним из наиболее популярных решений является аутентификация на основе токенов. Чаще всего для создания системы аутентификации используют JWT (порой даже там, где это не нужно). Но несмотря на популярность, JWT имеет ряд недостатков. Поэтому появляются новые решения для аутентификации на основе токенов. В этой статье мы рассмотрим PASETO — токен, который был разработан для замены JWT.

Platform-Agnostic SEcurity TOken (PASETO) можно перевести как платформо-независимый токен безопасности. На данный момент RFC для PASETO не принят, но есть достаточно свежий черновик, найти его можно тут. PASETO предназначен для обмена информацией при помощи Cookie, заголовков HTTP, параметров запроса в URI. Данные по умолчанию сериализуются в JSON-формате и  шифруются для передачи. В целом описание назначения токенов довольно похоже на RFC для JWT, но с тем отличием, что в описании PASETO изначально больше говорится про безопасность шифрования и подписи токенов.

Прежде чем перейти к деталям реализации PASETO, стоит понять, зачем вообще создавать новый вид токенов. Идея создать новый токен появилась в Paragon Initiative — одним из направлений в компании является безопасность веб-приложений. Изначально они разработали реализацию для PHP и написали статью, в которой объяснили, почему PASETO может быть хорошей альтернативой JWT. В основе PASETO использовались следующие принципы:

  1. токен должен быть безопасен по умолчанию;

  2. токен должен быть прост в использовании;

  3. токен не должен вызывать сложностей для анализа и исследования у разработчиков, аудиторов и т. д.

Поводом для разработки нового стандарта стали претензии к JWT. Частой проблемой в JWT является то, что периодически появляются критические уязвимости. Например, одна из самых известных проблем касалась поля «alg». Если кратко, то суть уязвимости заключалась в том, что злоумышленник мог поставить в поле «alg» значение «None», и многие библиотеки принимали такой токен без проверки его валидности и целостности, что позволяло злоумышленникам модифицировать токен без обнаружения этого сервером. Несмотря на то, что эту уязвимость уже многие пофиксили, в ряде проектов она может все еще оставаться, т. к. ряд библиотек больше не поддерживается, а кто-то не обновил исправленную версию библиотеки.

Еще одна проблема состоит в том, что не все разработчики хорошо разбираются в криптографии, и в поле «alg» могут использовать устаревшие или ненадежные алгоритмы, например, RSA with PKCS #1v1.5 Padding. С одной стороны, эта проблема больше относится к разработчикам, чем к JWT, но с другой — неплохо иметь механизм, который предупреждал бы о таком и предлагал бы изменить алгоритм шифрования на актуальный. Соответственно, PASETO проектировался с учетом решения этих проблем.

PASETO состоит из 4 частей: version, purpose, payload, footer — последняя часть является опциональной. Теперь поговорим подробнее о назначении каждой из частей.

Поле version определяет версию токена, криптографический протокол и набор криптографических примитивов, которые будут применяться для шифрования. Сейчас существует 4 версии PASETO. Первые две считаются устаревшими (если интересно подробнее про них почитать, то информацию можно найти тут), и если вы их используете, то вам нужно мигрировать на актуальную версию (3 или 4 на момент публикации статьи).

Поле purpose описывает, для каких целей будет использоваться токен. Поле может принимать значение либо local, либо public. В случае, если токен локальный, то он шифруется при помощи симметричного алгоритма и секретного ключа — это означает, что для возможности чтения данных другими сервисами или людьми, мы должны будем делиться с ними секретным ключом. Если токен будет украден, злоумышленник не сможет расшифровать данные, которые в нём находятся. Если вы не имеете возможности поделиться секретным ключом, то вам необходимо использовать публичный токен, который позволяет прочитать данные любому, у кого он есть. Для безопасности такие токены подписываются асимметричными ключами шифрования, которые позволяют защитить данные от изменения.

Поле payload — это всегда JSON, который шифруется при создании токена, и в который помещаются данные, нужные для передачи. Так же как и у JWT, у PASETO есть claims (заявки), которые по сути представляют собой поля в JSON, используемые для хранения различной информации. Также есть поля, которые зарезервированы для внутреннего использования. Ниже представлен список этих полей и краткое описание целей, для которых они используются.

Ключ

Название атрибута

Тип

Пример

iss

Issuer

string

{"iss":"paragonie.com"}

sub

Subject

string

{"sub":"test"}  

aud

Audience

string

{"aud":"pie-hosted.com"} 

exp

Expiration

DtTime

{"exp":"2039-01-01T00:00:00+00:00"}

nbf

Not Before

DtTime

{"nbf":"2038-04-01T00:00:00+00:00"}

iat

Issued At

DtTime

{"iat":"2038-03-17T00:00:00+00:00"}

jti

Token ID

string

{"jti":"87IFSGFgPNtQNNuw0AtuLttP"}

  • Issuer сообщает информацию о том, кто выпустил это токен.  

  • Subject — для кого или иногда для какой цели был выпущен токен. Например, для токена аутентификации субъектом может быть идентификатор пользователя. 

  • Audience сообщает о том, какое приложение является получателем токена, чаще всего там находится url или другой идентификатор приложения.

  • Expiration устанавливает время истечения токена.

  • Not Before устанавливает время, до которого токен будет невалиден.

  • Issued At показывает время создания токена.

  • Token ID — идентификатор токена, который не должен повторяться у разных токенов.

Следующее поле — footer, которое является необязательным. Обычно оно используется для хранения служебной информации. В отличие от payload, это поле необязательно должно иметь формат JSON-объекта. 

Отдельно стоит упомянуть Implicit Assertions — дополнительные данные, которые используются для процесса PAE (Pre-Authentication Encoding). Они могут представлять собой сведения, связанные с приложением, но при этом никогда не хранятся в payload.

Теперь рассмотрим, что такое PAE. По сути, этот процесс используется для дополнения (padding) данных, что, в свою очередь, должно предотвратить ряд угроз.

Сам алгоритм PAE довольно прост: 

  1. Функция PAE принимает массив строк.

  2. Длина массива передается в функцию LE64, которая кодирует 8-байтное число в бинарную строку в формате LittleEndian, после чего эта строка прибавляется к выходному значению.

  3. Затем в цикле проходим по элементам массива и прибавляем к выходному значению результат функции LE64 для длины элемента массива, а также добавляем само значение.

  4. Возвращаем результат.

Пример реализации приведен ниже:

function LE64(n) {
   var str = '';
   for (var i = 0; i < 8; ++i) {
       if (i === 7) {
           // Clear the MSB for interoperability
           n &= 127;
       }
       str += String.fromCharCode(n & 255);
       n = n >>> 8;
   }
   return str;
}


function PAE(pieces) {
   if (!Array.isArray(pieces)) {
       throw TypeError('Expected an array.');
   }
   var count = pieces.length;
   var output = LE64(count);
   for (var i = 0; i < count; i++) {
       output += LE64(pieces[i].length);
       output += pieces[i];
   }
   return output;
}

Соответственно, результаты работы функции могут выглядеть следующим образом:

  • если передаем в PAE пустой массив([]), то получим строку "\x00\x00\x00\x00\x00\x00\x00\x00"

  • если передаем в PAE массив с 1 пустым элементом(['']), то получим строку "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"

  • если передаем в PAE массив с 1 непустым элементом(['test']), то получим строку "\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00test"

Теперь давайте рассмотрим процесс шифрования у PASETO на примере локального токена в 4 версии.

  1. Для начала обозначим используемые переменные. Сообщение — m, ключ — k, футер — f, и implicit assertions — i.

  2. Устанавливаем заголовок h с версией и назначением токена — v4.local.

  3. Генерируем массив размером 256 бит при помощи криптографического генератора случайных чисел (CSPRNG), обозначим эту переменную как n.

  4. Генерируем ключ шифрования (Ek) при помощи хэш-функции BLAKE2b и n (по сути, это соль для нашего сообщения, которая поможет нам получать разные токены с одним и тем же ключом), особенностью которой является то, что на выходе мы можем получить хэш произвольного размера от 1 до 64 байт. В нашем случае нам нужен хэш размером 56 байт, первыми 32 байтами будет ключ шифрования. А последние 24 байта будет использовать как счетчик (n2) в алгоритме XChaCha20.

  5. Генерируем ключ аутентификации (Ak) также при помощи BLAKE2b и выбираем размер хеша в 32 байта. Полученный хеш и будет нашим ключом.

  6. Псевдокод для действий 4 и 5 представлен ниже:

tmp = crypto_generichash(
   msg = "paseto-encryption-key" || n,
   key = key,
   length = 56
);
Ek = tmp[0:32]
n2 = tmp[32:]
Ak = crypto_generichash(
   msg = "paseto-auth-key-for-aead" || n,
   key = key,
   length = 32
);
  1. Далее мы шифруем сообщение при помощи алгоритма XChaCha20, используя ключ шифрования и счетчик.

c = crypto_stream_xchacha20_xor(
   message = m
   nonce = n2
key = Ek
);
  1. Затем мы последовательно объединяем в массив заголовок h, случайную последовательность n (п.3), и зашифрованное сообщение c (п.7), футер (f), и i (Implicit Assertions — это необязательная часть футера, которая может использоваться как дополнительная переменная в функции PAE). В результате работы функции PAE мы получим переменную preAuth.

  2. Применим BLAKE2b к preAuth с использованием ключа аутентификации и получим хэш t.

t = crypto_generichash(
   message = preAuth
   key = Ak,
   length = 32
);
  1. На данном шаге мы завершаем формирование токена. Конечный результат зависит от наличия футера:

  • если футер пустой, то получаем: h || base64url(n || c || t);

  • если футер не пустой, то получаем: return h || base64url(n || c || t) || . || base64url(f);

  • || в данном случае используется как конкатенация;

  • base64url() означает Base64url из RFC 4648 без использования «=».

Теперь давайте разберёмся с тем, как токен выглядит на практике. Допустим, у нас есть данные, которые мы хотим поместить в токен.

Заполним данные, которые пойдут в payload, например, так:

{
   "sub": "test_user",
   "exp": "2023-12-26T17:15:06.611Z",
   "iat": "2023-12-26T13:15:06.611Z",
   "name": "Mr. paseto",
   "role": "auth master"
}

Далее поместим в футер дополнительные данные:

{
   "other": "Habr footer"
}

Затем зашифруем данные при помощи ключа:

df5cfb4cc657036ddc797b494f4afa38a47ec2c71c648d40fec7a087656504e4

И в результате получим такой токен:

v4.local.uXQosuhrIzrIDUlzvpVUOUTxYX5tbKySwF-gA_kxjvz1PkK4Exg_HDLs2FNW0_oElUvcsthifFqlfR3UAYt-wC9KMR80-EYV5RGTTeNFTZWoJm1xRzL_DcrMIK-Scwlh19KGeE7_VYoTzZZMxn9kWuOANtGzFSG5tFKl8WSAoSweXc5Dt0pp3ouF4bBc9Do5GT445MJOIAdnWMAjDOL9xxNTVD_APwSHND8Xy_504KbnNetJWrSRdGmrbW0NRg.eyJvdGhlciI6IkhhYnIgZm9vdGVyIn0

Из токена мы можем понять, что для шифрования применялись алгоритмы, соответствующие 4 версии PASETO. И что данные в токене зашифрованы симметричным ключом, без которого мы не можем узнать, что внутри него. Обратите внимание на то, что payload (3 часть) токена будет отличаться даже при использовании одного и того же ключа, т. к. на этапе шифрования к сообщению добавляется соль.

Предполагаемые варианты использования PASETO

Локальные токены используются для того, чтобы защитить данные от несанкционированного доступа в зашифрованных куках или параметрах HTTP-запроса. Их можно использовать для долгосрочной аутентификации в приложении. Для идентификации токена в таком случае используется поле jti, которое может храниться в БД.

Публичные токены предполагаются для одноразовой аутентификации для третьей стороны, в данном случае такие токены можно использовать для аутентификации OpenID.

Как есть преимущества и недостатки у PASETO по сравнению с JWT?

Преимущества PASETO:

  1. PASETO всегда использует стойкие алгоритмы шифрования в своих актуальных версиях, что позволяет упростить разработку и поддержку системы аутентификации в приложении. В JWT разработчик должен самостоятельно проверять корректность выбранного алгоритма шифрования, что может привести к неправильной/устаревшей конфигурации токена.

  2. PASETO даёт важную информацию визуально, то есть, взглянув на токены, вы можете понять алгоритмы, которые использовались для шифрования, и его назначение (public/local). Это может быть полезно на этапе тестирования и отладки. Чтобы получить какую-либо информацию в jwt, нужно как минимум раскодировать информационную часть токена.

  3. PASETO обычно быстрее шифруются и декодируются, чем JWT. Это позволяет тратить на них меньше вычислительной мощности сервера, что бывает полезно в высоконагруженных системах.

Недостатки PASETO:

  1. До сих пор не был выпущен полноценный RFC для PASETO, доступен только черновик.

  2. На данный момент PASETO является малоизвестным и непопулярным решением, из-за чего для некоторых языков программирования либо вообще нет реализаций, либо есть реализация только для устаревших версий. Узнать о поддержке конкретного языка можно, например, тут

  3. В сети довольно мало информации о PASETO, особенно по новым версиям, поэтому, чтобы разобраться в его работе, придётся разбираться еще и в RFC, что не всем нравится.

В заключение отмечу, что построить безопасную систему аутентификации можно и при помощи JWT. Но PASETO предлагает ряд преимуществ, которые позволяют упростить работу программиста и снизить число вероятных ошибок. И прежде чем выбирать для своего приложения тот или иной формат, необходимо хорошо обдумать детали, т. к. форматы между собой несовместимы.

Полезные cсылки, которые помогут вам лучше разобраться с тем, как работает PASETO:

  1. Текст последнего rfc на гитхабе.

  2. Репозиторий со спецификациями PASETO и другой дополнительной информацией.

Автор статьи @yurii_habr


НЛО прилетело и оставило здесь промокод для читателей нашего блога:
-15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.

Tags:
Hubs:
Total votes 16: ↑14 and ↓2+12
Comments9

Articles

Information

Website
firstvds.ru
Registered
Founded
Employees
51–100 employees
Location
Россия
Representative
FirstJohn