Я предлагаю снова поговорить о certificate chaining engine, о котором мы уже говорили в посте Certificate Chaining Engine — как это работает но в рамках его реализации в .NET и PowerShell. В Windows есть несколько реализаций этого chaining engine. Самые популярные:
CAPICOM — вещь несколько стрёмная и её лучше избегать. Тем более мне не удалось завести его в Vista/Windows 7. Реализация в .NET в виде X509Chain ничуть не хуже, но, в то же время, проще. Итак, предлагаю начать с создания этого объекта с помощью New-Object:
[↓] [vPodans] $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
[↓] [vPodans] $chain
ChainContext ChainPolicy ChainStatus ChainElements
------------ ----------- ----------- -------------
0 System.Security.Cryptograp... {} {}
[↓] [vPodans] $chain | gm -membertype methods
TypeName: System.Security.Cryptography.X509Certificates.X509Chain
Name MemberType Definition
---- ---------- ----------
Build Method bool Build(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate)
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
Reset Method System.Void Reset()
ToString Method string ToString()
[↓] [vPodans]
Здесь видно, что метод, который будет строить и проверять цепочку будет называться Build() и в качестве аргумента этот метод принимает только объекты x509Certificate2, а возвращать объект типа Boolean — True/False. Объекты такого типа можно найти в провайдере Certififcates (просто выполнить dir cert:\store\container) или импортировать из файла — Импорт сертификатов в PowerShell. Но сначала мы посмотрим, что здесь можнол настроить. А настроить можно ChainPolicy, представляющий собой объект X509ChainPolicy. Эта политика позволяет задавать порядок проверки отзыва сертификатов, соответствие сертификата каким-то certificate policy, наличие определённых application policy (в прошлом известен нам как EKU — Extended Key Usage) и некритичность каких-то ошибок. Мы всё разбирать не будем, а только основное:
[↓] [vPodans] $chain.ChainPolicy
ApplicationPolicy : {}
CertificatePolicy : {}
RevocationMode : Online
RevocationFlag : ExcludeRoot
VerificationFlags : NoFlag
VerificationTime : 17.10.2009 20:35:17
UrlRetrievalTimeout : 00:00:00
ExtraStore : {}
[↓] [vPodans]
Первое, что может быть интересным — Revocation Mode, который задаёт режим проверки отзыва и может иметь 3 вполне понятных значения, которые перечислены в X509RevocationMode:
[↓] [vPodans] [System.Enum]::GetNames([system.security.cryptography.x509certificates.x509revocationmode])
NoCheck
Online
Offline
Тут очень просто:
- NoCheck — проверяет цепочку сертификатов без проверки на отзыв любых сертификатов в цепочке;
- Online — проверяет сертификаты в цепочке скачивая новые списки CRL из свойства CDP сертификата, игнорируя локальный кеш CRL (используется по умолчанию, как это видно в просмотре свойств ChainPolicy)
- Offline — проверяет сертификаты на отзыв с использованием кешированного CRL (если есть).
Если проверяем сертификаты на отзыв, то мы можем выбрать что именно будем проверять и этот выбор перечислен в X509RevocationFlag:
[↓] [vPodans] [System.Enum]::GetNames([system.security.cryptography.x509certificates.x509revocationflag])
EndCertificateOnly
EntireChain
ExcludeRoot
и здесь тоже всё достаточно понятно:
- EndCertificateOnly — проверяет на отзыв только сам проверяемый сертификат. Остальные сертификаты в цепочке не проверяются;
- EntireChain — проверяет на отзыв абсолютно все сертификаты в цепочке включая корневой сертификат. Правила хорошего тона диктуют нам не включать в корневой сертификат CA поля CDP и AIA, поскольку это лишено смысла и в большинстве случаев с этим флагом вы будете получать невалидную цепочку;
- ExcludeRoot — проверяет на отзыв все сертификаты в цепочке, кроме корневого сертификата, что есть религионзно правильно и этот флаг установлен по умолчанию.
Следующий момент определяет флаги проверки, т.е. по каким именно критериям будет проверяться цепочка сертификатов:
[↓] [vPodans] [System.Enum]::GetNames([system.security.cryptography.x509certificates.x509verificationflags])
NoFlag
IgnoreNotTimeValid
IgnoreCtlNotTimeValid
IgnoreNotTimeNested
IgnoreInvalidBasicConstraints
AllowUnknownCertificateAuthority
IgnoreWrongUsage
IgnoreInvalidName
IgnoreInvalidPolicy
IgnoreEndRevocationUnknown
IgnoreCtlSignerRevocationUnknown
IgnoreCertificateAuthorityRevocationUnknown
IgnoreRootRevocationUnknown
AllFlags
Эти флаги так же достаточно понятны и подробно разжёваны в перечислении X509VerificationFlags.
Примечание: эти флаги показывают что будет исключено из проверки, а не наоборот. По умолчанию проверяется всё (установлен NoFlag).
Как можно задавать эти флаги и режимы, если есть такая потребность? Можно пойти двумя способами — правильным и неправильным, хотя оба рабочие:
[↓] [vPodans] # правильный метод:
[↓] [vPodans] $revflag = [System.Security.Cryptography.X509Certificates.X509RevocationFlag]::EndCertificateOnly
[↓] [vPodans] $chain.ChainPolicy.RevocationFlag = $revflag
[↓] [vPodans] $chain.ChainPolicy
ApplicationPolicy : {}
CertificatePolicy : {}
RevocationMode : Online
RevocationFlag : EndCertificateOnly
VerificationFlags : NoFlag
VerificationTime : 17.10.2009 20:35:17
UrlRetrievalTimeout : 00:00:00
ExtraStore : {}
[↓] [vPodans] # неправильный метод, но мне он нравится
[↓] [vPodans] $chain.ChainPolicy.RevocationFlag = "excluderoot"
[↓] [vPodans] $chain.ChainPolicy
ApplicationPolicy : {}
CertificatePolicy : {}
RevocationMode : Online
RevocationFlag : ExcludeRoot
VerificationFlags : NoFlag
VerificationTime : 17.10.2009 20:35:17
UrlRetrievalTimeout : 00:00:00
ExtraStore : {}
[↓] [vPodans]
В принципе, все эти enumeration можно просто указывать в виде строки, а PowerShell уже сам подобъёт его под нужный тип.
Примечание: флаги проверки на отзыв можно указывать только по одному, а флаги исключений можно перечислять через запятую. Это видно по названию класса: если класс указан в единственном числе (X509RevocationFlag), то и значение можно указать только одно, а если во множественном числе (X509VerificationFlags), то их можно указать несколько через запятую.
В принципе, можно делать проверку:
[↓] [vPodans] $valid = (dir cert:\currentuser\my)[1]
[↓] [vPodans] $invalid = (dir cert:\currentuser\my)[0]
[↓] [vPodans] $chain.Build($valid)
True
[↓] [vPodans] $chain.ChainElements | select -expand certificate
Thumbprint Subject
---------- -------
986D375362652FE9E39BA4D042A6B8BA75745998 CN=Administrator, CN=Users, DC=sysadmins, DC=lv
E82ACC45841280DDEAB9F7847418FA26354457A7 CN=sysadmins-LV-CA, DC=sysadmins, DC=lv
[↓] [vPodans]
Я из локального хранилища взял заведомо хороший сертификат и плохой (разумеется, это сертификат с сайта БиЛайн
). Метод Build() вернул True, что означает успешную проверку всей цепочки моего сертификата до доверенного корня. А вот что случилось с сертификатом билайна:
[↓] [vPodans] $chain.reset()
[↓] [vPodans] $chain.Build($invalid)
False
[↓] [vPodans] $chain.ChainElements | select -expand certificate
Thumbprint Subject
---------- -------
EB74DA32E865C78FCB853DDA5FE45962098E1B3B CN=trust.beeline.ru, OU=DIT, O=Vimpelcom, L=Moscow, S=Moscow, C=RU
[↓] [vPodans] $chain.ChainStatus
Status StatusInformation
------ -----------------
PartialChain Sertificesanas kedi nevareja veidot pie uzticamas saknes...
RevocationStatusUnknown Atsauksanas funkcija nevareja parbaudit sertifikata atsa...
OfflineRevocation Atsauksanas funkcija nevareja parbaudit atsauksanu, jo a...
[↓] [vPodans] $chain.ChainStatus | %{$_.statusinformation.trim()}
Sertificesanas kedi nevareja veidot pie uzticamas saknes iestades.
Atsauksanas funkcija nevareja parbaudit sertifikata atsauksanu.
Atsauksanas funkcija nevareja parbaudit atsauksanu, jo atsauksanas serveris bija bezsaiste.
[↓] [vPodans]
Поскольку при каждой проверке сертификаты в ChainElements накапливаются, то после каждой проверки следует очищать объект методом Reset(). Как и следовало ожидать, сертификат билайна вернул False, показывая, что цепочка у этого сертификата имеет проблемы. В случае неуспешной проверки, будет заполняться свойство ChainStatus. Данное свойство содержит все ошибки, которые были костатированы при проверке цепочки. Для меня пока непонятным стал факт, что ошибки он написал на латышском языке, хотя я его об этом не просил. Откуда он это взял — непонятно, но он сказал, что не смог построить цепочку до доверенного корня и функция проверки отзыва провалилась по всем статьям, поскольку пути в CDP сертификата нерабочие.
Примечание: обязательно следует учитывать тот факт, что при построении цепочки сертификатов и проверке доверия класс X509Chain в Windows 7 и Windows Server 2008 R2 работает в контексте LocalSystem и всегда игнорирует пользовательские контейнеры Trusted Root Certification Authorities. Поэтому если корень цепочки не заканчивается на одном из сертификатов Trusted Root CAs хранилища LocalMachine, то цепочка будет считаться недоверенной. Для предыдущих ОС цепочка может заканчиваться на пользовательском хранилище Current User.
В принципе это всё, что вам следует знать про реализацию certificate chaining engine в .NET и его использование в PowerShell. В качестве бонуса прилагаю скрипт для проверки цепочки сертификатов:
#####################################################################
# Test-Certificate.ps1
# Version 1.0
#
# Passes certificate through certificate chaining engine
#
# Vadims Podans (c) 2009
# http://www.sysadmins.lv/
#####################################################################
#requires –Version 2.0
function Test-Certificate {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
$Certificate,
[System.Security.SecureString]$Password,
[ValidateSet("NoCheck", "Online", "Offline")]
[string]$CRLMode = "Online",
[ValidateSet("EndCertificateOnly", "EntireChain", "ExcludeRoot")]
[string]$CRLFlag = "ExcludeRoot",
[ValidateSet("AllFlags", "AllowUnknownCertificateAuthority", "NoFlag", "IgnoreNotTimeValid",
"IgnoreCtlNotTimeValid", "IgnoreNotTimeNested", "IgnoreInvalidBasicConstraints",
"IgnoreWrongUsage", "IgnoreInvalidName", "IgnoreInvalidPolicy", "IgnoreEndRevocationUnknown",
"IgnoreCtlSignerRevocationUnknown", "IgnoreCertificateAuthorityRevocationUnknown",
"IgnoreRootRevocationUnknown")]
[string[]]$VerificationFlags = "NoFlag"
)
begin {
# создаём объекты X509Certificate2 и X509chain ещё до поступлениея первого объекта сертификата
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
# в X509Chain записываем параметры проверки сертификатов. Как критерии
# отзыва, так и исключения из проверки
$chain.ChainPolicy.RevocationFlag = $CRLFlag
$chain.ChainPolicy.RevocationMode = $CRLMode
$chain.ChainPolicy.VerificationFlags = $VerificationFlags
# мини-функция для предоставления понятного вывода на экран о статусе проверки
# и ошибках в цепочке, если они есть.
function _getstatus_ ($status, $chain, $cert) {
if ($status) {
Write-Host Current certificate $cert.SerialNumber chain and revocation status is valid -ForegroundColor Green
} else {
Write-Warning "Current certificate $($cert.SerialNumber) chain is invalid due of the following errors:"
$chain.ChainStatus | %{Write-Host $_.StatusInformation.trim() -ForegroundColor Red}
}
}
}
process {
# если аргумент сертификата уже является объектом X509Certificate2, то
# сразу строим цепочку и получаем статус цепочки вызывая мини-функцию
if ($_ -is [System.Security.Cryptography.X509Certificates.X509Certificate2]) {
$status = $chain.Build($_)
_getstatus_ $status $chain $_
} else {
# если аргумент не является объектом X509Certificate2, то это будет путь к файлу
# если указанный путь не существует, то ругаемся и выходим
if (!(Test-Path $Certificate)) {Write-Warning "Specified path is invalid"; return}
else {
# если путь существует, то проверяем, что это путь файловой системы.
# если это не так, то ругаемся и выходим
if ((Resolve-Path $Certificate).Provider.Name -ne "FileSystem") {
Write-Warning "Spicifed path is not recognized as filesystem path. Try again"; return
} else {
# если предварительные проверки прошли успешно, то выбираем объект файла
$Certificate = gi $(Resolve-Path $Certificate)
# и на основании расширения файла через Switch преобразуем файл в
# X509Certificate2 объект или объекты
switch -regex ($Certificate.Extension) {
"\.CER|\.DER|\.CRT" {$cert.Import($Certificate.FullName)}
"\.PFX" {
if (!$Password) {$Password = Read-Host "Enter password for PFX file $certificate" -AsSecureString}
# тут пришлось указать контекст хранилища, куда предполагается сохранить сертификат.
# на самом деле мы никуда не будем сохранять объект сертификата, это было сделано
# по причине отсутствия подходящего конструктора метода Import(), чтобы
# можно было указать только путь к файлу и пароль от PFX
$cert.Import($Certificate.FullName, $password, "UserKeySet")
}
"\.P7B|\.SST" {
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
$cert.Import([system.IO.File]::ReadAllBytes($file.FullName))
}
default {Write-Warning "Looks like your specified file is not a certificate file"; return}
}
# полученный объект или объекты сертификатов пропускаем через метод Build() и вызываем
# мини-функцию расшифровки статуса проверки
$cert | %{
$status = $chain.Build($_)
_getstatus_ $status $chain $_
}
# после каждой итерации очищаем объекты сертификата и цепочки
$cert.Reset()
$chain.Reset()
}
}
}
}
}
Данный скрипт может принимать аргументы в виде уже готовых объектов X509Certificate2 или с указанием пути к файлу сертификата, например:
dir cert:\currentuser\my | Test-Certificate
Test-Certificate .\mycert.cer
Ну и при желании можно поуказывать там разные параметры и флаги проверки.