Проверка подписи JWT/JWS на чистом OneScript: RS256 (RSASSA-PKCS1-v1_5
с SHA-256), открытые ключи из JWKS или DER, длинная арифметика и умножение
Монтгомери — без внешних криптобиблиотек и без вызовов openssl из процесса.
Библиотека появилась ради одной задачи: принять OIDC id-token из CI (GitHub Actions, GitLab CI) и доказать, что его выпустил именно тот провайдер, чьи ключи лежат в его JWKS. Всё остальное — следствие.
Публичный интерфейс — два класса. Всё остальное лежит в src/internal и
меняется без оглядки на обратную совместимость.
Кэш = Новый КэшJWKS(Издатель); // ключи: discovery, JWKS, кэш, ротация
Проверяющий = Новый ПроверяющийJWS(Кэш); // подпись и сроки
Итог = Проверяющий.Проверить(Токен); // → исход проверки, без исключенийЗависимость одна: 1connector (транспорт по умолчанию). Транспорт и часы внедряются, поэтому тесты идут без сети и без ожидания реального времени.
Библиотека доказывает происхождение токена — и только. Право предъявителя что-либо сделать из верной подписи не следует, а знать, кому что можно, может лишь приложение:
| Приложение | Библиотека |
|---|---|
| каким издателям верить и где взять токен | подпись RS256 по ключу с нужным kid |
iss, aud, sub, repository, job_workflow_ref и прочие клеймы |
alg из белого списка, а не из токена |
| что делать с отказом: отклонить, повторить, показать | exp/nbf с допуском на расхождение часов |
| где живут настройки провайдера | получение и кэш ключей, поведение при ротации |
| логирование | ничего не логирует |
Проверенные клеймы приложение разбирает само:
Клеймы = Итог.Клеймы;
Если Клеймы["iss"] <> "https://token.actions.githubusercontent.com"
Или Клеймы["aud"] <> "https://hub.example.com"
Или Клеймы["repository"] <> ОжидаемыйРепозиторий Тогда
ВызватьИсключение "Токен подписан верно, но выпущен не для нас";
КонецЕсли;algберётся из белого списка (RS256), а не из токена. Токен сalg: noneилиalg: HS256отвергается до того, как кто-либо посмотрит на подпись, — классическая подмена алгоритма не работает.- Ключ выбирается только по
kid. Токен безkidотвергается: угадывать, каким из ключей провайдера он подписан, библиотека не станет. - Токен без
expотвергается: срок «никогда» — не срок. Если ваш провайдер такие выпускает, снимите требование явно:УстановитьТребованиеExp(Ложь). - Модуль короче 2048 бит — отказ, а не предупреждение. Верхняя граница — 8192 бита. Чётный модуль, чётная или единичная открытая экспонента — тоже отказ.
- DigestInfo проверяется строго. Восстановленное EM сравнивается целиком
с эталоном
0x00 0x01 0xFF… 0x00 || DigestInfo(SHA-256) || хеш, поэтому BER-разночтения (DigestInfo без NULL-параметров), лишние байты после хеша, чужой OID, укороченный padding и ноль внутри padding отвергаются. Это закрыто отдельным набором тестов: подписи для таких EM собраны сырой операцией RSA закрытым ключом — обычным подписыванием их не получить. - Вырожденные подписи (
s = 0,s = 1,s ≥ n, длина ≠ длине модуля) отвергаются без переполнений. Требованиеs < n— не формальность: без него к любой годной подписи можно прибавитьnи получить вторую годную (ковкость подписи, RFC 8017 §5.2.2); это закрыто отдельным вектором. - Отказ вместо исключения. Недоступный JWKS, таймаут, мусор вместо JSON,
документ без нужного
kid— всё это исходы проверки (Успех = Ложьс кодом), а не исключение наружу. - Клеймы отдаются только при успехе. У отказа
Итог.Клеймы—Неопределено, чтобы непроверенную нагрузку нельзя было использовать по невнимательности. - Ограничения на разрастание: предел длины токена (64 КиБ), размера полученного тела JWKS (256 КиБ, считается в байтах UTF-8) и количества ключей в документе (20). Предел тела применяется к уже полученному ответу: он защищает разбор и кэш, но не канал — транспорт скачивает ответ целиком, и если вам нужен обрыв на лету, подставьте свой транспорт.
- Адрес ключей обязан быть
httpsи жить на хосте издателя. Иначе тот, кто подсунул документ discovery, уводит проверку на свои ключи.
Библиотека ничего не логирует. Ни токен, ни его клеймы не попадают ни в лог, ни в текст исключений: протокольные исходы уезжают структурой, которую пишет (или не пишет) приложение.
opm install jwt-verify.ЗависитОт("jwt-verify", "0.2.0")#Использовать jwt-verify
Кэш = Новый КэшJWKS("https://token.actions.githubusercontent.com");
Проверяющий = Новый ПроверяющийJWS(Кэш);
Итог = Проверяющий.Проверить(Токен);
Если Не Итог.Успех Тогда
ВызватьИсключение "Токен отклонён: " + Итог.Код + " — " + Итог.Сообщение;
КонецЕсли;
Сообщить("Пакет публикует " + Итог.Клеймы["repository"]); // acme/widgetsКэшJWKS сам сходит за /.well-known/openid-configuration, возьмёт оттуда
jwks_uri, прочитает ключи и запомнит подготовленный ключ. Для GitLab всё
то же самое с издателем https://gitlab.com.
MIT — см. LICENSE.