Sunday, June 26, 2016

Публикация CRL на внешнем хостинге

(Унылый апдейт от апреля 2017: ну всё, увы. SmartFile вслед за Дропбоксом ввели обязательный редирект на HTTPS, который нам противопоказан - см. внизу, почему. Придётся искать что-то другое.)

Частенько сталкиваюсь с типичной сисадминской задачкой: надо настроить CA и опубликовать в Интернете подписанный им список отозванных сертификатов (CRL). Это просто небольшой файлик, который надо сделать доступным по HTTP-линку, чтобы затем прописать этот линк в выдаваемых этим CA сертификатах. Любая сторона, желающая проверить, действителен ли ещё сертификат, найдёт в нём поле CDP, содержащее линк, скачает по нему файл, проверит на нём цифровую подпись и затем глянет, содержит ли CRL серийный номер сертификата in question, или нет.

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

Взамен хочется такого:
  • Чтобы можно было выкладывать CRL на каком-то внешнем хостинге, автоматически заливать туда изменения - желательно через какой-нибудь простенький REST-сервис.
  • Чтобы был постоянный линк на опубликованный файл, чтобы он не требовал никакой авторизации или сложных редиректов.
  • Чтобы этот линк содержал наш собственный домен - и для понтов, и для гибкости: чтобы не переделывать массу сертификатов только потому, что сменился хостинг.
  • Чтобы линк был доступен по plain HTTP - не по HTTPS! Что сегодня уже, кстати, нетривиально. Необходимо это потому, что иначе может возникнуть логическая петля: некто хочет проверить сертификат, поэтому идёт скачивать CRL, но для этого нужно проверить сертификат HTTPS-сервера, но для этого нужно скачать CRL, и так далее. Чтобы не допускать этой дурной бесконечности, стандарт требует публиковать CRL по обычному HTTP - что безопасно, поскольку сам CRL защищён цифровой подписью и имеет ограниченное время жизни. Поэтому подсунуть фальшивый не выйдет.
  • Ну и наконец, чтобы всё это удовольствие было на халяву.
Я малость поныкался и нашёл хостинг, всем пунктам удолетворяющий - SmartFile.
Они предоставляют бесплатный девелоперовский аккаунт, позволяющий держать какие-то бешенные гиги - для нашей задачки это более чем излишне, нам-то всего несколько килобайтиков залить. В отличие от платного, этот аккаунт не даёт использовать удобный веб-интерфейс - но зато есть прекрасный REST API. Им и воспользуемся.

Для этого API есть имплементации клиентской части на разных языках, но поскольку я адски ленив, то решил пользоваться обычным cURL - в моём случае, сборкой под Windows, поддерживающей SSL.

Процесс таков:

1. Регистрируемся и заходим в качестве девелопера.

2. Жмём на кнопку "Get API Key" и получаем две длинные строки, которые будут использоваться для аутентификации наших REST-запросов:
ACCESS_KEY = SeKyFDgmdA1ikWdnAuvrWxLX1do4q4
PASSWORD   = eSDBw3WcZskDSzs2Wu3wy4pixe8uIt

SmartFile использует Basic Authentication - схему, которая сама по себе не предоставляет шифрования пароля, но поскольку сами запросы идут по HTTPS, то проблемы в этом нет.

3. Итак, первый запрос - создаём папочку на хостинге. Назовём её, к примеру, PKI:
curl --user %ACCESS_KEY%:%PASSWORD% -F "path=PKI" -X PUT https://app.smartfile.com/api/2/path/oper/mkdir

4. Заливаем в неё наш CRL-файл:
curl --user %ACCESS_KEY%:%PASSWORD% -X POST -F path="@MyOwnCoolCA.crl" https://app.smartfile.com/api/2/path/data/PKI/

5. Заодно там же разместим и сертификат самого CA - его можно прописать в поле AIA выпекаемых им сертификатах, чтобы проверяющие могли подгрузить всю цепочку:
curl --user %ACCESS_KEY%:%PASSWORD% -X POST -F path="@MyOwnCoolCA.crt" https://app.smartfile.com/api/2/path/data/PKI/

6. Файлы залиты, но просто так их ещё не скачаешь - потребуется авторизация. Чтобы сделать их доступными urbi et orbi, надобно создать линк на содержащую их папку PKI:
curl --user %ACCESS_KEY%:%PASSWORD% -X POST -d "name=PKI&path=/PKI&usage_limit=1000000000&read=on&list=on" https://app.smartfile.com/api/2/link/

Параметр "usage_limit" задаёт, сколько раз файл может быть скачан по линку. К сожалению, мне не удалось выяснить, можно ли этот параметр выставить в бесконечность. Но для нашего маленького скромного CRL миллиарда закачек вполне хватит.

В ответ мы получаем линк вида http://file.ac/g743sRnJfgS. На этом работа с API закончена.

7. Осталось лишь прописать в DNS хостнейм, который мы хотим видеть в наших линках. Пускай это будет crl.mydomain.com. Можно создать его как CNAME-алиас для file.ac, или как A-запись для IP-адреса 209.43.40.101 - это не особо существенно.

Всё. На этом этапе у нас есть два живых линка:
  • http://crl.mydomain.com/g743sRnJfgS/MyOwnCoolCA.crl - собственно CRL,
  • http://crl.mydomain.com/g743sRnJfgS/MyOwnCoolCA.crt - сертификат подписавшего его CA.
Их уже можно взять и прописать в конфигурации CA, чтобы они фигурировали в полях CDP и AIA всех сертификатов, этим CA выданных.


Осталось лишь наладить обновления. Можно, конечно, просто регулярно заливать CRL каждые полчаса-час, но это путь какой-то тупой. Хочется, чтобы он заливался лишь тогда, когда CA подписывает новый выпуск - допустим, раз в неделю.

Дальше идёт уже конфигурация под определённую платформу - поскольку я юзаю Microsoft ADCS, то и дальнейшие штучки заточены под Windows. На Линуксе можно придумать что-то своё.

Итак, я хочу, чтобы всякий раз, как файл MyOwnCoolCA.crl меняется, автоматом исполнялся REST-запрос, отправляющий его содержание на хостинг. К счастью, нашёлся скрипт PowerShell, позволяющий добиться этого минимальным расходом ресурсов. Скрипт подписывается на события, происходящие с выбранными файлами в определённой папке, и запускает внешнюю програмку, когда они происходят:

UploadToSmartFile.ps1:
$ACCESS_KEY = "SeKyFDgmdA1ikWdnAuvrWxLX1do4q4"
$PASSWORD   = "eSDBw3WcZskDSzs2Wu3wy4pixe8uIt"

### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "C:\crl.mydomain.com"
$watcher.Filter = "MyOwnCoolCA.crl"
$watcher.EnableRaisingEvents = $true  

### DEFINE ACTIONS AFTER A EVENT IS DETECTED
$action = 

curl.exe --user ${ACCESS_KEY}:${PASSWORD} -X POST -F path="@$Event.SourceEventArgs.FullPath" https://app.smartfile.com/api/2/path/data/PKI/
}
  
### DECIDE WHICH EVENTS SHOULD BE WATCHED + SET CHECK FREQUENCY  
$created = Register-ObjectEvent $watcher "Created" -Action $action
$changed = Register-ObjectEvent $watcher "Changed" -Action $action
$renamed = Register-ObjectEvent $watcher "Renamed" -Action $action
while ($true) {sleep 5}

(Update: "curl.exe" - это важно; как неожиданно выяснилось, добрые люди из Микрософта определили в новых версиях Пауэршелла команду "curl" как alias на Invoke-WebRequest, поломав мне нафиг скрипт.)

Итак, то что делает скрипт: висит и ждёт, пока в директории C:\crl.mydomain.com будет либо создан файл MyOwnCoolCA.crl, либо изменён, либо какой-нибудь другой файл получит это название. В этом случае скрипт отправляет его содержимое на хостинг.

(N.B.: сама директория, как и скрипт, совершенно не обязана находиться непосредственно на CA - это может быть любой комп. Я обычно предпочитаю располагать её в папке SYSVOL - это гарантирует очень быструю репликацию на все контроллеры в домене. Дело вкуса).

Осталось отконфигурачить автоматическое исполнение скрипта при загрузке компьютера. Можно запускать его как сервис при помощи srvany.exe, но я воспользовался Task Scheduler.

Итак, создаём новый task со следующими параметрами:
  • Triggers: at startup
  • User account: SYSTEM
  • Action: powershell -ExecutionPolicy Bypass -File C:\CA\Script\UploadToSmartFile.ps1
и отменяем остановку скрипта через определённый период времени.
(Bypass требуется, поскольку по дефолту исполнение скриптов PowerShell запрещено).

Теперь после перезагрузки скрипт запустится автоматом и будет ждать, пока CA не подпишет новый CRL - после чего тот разом окажется доступен для клиентов по линку, прописанному в сертификатах.

Всё. Mischief managed.