Хабрахабр

Формирование JWS и JWK из rsa-ключей на примере интеграции Let’s Encrypt и ISPmanager

Всем привет! Меня зовут Дмитрий Смирнов, я разработчик из ISPsystem и это именно я в ответе за появление в панели ISPmanager 5 интеграции с Let’s Encrypt. Расскажу, как проходила разработка плагина, как он менялся и как пришел в теперешнее состояние. Из текста узнаете, как формировать JWS и JWK из rsa-ключей и получать Let’s Encrypt сертификат для ACME v01. Если интересно, добро пожаловать под кат.

image

Let’s Encrypt 1.0

Первая версия плагина была прекрасна во всех отношениях. Ее успех сравним только с ее монументальным крахом (да-да, сценаристы «Матрицы» тоже читали эту статью). Задачу мне тогда поставили примерно так: вот тебе letsencrypt.org, сделай что-нибудь с этим. Ну я и сделал.

Да и дружелюбным к пользователям он, мягко говоря, не был. Изначально плагин просто тянул с гитхаба официальный клиент Let’s Encrypt и работал напрямую с ним. Ничего не заказываем. Нет домена? Закругляемся. Не резолвятся псевдонимы? Вся подготовительная работа по выпуску сертификата ложилась на плечи пользователя, и любая ошибка приводила к неудачному получению сертификата.

Так началось мое увлекательное путешествие в удивительный мир интернет-безопасности и клиентоориентированности. Надо ли говорить, что плагин вернули на доработку.

Let’s Encrypt 2.0

Перед вторым подходом к разработке мы сформулировали несколько задач:

  • Реализовать получение сертификата на уровне протокола ACME.
  • Подробно информировать пользователя о процессе.
  • Сделать возможным получение сертификата при создании веб-домена.
  • Ожидать резолва имен домена в течение суток после начала процесса выдачи.

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

Специально для него ISRG разработали протокол Automatic Certificate Management Environment (ACME). Сервис Let’s Encrypt (LE) был создан корпорацией Internet Security Research Group (ISRG). Сам по себе процесс получения сертификата представляет собой POST-запросы к сервису LE, где тело запроса представлено в виде JSON, обернутого в JSON Web Signature (JWS).

Шаги для получения выглядят так:

  • регистрация,
  • авторизация и получение способов подтверждения владения доменом,
  • подтверждение владения,
  • получение сертификата.

Начнем по-порядку.

Регистрация и авторизация пользователя

Для создания и авторизации пользователя нужна пара rsa ключей в pem-формате, которые впоследствии послужат основой для конструирования JWS.

openssl genrsa -out private.pem 2048

Структура данных POST-запроса для общения с ACME v01:

{ "header": jws, //JSON Web Signature "protected": Base64Url(jws + Replay-Nonce), //Nonce — защита от повторов "payload": Base64Url(payload), //Запрос "signature": Base64Url(sign(protected.payload, private.pem)) //Подпись
}

Здесь стоит заострить внимание на трех вещах. Во-первых, Replay-Nonce возвращается в хедерах ответа acme-v01.api.letsencrypt.org/directory. Во-вторых, Payload — это JSON, в котором вы объясняете, чего, собственно, хотите от ACME в данном конкретном случае. В-третьих, JWS представляет из себя JSON следующего вида (оговорюсь, что есть способы получения подписи другими алгоритмами. Здесь приведен всего один, возможно, простейший):


}

Встал вопрос, где брать данные для JWK. Недолгие поиски в интернете дали свои плоды, и я нашел простой способ посмотреть на саму пару pem-ключей в расшифрованном виде. Вот пример:

openssl rsa -text -noout < private.pem

вывод команды в максимально сокращенном виде:

Private-Key: (2048 bit)
modulus: 00:a8:c5:cc:9c:24:9b:d1:8d:9a:67:81:4d:1f:57: ... 8c:45:51:9e:26:fc:12:35:9e:a0:10:fd:80:94:cc: 09:a5
publicExponent: 65537 (0x10001)
privateExponent: ...
prime1: ...
prime2: ...
exponent1: ...
exponent2: ...
coefficient: ...

Вот и они, так нужные нам данные, бери — не хочу. Я взял, привел к нужному виду, создал JWS. Но меня ждало жестокое разочарование: подпись оказалась неправильной. Все это вылилось в несколько долгих часов поиска информации в интернете, отладки и безысходности. И все-таки ответ всплыл.

1, но есть другой способ получить модулус в готовом для обработки и вставки в JWS виде. Оказалось, что первые два нуля являются артефактами, появляющимися при кодировании целого числа средствами ASN.

openssl rsa -noout -modulus < private.pem

Вуаля:

Modulus=A8C5CC9C249BD18D9A67814D1F57...8C45519E26FC12359EA010FD8094CC09A5

Получение сертификата

Теперь давайте пройдемся по запросам и payload’ам. В первую очередь позовем GET для acme-v01.api.letsencrypt.org/directory. Из полученного JSON

{ "key-change": "https://acme-v01.api.letsencrypt.org/acme/key-change", "meta": { "terms-of-service": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf" }, "new-authz": "https://acme-v01.api.letsencrypt.org/acme/new-authz", "new-cert": "https://acme-v01.api.letsencrypt.org/acme/new-cert", "new-reg": "https://acme-v01.api.letsencrypt.org/acme/new-reg", "revoke-cert": "https://acme-v01.api.letsencrypt.org/acme/revoke-cert", "zH_Sr0qwmwM": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417"
}

возьмем адрес пользовательского соглашения и адреса для запросов сертификата.

Регистрация

url = directory["new-reg"]
payload = { "resource": "new-reg", "agreement": directory["meta"]["terms-of-service"]
}

Авторизация

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

url = directory["new-authz"]
payload = { "resource": "new-authz", "identifier": { "type":"dns", "value": "name" }
}

Проверки

Приступим к проверкам имен домена. Для версии ACME v01 доступно три способа: http, dns, tls. Наш выбор пал на первый способ, как самый простой и доступный. Суть проста: в директории домена должна быть создана поддиректория .well-known/acme-challenge, куда будет положен токен проверки — файлик с именем, указанным в проверке.

Base64Url(отпечаток_jwk) — это будет так называемый ключ авторизации. Сам токен должен содержать в себе строку имя_токена. Получить отпечаток легко можно с помощью OpenSSL командой:

echo jwk | openssl dgst -sha256 -binary | base64url

Боюсь, для bash вам придется написать функцию base64url самостоятельно.

Детские ошибки — самые страшные. Как бы легко это ни было, я умудрился застрять на несколько часов. Будьте внимательны к этим данным, отпечаток должен быть чистым” :). В openssl в конце JWK передавался символ переноса строки.

Тело запроса будет выглядеть так:

payload = { "resource": "challenges", "keyAuthorization": ключ авторизации
}

а url берем из JSON проверки.

Я написал хитрый механизм, который рассовывал токены в нужные узлы кластера и потом их удалял, что в последствии оказалось лишней работой (об этом позднее).

Выдача сертификата

url = directory["new-cert"]
payload = { "resource": "new-cert", "csr": csr
}

И, о чудо, первый сертификат LE был получен успешно!

Клиентоориентированность

Оставалось решить проблемы, которые ждали рядовых пользователей. Как обойти неизбежный синхронный выпуск сертификата, когда еще не резолвятся псевдонимы домена? Мы решили, что пользователь должен получить сертификат сразу же при заказе, но самоподписанный.

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

Вот и все. Готовый свеженький сертификат от LE остается подложить на место старого самоподписанного. Именно таким плагин интеграции с Let’s Encrypt увидел свет.

Трудности

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

Для всех созданных средствами панели веб-доменов, мы начали добавлять псевдоним /.well-known/acme-challenge/, ведущий в директорию /usr/local/mgr5/www/letsencrypt. Спустя несколько месяцев стало ясно, что механизм рассылки токенов по директориям доменов себя не оправдал. Именно в нее начали помещаться токены для проверки, что в дальнейшем свело ошибки доступа к минимуму.

Проверка через DNS

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

echo ключ_авторизации | openssl dgst -sha256 -binary | base64url

Вот и все на сегодня. Про переход плагина на ACME v02 и поддержку wildcard сертификатов читайте в следующем выпуске.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть