OpenSSL провера валидности p12 сертификата

OpenSSL провера валидности p12 сертификата Сертификаты

Openssl — срок действия и продление корневого сертификата сертификационного центра

Сохранение одного и того же закрытого ключа в корневом ЦС позволяет всем сертификатам продолжать успешно проверять новый корень; все, что от вас требуется, – это доверять новому корню.

Отношение подписи сертификата основано на сигнатуре от закрытого ключа; сохраняя один и тот же секретный ключ (и, неявно, тот же самый открытый ключ) при создании нового публичного сертификата, с новым сроком действия и любыми другими новыми атрибутами, измененными по мере необходимости, сохраняет доверительные отношения на месте. CRL также могут переходить от старого сертификата к новому, как они есть, например, к сертификатам, подписанным закрытым ключом.


Итак, давайте проверим!

Сделать корневой ЦС:

openssl req -new -x509 -keyout root.key -out origroot.pem -days 3650 -nodes

Создайте из него дочерний сертификат:

openssl genrsa -out cert.key 1024
openssl req -new -key cert.key -out cert.csr

Подпишите дочерний сертификат:

openssl x509 -req -in cert.csr -CA origroot.pem -CAkey root.key -create_serial -out cert.pem
rm cert.csr

Все установлено, нормальное отношение сертификата. Давайте проверим доверие:

# openssl verify -CAfile origroot.pem -verbose cert.pem
cert.pem: OK

Итак, теперь, скажем, прошло 10 лет. Давайте сгенерируем новый открытый сертификат из одного и того же корневого закрытого ключа.

openssl req -new -key root.key -out newcsr.csr
openssl x509 -req -days 3650 -in newcsr.csr -signkey root.key -out newroot.pem
rm newcsr.csr

И .. это сработало?

# openssl verify -CAfile newroot.pem -verbose cert.pem
cert.pem: OK

Но почему? Они разные файлы, верно?

# sha1sum newroot.pem
62577e00309e5eacf210d0538cd79c3cdc834020  newroot.pem
# sha1sum origroot.pem
c1d65a6cdfa6fc0e0a800be5edd3ab3b603e1899  origroot.pem

Да, но это не означает, что новый открытый ключ не криптографически соответствует сигнатуре в сертификате. Различные серийные номера, одинаковый модуль:

# openssl x509 -noout -text -in origroot.pem
        Serial Number:
            c0:67:16:c0:8a:6b:59:1d
...
            RSA Public Key: (1024 bit)
                Modulus (1024 bit):
                    00:bd:56:b5:26:06:c1:f6:4c:f4:7c:14:2c:0d:dd:
                    3c:eb:8f:0a:c0:9d:d8:b4:8c:b5:d9:c7:87:4e:25:
                    8f:7c:92:4d:8f:b3:cc:e9:56:8d:db:f7:fd:d3:57:
                    1f:17:13:25:e7:3f:79:68:9f:b5:20:c9:ef:2f:3d:
                    4b:8d:23:fe:52:98:15:53:3a:91:e1:14:05:a7:7a:
                    9b:20:a9:b2:98:6e:67:36:04:dd:a6:cb:6c:3e:23:
                    6b:73:5b:f1:dd:9e:70:2b:f7:6e:bd:dc:d1:39:98:
                    1f:84:2a:ca:6c:ad:99:8a:fa:05:41:68:f8:e4:10:
                    d7:a3:66:0a:45:bd:0e:cd:9d
# openssl x509 -noout -text -in newroot.pem
        Serial Number:
            9a:a4:7b:e9:2b:0e:2c:32
...
            RSA Public Key: (1024 bit)
                Modulus (1024 bit):
                    00:bd:56:b5:26:06:c1:f6:4c:f4:7c:14:2c:0d:dd:
                    3c:eb:8f:0a:c0:9d:d8:b4:8c:b5:d9:c7:87:4e:25:
                    8f:7c:92:4d:8f:b3:cc:e9:56:8d:db:f7:fd:d3:57:
                    1f:17:13:25:e7:3f:79:68:9f:b5:20:c9:ef:2f:3d:
                    4b:8d:23:fe:52:98:15:53:3a:91:e1:14:05:a7:7a:
                    9b:20:a9:b2:98:6e:67:36:04:dd:a6:cb:6c:3e:23:
                    6b:73:5b:f1:dd:9e:70:2b:f7:6e:bd:dc:d1:39:98:
                    1f:84:2a:ca:6c:ad:99:8a:fa:05:41:68:f8:e4:10:
                    d7:a3:66:0a:45:bd:0e:cd:9d

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

Запустите экземпляр Apache и дайте ему перейти (структура файла debian, при необходимости отрегулируйте):

# cp cert.pem /etc/ssl/certs/
# cp origroot.pem /etc/ssl/certs/
# cp newroot.pem /etc/ssl/certs/
# cp cert.key /etc/ssl/private/

Мы установимэти директивы на VirtualHost прослушивании на 443 – помните, что корневой сертификат newroot.pem даже не существовал, когда был создан cert.pem и подписан.

SSLEngine on
SSLCertificateFile /etc/ssl/certs/cert.pem
SSLCertificateKeyFile /etc/ssl/private/cert.key
SSLCertificateChainFile /etc/ssl/certs/newroot.pem

Давайте посмотрим, как opensl видит это:

# openssl s_client -showcerts -CAfile newroot.pem -connect localhost:443

Certificate chain
 0 s:/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=server.lan
   i:/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=root
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
 1 s:/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=root
   i:/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=root
-----BEGIN CERTIFICATE-----
MIICHzCCAYgCCQCapHvpKw4sMjANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJB
...
-----END CERTIFICATE-----
(this should match the actual contents of newroot.pem)
...
Verify return code: 0 (ok)

Хорошо, а как насчет браузера с использованием криптографического API MS? Сначала нужно доверять корню, тогда все хорошо, с порядковым номером нового корня:

И мы все равно должны работать со старым корнем. Переключите конфигурацию Apache:

SSLEngine on
SSLCertificateFile /etc/ssl/certs/cert.pem
SSLCertificateKeyFile /etc/ssl/private/cert.key
SSLCertificateChainFile /etc/ssl/certs/origroot.pem

Сделайте полный перезапуск в Apache, перезагрузка не переключит сертификаты должным образом.

# openssl s_client -showcerts -CAfile origroot.pem -connect localhost:443

Certificate chain
 0 s:/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=server.lan
   i:/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=root
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
 1 s:/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=root
   i:/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=root
-----BEGIN CERTIFICATE-----
MIIC3jCCAkegAwIBAgIJAMBnFsCKa1kdMA0GCSqGSIb3DQEBBQUAMFQxCzAJBgNV
...
-----END CERTIFICATE-----
(this should match the actual contents of origroot.pem)
...
Verify return code: 0 (ok)

И с помощью браузера API Crypto API Apache представляет старый root, но новый root все еще находится в доверенном корневом хранилище компьютера. Он автоматически найдет его и проверит сертификат против доверенного (нового) корня, несмотря на то, что Apache представляет другую цепочку (старый корень). После удаления нового корня из доверенных корней и добавления исходного корневого сертификата все хорошо:

oldroot


Итак, вот и все! Храните тот же секретный ключ при обновлении, замените новый доверенный корень, и он почти все просто работает . Удачи!

Методологические рекомендации по использованию носителей ключевой информации (защищенных ключевых носителей)

К началу страницы

Экспортируемые и неэкспортируемые закрытые ключи

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

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

Для копирования закрытого ключа нарушителю потребуется получить физический доступ к ключевому носителю и узнать пароль (PIN-код).

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

Неэкспортируемые закрытые ключи обладают большей защищенностью, так как записанный на ключевой носитель закрытый ключ не подлежит копированию при помощи стандартных СКЗИ. Получение доступа к такому ключу требует применения специальных средств и техники.

Извлекаемые и неизвлекаемые закрытые ключи

Свойство неизвлекаемости закрытого ключа достигается способом его создания и хранения, и напрямую зависит от вида ключевого носителя. Для обеспечения свойства неизвлекаемости закрытого ключа используются только активные ключевые носители, содержащие в себе аппаратно реализованные функции СКЗИ, при использовании которых создается и используется неизвлекаемый закрытый ключ.

Для некоторых ключевых носителей существует возможность записи закрытого ключа на активные ключевые носители сторонними СКЗИ (установленными локально на компьютерное устройство или непосредственно в информационной системе удостоверяющего центра) и, в таком случае, такой носитель применяется как пассивный ключевой носитель, который может обеспечить только свойство неэкспортируемости закрытого ключа.

К извлекаемым закрытым ключам относятся все виды закрытых ключей, за исключением неизвлекаемых, включая экспортируемые и неэкспортируемые.

Пассивный ключевой носитель

Виды реализации: носитель с USB интерфейсом, носитель с бесконтактным интерфейсом (NFC интерфейс), смарт-карта.

Для доступа к защищенному содержимому данного ключевого носителя необходимо ввести пароль (PIN-код). Закрытый ключ хранится в ключевом контейнере на ключевом носителе. Пароль (PIN-код), которым защищён от доступа закрытый ключ на таком носителе, при получении следует изменить, обеспечить его надежное хранение и исключить доступ к паролю любых лиц.

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

На ключевом носителе установлено ограничение попыток неправильного ввода пароля (PIN-кода) и при превышении такого лимита ключевой носитель блокируется. Несмотря на это пассивный ключевой носитель обладает средним уровнем защищенности от атак злоумышленников – в момент подписания документа образуется короткий промежуток времени, когда закрытый ключ находится в памяти компьютерного устройства, где существует возможность его перехвата злоумышленником с высоким уровнем технических знаний и/или с использованием специальных технических средств. Для исключения такого вида атак существует активный ключевой носитель.

Про сертификаты:  Набор для демеркуризации

Активный ключевой носитель (криптографический ключевой носитель)

Виды реализации: носитель с USB интерфейсом, носитель с бесконтактным интерфейсом (NFC интерфейс), смарт-карта.

Активный ключевой носитель содержит в себе функции СКЗИ. Закрытый ключ на таком ключевом носителе хранится в защищенном ключевом контейнере и в специальном внутреннем формате.

У такого носителя существует ряд технических преимуществ перед пассивным ключевым носителем:

  1. создание закрытого ключа происходит на самом носителе с использованием аппаратных криптографических функций ключевого носителя;
  2. при подписании электронного документа закрытый ключ не копируется в память или реестр компьютерного устройства – подписание электронного документа происходит на самом ключевом носителе;
  3. закрытый ключ ни в какой момент времени не покидает ключевой носитель.

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

Компрометация закрытого ключа на таком носителе возможна только в случае его хищения вместе с паролем (PIN-кодом).

Активный ключевой носитель (криптографический ключевой носитель) обладает высоким уровнем защищенности от атак злоумышленников. Риск атаки «подмена хэша» злоумышленником присутствует, но такие случаи крайне редки.

Технические меры предосторожности

При выборе вида ключевого носителя для хранения закрытого ключа электронной подписи следует учитывать, что электронная подпись считается равнозначной собственноручной подписи в случаях, установленных Федеральным законом от 06.04.2021 № 63-ФЗ «Об электронной подписи». Рекомендуется использовать ключевые носители с наивысшей степенью защиты закрытого ключа.

Рекомендуется сменить пароль доступа к ключевому носителю (PIN-код), установленный его изготовителем, на уникальный – известный только владельцу электронной подписи. Рекомендуемая длина пароля – не менее 6 символов с использованием специальных символов, прописных и строчных латинских букв. Рекомендуется периодическая смена пароля.

Не рекомендуется при выборе пароля основываться на типовых шаблонах и идущих подряд на клавиатуре или алфавите символов (qwerty, abcde, 12345 и другие) на каком-либо идентификаторе, паспортных данных, кличек питомцев и подобных ассоциаций.

Не рекомендуется активировать функцию «запомнить пароль» в средствах электронной подписи и настройках программного обеспечения, которое необходимо для использования ключевого носителя.

Организационные меры предосторожности

Не рекомендуется в рамках организации процедур безопасной работы с ключевым носителем:

  • передавать ключевой носитель третьим лицам;
  • записывать пароль доступа к ключевому носителю (PIN-код) на бумаге или непосредственно на ключевом носителе, запоминать пароли в реестровой памяти систем электронных устройств и хранить парольную информацию в общедоступных местах;
  • оставлять ключевой носитель без присмотра в доступных или общественных местах;
  • оставлять без присмотра ключевой носитель в компьютерном устройстве, на котором осуществляется подписание электронных документов (usb-порты в системном блоке компьютера, ноутбука, планшета или других электронных устройствах).

Рекомендуется в рамках организации процедур безопасной работы с ключевым носителем:

  • при необходимости, обеспечивать сотрудников организации, не имеющих права действовать без доверенности, их персональными закрытыми ключами и сертификатами электронной подписи, с наделением их правом подписи распорядительными документами организации путем оформления доверенности;
  • хранить ключевой носитель в недоступном для третьих лиц месте;

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

Мониторинг срока действия сертификатов в windows на netxms

Недавно у нас встала задача мониторинга срока действия сертификатов на серверах с Windows. Ну как встала, после того, как сертификаты несколько раз превращались в тыкву, в то самое время, когда ответственный за их продление бородатый коллега находился в отпуске. После этого мы с ним

что-то заподозрили

решили над этим задуматься. Так как у нас не спеша внедряется система мониторинга NetXMS, она и стала главным и, в принципе, единственным кандидатом на эту задачу.

Результат в итоге был получен в таком виде:

OpenSSL провера валидности p12 сертификата

А сам процесс далее.

Поехали. Встроенного счетчика заканчивающегося срока сертификатов в NetXMS нет, поэтому нужно создавать свой и использовать скрипты для обеспечения его данными. Конечно, на Powershell, это же Windows. Скрипт должен прочитать все сертификаты в операционной системе, взять оттуда срок их истечения в днях и передать это число в NetXMS. Через его агента. Вот с него и начнем.

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

Чтобы сервер NetXMS узнал о существовании нашего кастомного параметра, он должен получить его от агента. Иначе этот параметр невозможно будет добавить, по причине его отсутствия. Поэтому, в конфигурационный файл агента nxagentd.conf мы добавляем строку внешнего параметра с именем HTTPS.CertificateExpireDateSimple, в которой прописываем запуск скрипта:

ExternalParameter = HTTPS.CertificateExpireDateSimple: powershell.exe -File "\servershareNetXMS_CertExpireDateSimple.ps1"

Учитывая, что скрипт запускается по сети, нужно не забыть про

Execution Policy

, а так же не забыть прочие “-NoLogo -NoProfile -NonInteractive”, которые я опустил для лучшей читаемости кода.

В результате чего конфиг агента выглядит примерно так:

#
# NetXMS agent configuration file
# Created by agent installer at Thu Jun 13 11:24:43 2021
#
 
MasterServers = netxms.corp.testcompany.ru
ConfigIncludeDir = C:NetXMSetcnxagentd.conf.d
LogFile = {syslog}
FileStore = C:NetXMSvar
SubAgent = ecs.nsm
SubAgent = filemgr.nsm
SubAgent = ping.nsm
SubAgent = logwatch.nsm
SubAgent = portcheck.nsm
SubAgent = winperf.nsm
SubAgent = wmi.nsm
 
ExternalParameter = HTTPS.CertificateExpireDateSimple: powershell.exe -File "\servershareNetXMS_CertExpireDateSimple.ps1"

После этого нужно сохранить конфиг и перезапустить агента. Можно это сделать из консоли NetXMS: открыть конфиг (Edit agent’s confuguration file), отредактировать, выполнить Save&Apply, в результате чего, собственно, произойдет то же самое. Потом перечитать конфигурацию (Poll > Configuration), если совсем нет сил подождать. После этих действий должна появиться возможность добавить наш кастомный параметр.

В консоли NetXMS идем в Data Collection Configuration подопытного сервера, на котором мы собрались мониторить сертификаты и создаем там новый параметр (в дальнейшем, после настройки, есть смысл перенести его в шаблоны). Выбираем HTTPS.CertificateExpireDateSimple из списка, вписываем Description с понятным именем, тип ставим Integer и настраиваем интервал опроса. Слишком коротким его нет смысла делать, чтобы не забивать базу лишней информацией, слишком длинным неудобно будет ждать при проверке. Для сертификатов я обычно ставлю 600 секунд. На время отладки есть смысл сделать его покороче, 30 секунд, например:

OpenSSL провера валидности p12 сертификата

Всё, готово, пока достаточно. Можно проверять… нет, еще рано. Сейчас, естественно, ничего мы не получим. Просто потому, что скрипт еще не написали. Исправляем это упущение. Скрипт будет выдавать просто цифру, количество дней, оставшихся до окончания срока действия сертификата. Самую минимальную из всех имеющихся. Пример скрипта:

try {
    # Получаем все сертификаты из хранилища сертификатов
    $lmCertificates = @( Get-ChildItem -Recurse -path 'Cert:LocalMachineMy' -ErrorAction Stop )
     
    # Если сертификатов нет, вернуть "10 лет"
    if ($lmCertificates.Count -eq 0) { return 3650 }
 
    # Получаем Expiration Date всех сертификатов
    $expirationDates = @( $lmCertificates | ForEach-Object { return $_.NotAfter } )
 
    # Получаем наиболее близкий Expiration Date из всех
    $minExpirationDate = ($expirationDates | Measure-Object -Minimum -ErrorAction Stop ).Minimum
 
    # Конвертируем наиболее близкий Expiration Date в количество оставшихся дней с округлением в меньшую сторону
    $daysLeft = [Math]::Floor( ($minExpirationDate - [DateTime]::Now).TotalDays )
 
    # Возвращаем значение
    return $daysLeft
}
catch {
    return -1
}

Получается так:

Про сертификаты:  Буран 0.3

OpenSSL провера валидности p12 сертификата

723 дня, до истечения срока действия сертификата еще почти два года. Логично, потому что сертификаты на Exchange тестового стенда я перевыписывал совсем недавно.

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

Второй вариант, несколько посложнее.

Опять редактируем конфиг агента и там вместо строчки с ExternalParameter пишем две другие:

ExternalList = HTTPS.CertificateNames: powershell.exe -File "\servershareNetXMS_CertNames.ps1"
ExternalParameter = HTTPS.CertificateExpireDate(*): powershell.exe -File "\servershareNetXMS_CertExpireDate.ps1" -CertificateId "$1"

В

ExternalList

мы получаем просто список строк. В нашем случае, список строк с именами сертификатов. Список этих строк мы получим скриптом. Имя списка —

HTTPS.CertificateNames

.

Скрипт NetXMS_CertNames.ps1:

#Список возможных имен сертификатов
$nameTypeList = @(
        [System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName,
        [System.Security.Cryptography.X509Certificates.X509NameType]::DnsName,
        [System.Security.Cryptography.X509Certificates.X509NameType]::DnsFromAlternativeName,
        [System.Security.Cryptography.X509Certificates.X509NameType]::UrlName,
        [System.Security.Cryptography.X509Certificates.X509NameType]::EmailName,
        [System.Security.Cryptography.X509Certificates.X509NameType]::UpnName
)
 
#Ищем все сертификаты, имеющие закрытый ключ
$certList = @( Get-ChildItem -Path 'Cert:LocalMachineMy' | Where-Object { $_.HasPrivateKey -eq $true } )
 
#Проходим по списку сертификатов, формируем строку "Имя сертификата - Дата - Thumbprint" и возвращаем её
foreach ($cert in $certList) {
    $name = '(unknown name)'
    try {
        $thumbprint = $cert.Thumbprint
        $dateExpire = $cert.NotAfter
        foreach ($nameType in $nameTypeList) {
            $name_temp = $cert.GetNameInfo( $nameType, $false)
            if ($name_temp -ne $null -and $name_temp -ne '') {
                $name = $name_temp;
                break;
            }
        }
        Write-Output "$($name) - $($dateExpire.ToString('dd.MM.yyyy')) - [T:$($thumbprint)]"
    }
    catch {
        Write-Error -Message "Error processing certificate list: $($_.Exception.Message)"
    }
}

А уже в

ExternalParameter

мы на вход подаем строки из списка ExternalList, а на выходе получим всё то же количество дней для каждой. Идентификатором служит Thumbprint сертификата. Обратите внимание, что HTTPS.CertificateExpireDate в этом варианте содержит звездочку (*). Это необходимо для того, чтобы он принимал внешние переменные, как раз наш CertificateId.

Скрипт NetXMS_CertExpireDate.ps1:

#Определяем входящий параметр $CertificateId
param (
    [Parameter(Mandatory=$false)]
    [String]$CertificateId
)
 
#Проверка на существование
if ($CertificateId -eq $null) {
    Write-Error -Message "CertificateID parameter is required!"
    return
}
 
#По Thumbprint из строки в $CertificateId ищем сертификат и определяем его Expiration Date 
$certId = $CertificateId;
try {
    if ($certId -match '^.*[T:(?<Thumbprint>[A-Z0-9] )]$') {
        $thumbprint = $Matches['Thumbprint']
        $certificatePath = "Cert:LocalMachineMy$($thumbprint)"
         
        if (Test-Path -PathType Leaf -Path $certificatePath ) {
            $certificate = Get-Item -Path $certificatePath;
            $certificateExpirationDate = $certificate.NotAfter
            $certificateDayToLive = [Math]::Floor( ($certificateExpirationDate - [DateTime]::Now).TotalDays )
            Write-Output "$($certificateDayToLive)";
        }
        else {
            Write-Error -Message "No certificate matching this thumbprint found on this server $($certId)"
        }
    }
    else {
        Write-Error -Message "CertificateID provided in wrong format. Must be FriendlyName [T:<thumbprint>]"
    }
}
catch {
    Write-Error -Message "Error while executing script: $($_.Exception.Message)"
}

В Data Collection Configuration сервера создаем новый параметр. В Parameter выбираем наш

HTTPS.CertificateExpireDate(*)

из списка, и, (внимание!) меняем звездочку на

{instance}

. Этот важный момент позволит создать отдельный счетчик для каждого инстанса (сертификата). Остальное заполняется, как в предыдущем варианте:

OpenSSL провера валидности p12 сертификата

Для того, чтобы счетчики было из чего создавать, на вкладке Instance Discovery нужно выбрать Agent List из списка и в поле List Name вписать имя нашего ExternalList из скрипта — HTTPS.CertificateNames.

Почти готово, немного подождать или принудительно сделать Poll > Configuration и Poll > Instance Discovery, если совсем невозможно ждать. В результате получаем все наши сертификаты со сроками действия:

OpenSSL провера валидности p12 сертификата

То, что надо? Ну да, только червячок перфекционизма смотрит на этот ненужный Thumbprint в имени счетчика печальными глазами и никак не дает закончить статью. Чтобы его накормить, снова открываем свойства счетчика и на вкладке Instance Discovery в поле «Instance discovery filter script» добавляем написанный на NXSL (внутреннем языке NetXMS) скрипт:

instance = $1;
 if (instance ~= "^(.*)s-s[T:[a-zA-Z0-9] ]$")
 {
 return %(true, instance, $1);
 }
 return true;

который будет отфильтровывать Thumbprint:

OpenSSL провера валидности p12 сертификата

А чтобы его отобразить отфильтрованным, на вкладке General в поле Description меняем CertificateExpireDate: {instance} на CertificateExpireDate: {instance-name}:

OpenSSL провера валидности p12 сертификата

Всё, наконец-то финиш из КДПВ:

OpenSSL провера валидности p12 сертификата

Красота же?

Осталось настроить оповещения, чтобы они приходили на почту, когда срок сертификата подходит к своему логическому концу.

1. Сначала нужно создать шаблон события (Event Template) для активации его при уменьшении значения счетчика до какого-то заданного нами порога. В Event Configuration создаем два новых шаблона с именами, скажем CertificateExpireDate_Threshold_Activate со статусом Warning:

OpenSSL провера валидности p12 сертификата

и аналогичный CertificateExpireDate_Threshold_Deactivate со статусом Normal.

2. Дальше идем в свойства счетчика и на вкладке Tresholds настраиваем порог:

OpenSSL провера валидности p12 сертификата

где выбираем наши созданные эвенты CertificateExpireDate_Threshold_Activate и CertificateExpireDate_Threshold_Deactivate, ставим количество замеров (Samples) 1 (конкретно для этого счетчика больше ставить смысла нет), значение в 30 (дней), к примеру, и, что важно, настраиваем время повторения эвента. Для сертификатов в продакшене я ставлю раз в сутки (86400 секунд), иначе можно утонуть в оповещениях (что, кстати и случилось один раз, причем так, что переполнился почтовый ящик за выходные). На время отладки есть смысл поставить поменьше, 60 секунд, к примеру.

3. В Action Configuration создаем шаблон письма оповещения, типа такого:

OpenSSL провера валидности p12 сертификата

Все эти %m, %S и т.п. — макросы, в которые будут подставлены значения из нашего параметра. Подробнее они описаны в мануале NetXMS.

4. И, наконец, объединяя предыдущие пункты, в Event Processing Policy создаем правило, по которому будет создаваться Alarm и отправляться письмо:

OpenSSL провера валидности p12 сертификата

Сохраняем политику, всё, можно тестировать. Поставим порог повыше, для проверки. У меня ближайший сертификат истекает через 723 дня, для проверки поставил 724. В результате получим такой аларм:

OpenSSL провера валидности p12 сертификата

и такое оповещение по почте:

OpenSSL провера валидности p12 сертификата

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

Откуда берутся сертификаты?

Еще совсем недавно было всего 2 способа заполучить X.509 сертификат, но времена меняются и с недавнего времени есть и третий путь.

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

    not trusted

  2. Приобрести сертификат в УЦ. Это будет стоить денег в зависимости от различных его характеристик и возможностей, указанных выше.
  3. Получить бесплатный сертификат LetsEncrypt, доступны только самые простые DV сертификаты.

Для первого сценария достаточно пары команд и чтобы 2 раза не вставать создадим сертификат с алгоритмом эллиптических кривых. Первым шагом нужно создать закрытый ключ. Считается, что шифрование с алгоритмом эллиптических кривых дает больший выхлоп, если измерять в тактах CPU, либо байтах длины ключа. Поддержка ECC не определена однозначно в TLS < 1.2.

openssl ecparam -name secp521r1 -genkey -param_enc explicit -out private-key.pem

Далее, создает CSR — запрос на подписание сертификата.

openssl req -new -sha256 -key private.key -out server.csr -days 730

И подписываем.

openssl x509 -req -sha256 -days 365 -in server.csr -signkey private.key -out public.crt

Результат можно посмотреть командой:

openssl x509 -text -noout -in public.crt

Openssl имеет огромное количество опций и команд. Man страница не очень полезна, справочник удобнее использовать так:

openssl -help
openssl x509 -help
openssl s_client -help

Ровно то же самое можно сделать с помощью java утилиты keytool.

keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -storepass password -validity 360 -keysize 2048

Следует серия вопросов, чтобы было чем запомнить поля owner и issuer

What is your first and last name?
What is the name of your organizational unit?
What is the name of your organization?
What is the name of your City or Locality?
What is the name of your State or Province?
What is the two-letter country code for this unit?
Is CN=Johnnie Walker, OU=Unknown, O=Unknown, L=Moscow, ST=Moscow, C=RU correct?

Конвертируем связку ключей из проприетарного формата в PKCS12.

keytool -importkeystore -srckeystore keystore.jks -destkeystore keystore.jks -deststoretype pkcs12

Смотрим на результат:

keytool -list -v -alias selfsigned -storepass password -keystore keystore.jks
Alias name: selfsigned
Creation date: 20.01.2021
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=Johnnie Walker, OU=Unknown, O=Unknown, L=Moscow, ST=Moscow, C=RU
Issuer: CN=Johnnie Walker, OU=Unknown, O=Unknown, L=Moscow, ST=Moscow, C=RU
Serial number: 1f170cb9
Valid from: Sat Jan 20 18:33:42 MSK 2021 until: Tue Jan 15 18:33:42 MSK 2021
Certificate fingerprints:
     MD5:  B3:E9:92:87:13:71:2D:36:60:AD:B5:1F:24:16:51:05
     SHA1: 26:08:39:19:31:53:C5:43:1E:ED:2E:78:36:43:54:9B:EA:D4:EF:9A
     SHA256: FD:42:C9:6D:F6:2A:F1:A3:BC:24:EA:34:DC:12:02:69:86:39:F1:FC:1B:64:07:FD:E1:02:57:64:D1:55:02:3D

Signature algorithm name: SHA256withRSA
Subject Public Key Algorithm: 2048-bit RSA key
Version: 3

Extensions:

#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 30 95 58 E3 9E 76 1D FB   92 44 9D 95 47 94 E4 97  0.X..v...D..G...
0010: C8 1E F1 92                                        ....
]
]

Значению ObjectId: 2.5.29.14 соответствует определение ASN.1, согласно RFC 3280 оно всегда non-critical. Точно так же можно узнать смысл и возможные значения других ObjectId, которые присутствуют в сертификате X.509.

subjectKeyIdentifier EXTENSION ::= {
    SYNTAX SubjectKeyIdentifier
    IDENTIFIED BY id-ce-subjectKeyIdentifier
}

SubjectKeyIdentifier ::= KeyIdentifier

Отслеживание окончания доменов и ssl-сертификатов

При работе с множеством сайтов возникает необходимость в постоянном контроле срока окончания доменов и особенно сертификатов т.к. в основном используются 90-дневненые «Let’s Encrypt», и к тому же на многих хостингах нет автоматического продления.

Про сертификаты:  Должен ли корневой сертификат проходить проверку состояния отзыва? — Хабр Q&A

Для этих целей был написан PHP-скрипт для мониторинга (скачать с GitHub), который помещаются в родительскую категорию сайта и состоит из следующих файлов:

monitoring/
├── config.php
├── cron.php
├── chache.json
├── calendar.php

Далее подробнее о каждом файле:

Конфигурационный файл, содержит настройки локали PHP, e-mail для уведомлений и списки проверяемых доменов в виде массивов.

<?php
// Ошибки PHP.
//error_reporting(E_ALL);
//ini_set('display_errors', 1); 

// Локаль.
setlocale(LC_ALL, 'ru_RU.utf8');
date_default_timezone_set('Europe/Moscow');
header('Content-type: text/html; charset=utf-8');
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');
mb_http_output('UTF-8');
mb_language('uni');

// E-mail для уведомлений.
$email = 'mail@example.com';

// За сколько отправлять уведомление.
$warn = 259200; // 3 дня 

$domains = array(
	'php.ru',
	'php.su',
	'php.net',
	'habr.com',
	'wikipedia.org',
);

$certificates = array(
	'php.ru',
	'php.su',
	'php.net',
	'habr.com',
	'wikipedia.org',
);

PHP-обработчик, получает даты окончания делегирования доменов через серверы whois и сроки действия SSL-сертификатов, далее сохраняет полученные даты в файл chache.json. В случаи ошибки или приближающейся даты окончания отправит сообщение на почту.

<?php
require_once __DIR__ . '/config.php';

$chache = array();
$mail_text = array();

// Проверка доменов
foreach ($domains as $domain) {
	$date = 0;
	$zone = explode('.', $domain);
	$zone = end($zone);

	switch ($zone) { 
		case 'ru': 
		case 'su': 
		case 'рф': $server = 'whois.tcinet.ru'; break;					
		case 'com':		
		case 'net': $server = 'whois.verisign-grs.com'; break;					
		case 'org': $server = 'whois.pir.org'; break;					
	}

	$socket = fsockopen($server, 43);
	if ($socket) {
		fputs($socket, $domain . PHP_EOL);
		while (!feof($socket)) {
			$res = fgets($socket, 128);
			if (mb_stripos($res, 'paid-till:') !== false) {
				$date = explode('paid-till:', $res);
				$date = strtotime(trim($date[1]));
				break;
			}
			if (mb_stripos($res, 'Registry Expiry Date:') !== false) {
				$date = explode('Registry Expiry Date:', $res);
				$date = strtotime(trim($date[1]));
				break;
			}	
		}
		fclose($socket);
	}
	
	if (!empty($date) && time()   $warn > $date) {
		$mail_text[] = $domain . ' - заканчивается ' . date('d.m.Y H:i', $date);
	} elseif (empty($date)) {
		$mail_text[] = $domain . ' - не удалось получить whois';
	} 

	$chache['domains'][$domain] = $date;
}

// Проверка SSL-сертификатов
foreach ($certificates as $domain) {
	$date = 0;
	$url = 'ssl://' . $domain . ':443';
	$context = stream_context_create(
		array(
			'ssl' => array(
				'capture_peer_cert' => true,
				'verify_peer' => false,
				'verify_peer_name' => false
			)
		)
	);

	$fp = @stream_socket_client($url, $err_no, $err_str, 30, STREAM_CLIENT_CONNECT, $context);
	$cert = @stream_context_get_params($fp);
 
	if (empty($err_no)) {
		$info = openssl_x509_parse($cert['options']['ssl']['peer_certificate']);
		$date = $info['validTo_time_t'];
	}
	
	if (!empty($date) && time()   $warn > $date) {
		$mail_text[] = $domain . ' - заканчивается сертификат ' . date('d.m.Y H:i', $date);
	} elseif (empty($date)) {
		$mail_text[] = $domain . ' - не удалось получить сертификат';
	}
	
	$chache['certificates'][$domain] = $date;
}

// Сохранение в файл.
file_put_contents(__DIR__ . '/chache.json', json_encode($chache));

// Вывод данных в браузер.
echo '<pre>' . print_r($chache, true) . '</pre>';

// Отправка уведомления.
if (!empty($mail_text)) {
	mb_send_mail(
		$email,
		'Мониторинг срока действия доменов и SSL-сертификатов', 
		implode('<br>', $mail_text), 
		"MIME-Version: 1.0rnContent-Type: text/html;"
	);
}

Whois-сервер для других доменных зон можно получить здесь.

Данный скрипт должен запускаться по крону, например раз в день, в 9 утра:

0	9	*	*	*	/usr/local/bin/wget -O - -q "https://example.com/monitoring/cron.php"

Письмо уведомление будет примерно следующего вида:

* Чтобы письма не попадали в спам, лучше сделать отправку писем через SMTP.

Скрипт носит чисто информационный характер и выводит календарь с выделенными датами окончания сертификатов и доменов. Выполняется прямым вызовом https://ваш_домен/monitoring/calendar.php.

<?php
require_once __DIR__ . '/config.php';

class Calendar 
{
	/**
	 * Вывод календаря на один месяц.
	 */
	public static function  getMonth($month, $year, $events = array())
	{
		$months = array(
			1  => 'Январь',
			2  => 'Февраль',
			3  => 'Март',
			4  => 'Апрель',
			5  => 'Май',
			6  => 'Июнь',
			7  => 'Июль',
			8  => 'Август',
			9  => 'Сентябрь',
			10 => 'Октябрь',
			11 => 'Ноябрь',
			12 => 'Декабрь'
		);
 
		$month = intval($month);
		$out = '
		<div class="calendar-item">
			<div class="calendar-head">' . $months[$month] . ' ' . $year . '</div>
			<table>
				<tr>
					<th>Пн</th>
					<th>Вт</th>
					<th>Ср</th>
					<th>Чт</th>
					<th>Пт</th>
					<th>Сб</th>
					<th>Вс</th>
				</tr>';
 
		$day_week = date('N', mktime(0, 0, 0, $month, 1, $year));
		$day_week--;
 
		$out.= '<tr>';
 
		for ($x = 0; $x < $day_week; $x  ) {
			$out.= '<td></td>';
		}
 
		$days_counter = 0;		
		$days_month = date('t', mktime(0, 0, 0, $month, 1, $year));
	
		for ($day = 1; $day <= $days_month; $day  ) {
			if (date('j.n.Y') == $day . '.' . $month . '.' . $year) {
				$class = 'today';
			} elseif (time() > strtotime($day . '.' . $month . '.' . $year)) {
				$class = 'last';
			} else {
				$class = '';
			}
			
			$event_show = false;
			$event_text = array();
			if (!empty($events)) {
				foreach ($events as $date => $text) {
					$date = explode('.', $date);
					if (count($date) == 3) {
						$y = explode(' ', $date[2]);
						if (count($y) == 2) {
							$date[2] = $y[0];
						}

						if ($day == intval($date[0]) && $month == intval($date[1]) && $year == $date[2]) {
							$event_show = true;
							$event_text[] = $text;
						}
					} elseif (count($date) == 2) {
						if ($day == intval($date[0]) && $month == intval($date[1])) {
							$event_show = true;
							$event_text[] = $text;
						}
					} elseif ($day == intval($date[0])) {
						$event_show = true;
						$event_text[] = $text;
					}				
				}
			}
			
			if ($event_show) {
				$out.= '<td class="calendar-day ' . $class . ' event">' . $day;
				if (!empty($event_text)) {
					$out.= '<div class="calendar-popup">' . implode('<br>', $event_text) . '</div>';
					}
				$out.= '</td>';
			} else {
				$out.= '<td class="calendar-day ' . $class . '">' . $day . '</td>';
			}
 
			if ($day_week == 6) {
				$out.= '</tr>';
				if (($days_counter   1) != $days_month) {
					$out.= '<tr>';
				}
				$day_week = -1;
			}
 
			$day_week  ; 
			$days_counter  ;
		}
 
		$out .= '</tr></table></div>';
		return $out;
	}
	
	/**
	 * Вывод календаря на несколько месяцев.
	 */
	public static function  getInterval($start, $end, $events = array())
	{
		$curent = explode('.', $start);
		$curent[0] = intval($curent[0]);
		
		$end = explode('.', $end);
		$end[0] = intval($end[0]);
 
		$begin = true;
		$out = '<div class="calendar-wrp">';
		do {
			$out .= self::getMonth($curent[0], $curent[1], $events);
 
			if ($curent[0] == $end[0] && $curent[1] == $end[1]) {
				$begin = false;
			}		
 
			$curent[0]  ;
			if ($curent[0] == 13) {
				$curent[0] = 1;
				$curent[1]  ;
			}
		} while ($begin == true);	
		
		$out .= '</div>';
		return $out;
	}
}

?><!DOCTYPE html>
<html lang="ru">
<head>
	<meta http-equiv="content-type" content="text/html; charset=utf-8">
	<title>Мониторинг срока действия доменов и SSL-сертификатов</title>
	<style type="text/css">
	body {font: 14px/1.2 Arial, sans-serif;}
	.wrapper {width: 940px;padding: 15px;margin: 0 auto;}
	.errors {margin-bottom: 20px;border: 1px solid red;padding: 15px;background: #fff4f4;color: red;}

	/* Стили календаря */
	.calendar-item {width: 200px;display: inline-block;vertical-align: top;margin: 0 16px 20px;}
	.calendar-head {text-align: center;padding: 5px;font-weight: 700;font-size: 14px;}
	.calendar-item table {border-collapse: collapse;width: 100%;}
	.calendar-item th {font-size: 12px;padding: 6px 7px;text-align: center;color: #888;font-weight: normal;}
	.calendar-item td {font-size: 13px;padding: 6px 5px;text-align: center;border: 1px solid #ddd;}
	.calendar-item tr th:nth-child(6), .calendar-item tr th:nth-child(7),
	.calendar-item tr td:nth-child(6), .calendar-item tr td:nth-child(7)  {color: #e65a5a;}	
	.calendar-day.last {color: #999 !important;}	
	.calendar-day.today {font-weight: bold;}
	.calendar-day.event {background: #ffadad;position: relative;cursor: pointer;}
	.calendar-day.event:hover .calendar-popup {display: block;}
	.calendar-popup {display: none;position: absolute;top: 40px;left: 0;min-width: 200px;padding: 15px;background: #fff;text-align: left;font-size: 13px;z-index: 100;box-shadow: 0 0 10px rgba(0,0,0,0.5);color: #000;}
	.calendar-popup:before {content: ""; border: solid transparent;position: absolute;left: 8px;bottom: 100%;border-bottom-color: #fff;border-width: 9px;margin-left: 0;}
	</style>
</head>
<body>
<div class="wrapper">
	<h1>Мониторинг доменов и SSL-сертификатов</h1>
	
	<?php
	// Загружаем данные из кеша.
	$data = json_decode(file_get_contents(__DIR__ . '/chache.json'), true);
	if (empty($data['domains']) && empty($data['certificates'])) {
		echo '<div class="errors">Нет данных для отображения</div>';
	} else {
		// Формируем данные для календаря
		$events = array();
		$date_end = 0;

		if (!empty($data['domains'])) {
			foreach ($data['domains'] as $i => $row) {
				if (empty($row)) {
					$errors[] = 'Не удалось проверить домен ' . $i;
				} else {
					$events[date('d.m.Y H:i', $row)] = 'Заканчивается домен ' . $i;
					if ($row > $date_end) {
						$date_end = $row;
					}
				}
			}
		}
		
		if (!empty($data['certificates'])) {
			foreach ($data['certificates'] as $i => $row) {
				if (empty($row)) {
					$errors[] = 'Не удалось проверить сертификат ' . $i;
				} else {
					$events[date('d.m.Y H:i', $row)] = 'Заканчивается сертификат ' . $i;
					if ($row > $date_end) {
						$date_end = $row;
					}
				}
			}
		}

		// Вывод ошибок.
		if (!empty($errors)) {
			echo '<div class="errors">' . implode('<br>', $errors) . '</div>';
		}
		
		// Вывод календаря.
		if (!empty($date_end)) {
			echo Calendar::getInterval(date('m.Y'), date('m.Y', $date_end), $events);
		}
	}
	?>	
</div>
</body>
</html>

Оцените статью
Мой сертификат
Добавить комментарий