Posts on this page:
Давным давно я мечтал об удобном (ну или не очень, но чтобы стандартном) способе работы с CRL'ами в PowerShell. Ленивые разрабы .NET'а как не чесались, так и до сих пор не чешутся по этому поводу, хотя, задача весьма востребованная. Я несколько раз пытался подойти к решению этого вопроса самостоятельно. Но незнание чего-то не позволяло этого достичь. Но не так давно в качестве практического упражнения в ASN.1 я написал вот такой страшный парсер: Basic CRL parser for PowerShell. Это даже не концепт, а просто отработка навыков работы с ASN.1. Но в виду его неуниверсальности, розовомедленности его даже стыдно запускать и показывать как он работает. Неделю назад я снова вспомнил об этой теме и решил подойти к вопросу более основательно и написал код с использованием p/invoke неуправляемого кода и с блек-джеком и шлюхами, который используется стандартыми Windows инструментами для работы с CRL объектами. Я не буду рассказывать об истории его создания и как он там внутри работает, потому что это лишено всякого смысла. Взамен я предлагаю рабочий код, который вы можете использовать в собственных целях:
##################################################################### # Get-CRL.ps1 # Version 1.0 # # Retrieves CRL object from a file or a DER-encoded byte array. # # Vadims Podans (c) 2011 # http://www.sysadmins.lv/ ##################################################################### #requires -Version 2.0 function Get-CRL { <# .Synopsis Retrieves CRL object from a file or a DER-encoded byte array. .Description Retrieves CRL object from a file or a DER-encoded byte array. .Parameter Path Specifies the path to a file. .Parameter RawCRL Specifies a pointer to a DER-encoded CRL byte array. .Example Get-CRL C:\Custom.crl Returns X509CRL2 object from a specified file .Example $Raw = [IO.FILE]::ReadAllBytes("C:\Custom.crl") Get-CRL -RawCRL $Raw Returns X509CRL2 object from a DER-encoded byte array. .Outputs System.Security.Cryptography.X509Certificates.X509CRL2 .NOTES Author: Vadims Podans Blog : http://en-us.sysadmins.lv #> [OutputType('System.Security.Cryptography.X509Certificates.X509CRL2')] [CmdletBinding(DefaultParameterSetName='FileName')] param( [Parameter(ParameterSetName = "FileName", Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string]$Path, [Parameter(ParameterSetName = "RawData", Mandatory = $true, Position = 0)] [Byte[]]$RawCRL ) #region content parser switch ($PsCmdlet.ParameterSetName) { "FileName" { if ($(Get-Item $Path -ErrorAction Stop).PSProvider.Name -ne "FileSystem") { throw {"File either does not exist or not a file object"} } if ($(Get-Item $Path -ErrorAction Stop).Extension -ne ".crl") { throw {"File is not valid CRL file"} } $Content = Get-Content $Path if ($Content[0] -eq "-----BEGIN X509 CRL-----") { [Byte[]]$cBytes = [Convert]::FromBase64String($(-join $Content[1..($Content.Count - 2)])) } elseif ($Content[0][0] -eq "M") { [Byte[]]$cBytes = [Convert]::FromBase64String($(-join $Content)) } else { [Byte[]]$cBytes = [IO.File]::ReadAllBytes($Path) } } "RawData" {[Byte[]]$cBytes = $RawCRL} } #endregion $signature = @" [DllImport("CRYPT32.DLL", CharSet = CharSet.Auto, SetLastError = true)] public static extern int CertCreateCRLContext( int dwCertEncodingType, byte[] pbCrlEncoded, int cbCrlEncoded ); [DllImport("CRYPT32.DLL", SetLastError = true)] public static extern Boolean CertFreeCRLContext( IntPtr pCrlContext ); [DllImport("CRYPT32.DLL", CharSet = CharSet.Auto, SetLastError = true)] public static extern int CertNameToStr( int dwCertEncodingType, ref CRYPTOAPI_BLOB pName, int dwStrType, System.Text.StringBuilder psz, int csz ); [DllImport("CRYPT32.DLL", CharSet = CharSet.Auto, SetLastError = true)] public static extern IntPtr CertFindExtension( [MarshalAs(UnmanagedType.LPStr)]String pszObjId, int cExtensions, IntPtr rgExtensions ); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct CRL_CONTEXT { public int dwCertEncodingType; public byte[] pbCrlEncoded; public int cbCrlEncoded; public IntPtr pCrlInfo; public IntPtr hCertStore; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct CRL_INFO { public int dwVersion; public CRYPT_ALGORITHM_IDENTIFIER SignatureAlgorithm; public CRYPTOAPI_BLOB Issuer; public Int64 ThisUpdate; public Int64 NextUpdate; public int cCRLEntry; public IntPtr rgCRLEntry; public int cExtension; public IntPtr rgExtension; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct CRYPT_ALGORITHM_IDENTIFIER { [MarshalAs(UnmanagedType.LPStr)]public String pszObjId; public CRYPTOAPI_BLOB Parameters; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct CRYPTOAPI_BLOB { public int cbData; public IntPtr pbData; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct CRL_ENTRY { public CRYPTOAPI_BLOB SerialNumber; public Int64 RevocationDate; public int cExtension; public IntPtr rgExtension; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct CERT_EXTENSION { [MarshalAs(UnmanagedType.LPStr)]public String pszObjId; public Boolean fCritical; public CRYPTOAPI_BLOB Value; } "@ Add-Type @" using System; using System.Security; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; namespace System { namespace Security { namespace Cryptography { namespace X509Certificates { public class X509CRL2 { public int Version; public string Type; public X500DistinguishedName IssuerDN; public string Issuer; public DateTime ThisUpdate; public DateTime NextUpdate; public Oid SignatureAlgorithm; public X509ExtensionCollection Extensions; public X509CRLEntry[] RevokedCertificates; public byte[] RawData; } public class X509CRLEntry { public string SerialNumber; public DateTime RevocationDate; public int ReasonCode; public string ReasonMessage; } } } } } "@ try {Add-Type -MemberDefinition $signature -Namespace PKI -Name CRL} catch {throw "Unable to load required types"} #region Variables [IntPtr]$pvContext = [IntPtr]::Zero [IntPtr]$rgCRLEntry = [IntPtr]::Zero [IntPtr]$pByte = [IntPtr]::Zero [byte]$bByte = 0 [IntPtr]$rgExtension = [IntPtr]::Zero $ptr = [IntPtr]::Zero $Reasons = @{1="Key compromise";2="CA Compromise";3="Change of Affiliation";4="Superseded";5="Cease Of Operation"; 6="Hold Certificiate";7="Privilege Withdrawn";10="aA Compromise"} #endregion # retrive CRL context and CRL_CONTEXT structure $pvContext = [PKI.CRL]::CertCreateCRLContext(65537,$cBytes,$cBytes.Count) if ($pvContext.Equals([IntPtr]::Zero)) {throw "Unable to retrieve context"} $CRL = New-Object System.Security.Cryptography.X509Certificates.X509CRL2 # void first marshaling operation, because it throws unexpected exception try {$CRLContext = [Runtime.InteropServices.Marshal]::PtrToStructure([IntPtr]$pvContext,[PKI.CRL+CRL_CONTEXT])} catch {} $CRLContext = [Runtime.InteropServices.Marshal]::PtrToStructure([IntPtr]$pvContext,[PKI.CRL+CRL_CONTEXT]) $CRLInfo = [Runtime.InteropServices.Marshal]::PtrToStructure($CRLContext.pCrlInfo,[PKI.CRL+CRL_INFO]) $CRL.Version = $CRLInfo.dwVersion + 1 $CRL.Type = "Base CRL" $CRL.RawData = $cBytes $CRL.SignatureAlgorithm = New-Object Security.Cryptography.Oid $CRLInfo.SignatureAlgorithm.pszObjId $CRL.ThisUpdate = [datetime]::FromFileTime($CRLInfo.ThisUpdate) $CRL.NextUpdate = [datetime]::FromFileTime($CRLInfo.NextUpdate) $csz = [PKI.CRL]::CertNameToStr(65537,[ref]$CRLInfo.Issuer,3,$null,0) $psz = New-Object text.StringBuilder $csz $csz = [PKI.CRL]::CertNameToStr(65537,[ref]$CRLInfo.Issuer,3,$psz,$csz) $CRL.IssuerDN = New-Object Security.Cryptography.X509Certificates.X500DistinguishedName $psz $CRL.Issuer = $CRL.IssuerDN.Format(0) $rgCRLEntry = $CRLInfo.rgCRLEntry if ($CRLInfo.cCRLEntry -ge 1) { for ($n = 0; $n -lt $CRLInfo.cCRLEntry; $n++) { $Entry = New-Object System.Security.Cryptography.X509Certificates.X509CRLEntry $SerialNumber = "" $CRLEntry = [Runtime.InteropServices.Marshal]::PtrToStructure($rgCRLEntry,[PKI.CRL+CRL_ENTRY]) $pByte = $CRLEntry.SerialNumber.pbData $SerialNumber = "" for ($m = 0; $m -lt $CRLEntry.SerialNumber.cbData; $m++) { $bByte = [Runtime.InteropServices.Marshal]::ReadByte($pByte) $SerialNumber = "{0:x2}" -f $bByte + $SerialNumber $pByte = [int]$pByte + [Runtime.InteropServices.Marshal]::SizeOf([byte]) } $Entry.SerialNumber = $SerialNumber $Entry.RevocationDate = [datetime]::FromFileTime($CRLEntry.RevocationDate) $CRLReasonCode = "" [IntPtr]$rcExtension = [PKI.CRL]::CertFindExtension("2.5.29.21",$CRLEntry.cExtension,$CRLEntry.rgExtension) if (!$rcExtension.Equals([IntPtr]::Zero)) { $CRLExtension = [Runtime.InteropServices.Marshal]::PtrToStructure($rcExtension,[PKI.CRL+CERT_EXTENSION]) $pByte = $CRLExtension.Value.pbData $bBytes = $null for ($m = 0; $m -lt $CRLExtension.Value.cbData; $m++) { $bByte = [Runtime.InteropServices.Marshal]::ReadByte($pByte) [Byte[]]$bBytes += $bByte $pByte = [int]$pByte + [Runtime.InteropServices.Marshal]::SizeOf([byte]) } $Entry.ReasonCode = $bBytes[2] $Entry.ReasonMessage = $Reasons[$Entry.ReasonCode] } $CRL.RevokedCertificates += $Entry $rgCRLEntry = [int]$rgCRLEntry + [Runtime.InteropServices.Marshal]::SizeOf([PKI.CRL+CRL_ENTRY]) } } $rgExtension = $CRLInfo.rgExtension if ($CRLInfo.cExtension -ge 1) { $Exts = New-Object Security.Cryptography.X509Certificates.X509ExtensionCollection for ($n = 0; $n -lt $CRLInfo.cExtension; $n++) { $ExtEntry = [Runtime.InteropServices.Marshal]::PtrToStructure($rgExtension,[PKI.CRL+CERT_EXTENSION]) [IntPtr]$rgExtension = [PKI.CRL]::CertFindExtension($ExtEntry.pszObjId,$CRLInfo.cExtension,$CRLInfo.rgExtension) $pByte = $ExtEntry.Value.pbData $bBytes = $null for ($m = 0; $m -lt $ExtEntry.Value.cbData; $m++) { [byte[]]$bBytes += [Runtime.InteropServices.Marshal]::ReadByte($pByte) $pByte = [int]$pByte + [Runtime.InteropServices.Marshal]::SizeOf([byte]) } $ext = New-Object Security.Cryptography.X509Certificates.X509Extension $ExtEntry.pszObjId, @([Byte[]]$bBytes), $ExtEntry.fCritical [void]$Exts.Add($ext) $rgExtension = [int]$rgExtension + [Runtime.InteropServices.Marshal]::SizeOf([PKI.CRL+CERT_EXTENSION]) } if ($exts | ?{$_.Oid.Value -eq "2.5.29.27"}) {$CRL.Type = "Delta CRL"} $CRL.Extensions = $Exts } $CRL [void][PKI.CRL]::CertFreeCRLContext($pvContext) }
И, собственно, его вывод:
[↓] [vPodans] Get-CRL .\Desktop\pica-1.crl Version : 2 Type : Base CRL IssuerDN : System.Security.Cryptography.X509Certificates.X500DistinguishedName Issuer : CN=Sysadmins LV Internal Class 1 SubCA-1, OU=Information Systems, O=Sysadmins LV, C=LV ThisUpdate : 22.02.2011 19:22:27 NextUpdate : 26.02.2011 19:42:27 SignatureAlgorithm : System.Security.Cryptography.Oid Extensions : {System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, System.Security.Cryptography .Oid, System.Security.Cryptography.Oid...} RevokedCertificates : {System.Security.Cryptography.X509Certificates.X509CRLEntry, System.Security.Cryptography.X509Cer tificates.X509CRLEntry, System.Security.Cryptography.X509Certificates.X509CRLEntry, System.Securi ty.Cryptography.X509Certificates.X509CRLEntry...} RawData : {48, 130, 3, 39...} [↓] [vPodans]
И некоторые внутренности:
[↓] [vPodans] $crl = Get-CRL .\Desktop\pica-1.crl [↓] [vPodans] $crl.Extensions[0].format(0) KeyID=1b fa 5e 73 2d 67 13 5c ce d3 0e e6 e8 7a a9 60 8c 0b 63 fc [↓] [vPodans] $crl.Extensions[4].format(1) [1]Freshest CRL Distribution Point Name: Full Name: URL=http://www.sysadmins.lv/pki/pica-1+.crl [↓] [vPodans] $crl.RevokedCertificates SerialNumber RevocationDate ReasonCode ReasonMessage ------------ -------------- ---------- ------------- 3bfe8e77000000000078 27.12.2010 19:32:00 0 163c8142000000000072 27.11.2010 23:27:00 5 Cease Of Operation 14d70748000000000071 27.11.2010 23:27:00 5 Cease Of Operation 411726e0000000000054 04.08.2010 21:26:00 0 1cee2e2000000000002b 01.05.2010 15:32:00 0 2ee0af5a000000000021 24.04.2010 22:25:00 0 [↓] [vPodans] $crl.GetType().FullName System.Security.Cryptography.X509Certificates.X509CRL2 [↓] [vPodans]
Не каждый знает, что центр сертификации Windows поддерживает не одну, а несколько таблиц:
Мы уже знаем, как обращаться к таблице запросов. По схожему принципу можно обращаться и к другим таблицам. Для переключения между ними используется метод SetTable интерфейса ICertView2. По умолчанию всегда используется таблица CVRC_TABLE_REQCERT. Вот какие кодовые номера у таблиц:
Метод SetTable нужно вызывать сразу после вызова метода OpenConnection. Таблица CRL'ов хранит историю всех CRL'ов (вместе с самими CRL'ами), которые были сгенерированы сервером CA, а так же прочую полезную информацию.
Вот как смотрится схема этой таблицы:
$CA = "dc1\Contoso CA" $CaView = New-Object -ComObject CertificateAuthority.View # открываем подключение к CA $CaView.OpenConnection($CA) # указываем необходимую таблицу $CaView.SetTable(0x5000) # говорим, что мы хотим посмотреть схему $Columns = $CaView.EnumCertViewColumn(0) # начинаем итерацию по столбцам БД [void]$Columns.Next() do { # создаём временный объект, чтобы получить красивый вывод и наполняем его данными $Column = "" | Select Name, DisplayName, Type, MaxLength $Column.Name = $Columns.GetName() $Column.DisplayName = $Columns.GetDisplayName() $Column.Type = switch ($Columns.GetType()) { 1 {"Long"} 2 {"DateTime"} 3 {"Binary"} 4 {"String"} } [string]$Column.MaxLength = $Columns.GetMaxLength() if ($Columns.IsIndexed() -eq 1) {$Column.MaxLength += ", Indexed"} $Column } until ($Columns.Next() -eq -1) # закрываем подключение к БД $Columns.Reset()
Её вид:
Name DisplayName Type MaxLength ---- ----------- ---- --------- CRLRowId CRL Row ID Long 4, Indexed CRLNumber CRL Number Long 4, Indexed CRLMinBase CRL Minimum Base Number Long 4 CRLNameId CRL Name ID Long 4 CRLCount CRL Count Long 4 CRLThisUpdate CRL This Update DateTime 8 CRLNextUpdate CRL Next Update DateTime 8, Indexed CRLThisPublish CRL This Publish DateTime 8 CRLNextPublish CRL Next Publish DateTime 8, Indexed CRLEffective CRL Effective DateTime 8 CRLPropagationComplete CRL Propagation Complete DateTime 8, Indexed CRLLastPublished CRL Last Published DateTime 8, Indexed CRLPublishAttempts CRL Publish Attempts Long 4, Indexed CRLPublishFlags CRL Publish Flags Long 4 CRLPublishStatusCode CRL Publish Status Code Long 4, Indexed CRLPublishError CRL Publish Error Information String 8192 CRLRawCRL CRL Raw CRL Binary 536870912
Вот пример вывода:
$CA = "dc1\Contoso CA" $CaView = New-Object -ComObject CertificateAuthority.View $CaView.OpenConnection($CA) $CaView.SetTable(0x5000) $ColumnCount = $CaView.GetColumnCount(0) $CaView.SetResultColumnCount($ColumnCount) 0..($ColumnCount - 1) | %{$CAView.SetResultColumn($_)} $Row = $CaView.OpenView() [void]$Row.Next() while ($Row.Next() -ne -1) { $cert = New-Object psobject $Column = $Row.EnumCertViewColumn() while ($Column.Next() -ne -1) { $current = $Column.GetName() $Cert | Add-Member -MemberType NoteProperty $($Column.GetDisplayName()) -Value $($Column.GetValue(1)) -Force } $Cert $Column.Reset() } $Row.Reset()
CRL Row ID : 409 CRL Number : 347 CRL Minimum Base Number : 340 CRL Name ID : 1 CRL Count : 0 CRL This Update : 2010.02.25. 17:55:51 CRL Next Update : 2010.02.26. 19:15:51 CRL This Publish : 2010.02.25. 18:05:51 CRL Next Publish : 2010.02.26. 18:05:51 CRL Effective : 2010.02.18. 17:55:51 CRL Propagation Complete : 2010.02.25. 19:05:51 CRL Last Published : 2010.02.25. 18:05:51 CRL Publish Attempts : 1 CRL Publish Flags : 6 CRL Publish Status Code : 0 CRL Publish Error Information : - CRL Raw CRL : MIICzDCCAbQCAQEwDQYJKoZIhvcNAQEFBQAwQzETMBEGCgmSJomT8ixkARkWA2Nv bTEXMBUGCgmSJomT8ixkARkWB2NvbnRvc28xEzARBgNVBAMTCkNvbnRvc28gQ0EX DTEwMDIyNTE3NTU1MVoXDTEwMDIyNjE5MTU1MVqgggE7MIIBNzAfBgNVHSMEGDAW gBQSyac4taTAA9cqtmWlsPEzGBKsPzAQBgkrBgEEAYI3FQEEAwIBATALBgNVHRQE BAICAVswHAYJKwYBBAGCNxUEBA8XDTEwMDIyNjE4MDU1MVowDgYDVR0bAQH/BAQC AgFUMIHGBgkrBgEEAYI3FQ4EgbgwgbUwgbKgga+ggayGgalsZGFwOi8vL0NOPUNv bnRvc28lMjBDQSxDTj1EQzEsQ049Q0RQLENOPVB1YmxpYyUyMEtleSUyMFNlcnZp Y2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9Y29udG9zbyxEQz1j b20/ZGVsdGFSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNzPWNSTERpc3Ry aWJ1dGlvblBvaW50MA0GCSqGSIb3DQEBBQUAA4IBAQB3DuZRToyK/OkrnCQQUl1P qgv/V9nPhAR6LF/b25Tq7fhKZniAk/cPgj2L3IMsLx2lmrI7GEEDt4UDcuc3EPtF f7gixrN3K+eSE/Er0NxBdJFUhHK9e/CVXqJFV2pGSa77mvcI75l2h5mlUGDZzGOl <...> <...>
Что касается остальных двух таблиц, то они, как я уже говорил, работают по тому же принципу и вы так же можете накладывать фильтры вывода методом SetRestriction. Они содержат ту же информацию, что вы видите в оснастке CertSrv.msc, когда выделяете запрос, нажимаете Action –> All Tasks –> View Attributes/Extensions.
На этом я завершаю цикл статей по работе с базой данных центров сертификации Windows с помощью Windows PowerShell.
В предыдущих статьях (раз и два) мы говорили о том, как получить схему БД центра сертификации и как получать определённые свойства каждой записи в БД. Но очень часто нам нужно будет ограничивать вывод БД по каким-то критериям. Например, мы можем хотеть получать сведения о запросах на сертификат, которые выданы на определённое имя (common name). А можем и хотеть получать сведения о ещё не одобренных запросах (хранящихся в папке Pending Requests). А может хотим отфильтровать вывод по дате, когда заканчивается срок действия сертификата. Вобщем, хотелок может быть очень много. Самый простой вариант фильтрации — получить все строки из БД, а потом через Where-Object отфильтровать нужные. Практически во всех случаях это будет плохим решением, потому что оно медленное, создаёт нагрузку на БД и очень ресурсоёмким.
Решение этой проблемы мы можем возложить на COM интерфейсы, которые будут самостоятельно извлекать только те данные, которые нам нужны (подпадают под определённый фильтр). Для этого у ICertView2 есть метод SetRestriction:
CCertView.SetRestriction( _ ByVal ColumnIndex, _ ByVal SeekOperator, _ ByVal SortOrder, _ ByVal pvarValue _ )
В свойстве ColumnIndex мы указываем номер колонки или название таблицы. Если мы захотим посмотреть запросы сертификатов, находящиеся в папке Pending Requests мы можем указать таблицу CV_COLUMN_QUEUE_DEFAULT, а если только отклонённые/ошибочные запросы, то таблицу CV_COLUMN_LOG_FAILED_DEFAULT. Вот значения для этих параметров:
Как видите, при использовании отрицательных значений мы задаём фильтр на уровне таблиц. Если хотим фильтровать на уровне столбцов (т.е. значение определённого столбца соответствует чему-то), значение этого аргумента должно быть натуральным и это значение должно быть равно номеру столбца. Как мы уже знаем, номер столбца можно получить при помощи метода GetColumnIndex.
Дальнейшие аргументы имеют смысл только если ColumnIndex является натуральным числом. SeekOperator задаёт уровень сравнения и они достаточно понятно расписаны в таблице. Единственное, что тут хочу отметить — это числовые значения операторов сравнения:
Далее идёт аргумент, задающий порядок сортировки:
По большому счёту этот аргумент использовать не нужно, особенно учитывая, что иногда его использовать нельзя. И последний аргумент указывает маску, которой должен соответствовать фильтр.
Возьмём пример, мы хотим получить все строки БД, у которых столбец Request Common Name равен "contoso-dc2-ca" (сертификаты выданные подчинённому CA). Для этого в ColumnIndex укажем 60 (номер столбца CommonName), в SeekOperator укажем 1, а в pvarValue укажем значение, которому должно оно соответствовать: contoso-dc2-ca.
Давайте попробуем создать такой фильтр. Сначала обязательная часть по инициализации интерфейса и определения отображаемых столбцов выходных объектов.
$CaView = New-Object -ComObject CertificateAuthority.View $CaView.OpenConnection("dc1\contoso ca") $properties = "RequestID","RequesterName","CommonName","NotBefore","NotAfter","SerialNumber" $CaView.SetResultColumnCount($properties.Count) $properties | %{$CAView.SetResultColumn($CAView.GetColumnIndex($False, $_))}
Примечание: столбец, по которому устанавливается фильтр не обязательно должен присустствовать в выходе. Вы можете вообще показывать только столбец RequestID, а фильтры устанавливать по другим столбцам.
И сам фильтр:
# получаем номер столбца, по которому будет производиться фильтрация $RColumn = $CAView.GetColumnIndex($False, "CommonName") # устанавливаем сам фильтр $CaView.SetRestriction($RColumn,1,0,"contoso-dc2-ca")
По умолчанию фильтр устанавливается на таблице CV_COLUMN_LOG_DEFAULT, которая содержит запросы сертификатов, содержащихся в папках Revoked Certificates, Issued Certificates и Failed Requests. Мы не можем в пределах одного фильтра указать конкретную таблицу. Но вы можете использовать несколько фильтров одновременно и каждый из них будет обрабатываться по очереди с применением логического оператора И. Т.е. в результате мы получим только те строки БД, которые подпадают под каждый фильтр. Если строка не соответствует какому-то фильтру, она будет отфильтрована и не будет отображена на выходе. Поэтому я вам покажу ещё один вариант полезного фильтра, который фильтрует строки БД по папкам (Revoked Certificates, Issued Certificates, Pending Requests, FailedRequests). В БД есть такой столбец под названием Disposition. Этот столбец имеет тип Long и вот какие полезные значения может принимать:
Поэтому если мы хотим получить выданные и неотозванные сертификаты на имя contoso-dc2-ca, мы создадим ещё один фильтр по столбцу Disposition (и значением равным 20):
$RColumn = $CAView.GetColumnIndex($False, "Disposition") $CaView.SetRestriction($RColumn,1,0,20)
Если мы хотим добавить фильтр, что сертификат должен быть действующий, т.е. столбец NotAfter должен быть больше, чем текущие дата и время:
$RColumn = $CAView.GetColumnIndex($False, "NotAfter") $CaView.SetRestriction($RColumn,0x10,0,[datetime]::Now)
Когда с фильтрами покончено, можно начинать шахматы:
$Row = $CaView.OpenView() while ($Row.Next() -ne -1) { $cert = New-Object psobject $Column = $Row.EnumCertViewColumn() while ($Column.Next() -ne -1) { $current = $Column.GetName() $Cert | Add-Member -MemberType NoteProperty $($Column.GetDisplayName()) -Value $($Column.GetValue(1)) -Force } $Cert $Column.Reset() } $Row.Reset()
и что мы получили на выходе:
Issued Request ID : 21 Requester Name : CONTOSO\Administrator Issued Common Name : contoso-DC2-CA Certificate Effective Date : 2009.03.30. 13:56:53 Certificate Expiration Date : 2011.03.30. 14:06:53 Serial Number : 6127fbc7000000000015 Issued Request ID : 40 Requester Name : CONTOSO\Administrator Issued Common Name : contoso-DC2-CA Certificate Effective Date : 2010.03.06. 10:56:53 Certificate Expiration Date : 2015.03.05. 10:56:53 Serial Number : 158f4a3b000100000028 Issued Request ID : 42 Requester Name : CONTOSO\Administrator Issued Common Name : contoso-DC2-CA Certificate Effective Date : 2010.03.06. 11:10:31 Certificate Expiration Date : 2015.03.05. 11:10:31 Serial Number : 159bc07a00010000002a
т.е. мы получили только те строки БД, которые подпали под каждый указанный фильтр. Если запустить этот же код 1 апреля, мы уже не получим строку под номером 21, потому что NotAfter (Certificate Expiration Date) не будет больше, чем текущее время. На сегодня всё.
В предыдущей части мы рассмотрели возможность извлечения схемы БД центров сертификации. По схожему принципу можно и делать выборку из неё посредством тех же COM интерейсов. Как я уже говорил, основным интерфейсом для доступа к БД будет ICertView2:
$CaView = New-Object -ComObject CertificateAuthority.View
Подключаемся к конкретному серверу CA при помощи метода OpenConnection:
$CaView.OpenConnection("dc1\contoso ca")
Для начала мы рассмотрим простой пример, выберем все строки из БД, но с заранее определёнными столбцами. Это нужно затем, чтобы не отображать ненужную информацию, которая будет мешать и поедать память. Каждая строка в БД максимально может занимать до полумегабайта места в памяти. Выглядит нестрашно, но когда у вас тысяча запросов в БД это уже будет примерно 500 мегабайт. Но тысяча запросов — это очень даже немного. Поэтому давайте для начала посмотрим все запросы вот в каком виде: номер запроса, имя реквестора (пользователя, от лица которого был подан запрос в CA), имя, на которое выдан сертификат, серийный номер сертификата и срок действия сертификата. Схемы БД для всех существующих Windows CA можно получить при помощи скрипта в предыдущей части или найти и в оффлайне: [MS-CSRA] Product Behavior, note 5.
Необходимые столбцы добавляются при помощи методов SetResultColumnCount, GetColumnIndex и SetResultColumn:
$CaView.SetResultColumnCount(6) $Column0 = $CaView.GetColumnIndex($false,"RequestID") $CaView.SetResultColumn($Column0) $Column1 = $CaView.GetColumnIndex($false,"RequesterName") $CaView.SetResultColumn($Column1) <...>
Здесь я показал последовательность действий. Сначала задаём количество столбцов, которые будут возвращены. Далее извлекаем номер каждого столбца и добавляем его в результирующую таблицу. Но эту процедуру можно упростить до трёх строчек, вне зависимости от количества требуемых столбцов:
$properties = "RequestID","RequesterName","CommonName","NotBefore","NotAfter","SerialNumber" $CaView.SetResultColumnCount($properties.Count) $properties | %{$CAView.SetResultColumn($CAView.GetColumnIndex($False, $_))}
Если попытаетесь добавить столбцов больше, чем указано в методе SetResultColumnCount, получите ошибку. Поэтому сначала подсчитывайте количество требуемых столбцов и только после этого их добавлять. В принципе, теперь всё, можно начинать опрашивать БД. Для этого открываем её методом OpenView:
$Row = $CaView.OpenView()
Этот метод вернёт указатель на объект интерфейса IEnumCERTVIEWROW и начинаются шахматы (перевод шагового искателя в исходную позицию). Дальше уже нужно запускать цикл Do…Until или While…Do. Я предпочитаю второй вариант по той простой причине, что если если БД пустая (при указании дополнительных фильтров и ничего под него не подпало) первый же вызов метода Next вернёт –1. Поэтому нужно либо сразу отслеживать значение индекса или использовать такой тип цикла, где значение индекса проверяется перед запуском первой итерации цикла, как While…Do. И вот как это выглядит:
Внимание: если в БД CA находится большое количество запросов, вы получите огромную кучу объектов на выходе, потому что будут возвращены все запросы из секций Issued Certificates, Revoked Certificates и Failed Requests.
# начинаем шахматы и шагать шаговым искателем по строчкам while ($Row.Next() -ne -1) { # создаём пустой объект, который будет являться нашим выходным объектом $cert = New-Object psobject # переставляем шаговый искатель с вертикального в горизонтальное направление $Column = $Row.EnumCertViewColumn() # шагаем последовательно по столбцам, которые мы определили вначале скрипта while ($Column.Next() -ne -1) { # извлекаем название текущего столбца $current = $Column.GetName() # добавляем свойство к нашему выходному объекту с именем равным имени столбца # и добавляем соответствующее значение столбца текущей строчки $Cert | Add-Member -MemberType NoteProperty $($Column.GetDisplayName()) -Value $($Column.GetValue(1)) -Force } # выкидываем объект на выход $Cert # сбрасываем положение горизонтального шагового искателя $Column.Reset() } # сбрасываем положение вертикального шагового искателя $Row.Reset()
И вот как выглядит примерный вывод:
Issued Request ID : 1 Requester Name : CONTOSO\DC1$ Issued Common Name : Contoso CA Certificate Effective Date : 2009.02.15. 14:31:15 Certificate Expiration Date : 2014.02.15. 14:40:11 Serial Number : 5dd87e4cffe3b3bc43f608eb57c767f7 Issued Request ID : 2 Requester Name : CONTOSO\DC1$ Issued Common Name : DC1.contoso.com Certificate Effective Date : 2009.02.15. 14:51:45 Certificate Expiration Date : 2010.02.15. 14:51:45 Serial Number : 15109536000000000002 Issued Request ID : 3 Requester Name : CONTOSO\administrator Issued Common Name : Users Administrator Certificate Effective Date : 2009.02.15. 15:04:02 Certificate Expiration Date : 2011.02.15. 15:04:02 Serial Number : 151bd2c9000000000003 <...>
круто, да? Но здесь мы получили сведения о каждом запросе, а мы хотим получать что-то более конкретное, как быть? В следующий раз я как раз и покажу, как можно создавать более конкретные запросы, чтобы получать только результаты, подпадающие под определённый критерий.
Иногда бывает очень полезным найти какой-нибудь запрос в БД CA. Можно, конечно же, сделать это через certutil, но это не всегда удобно, потому что если под конкретный запрос подпадает несколько строк из БД, нужно уже вручную как-то парсить вывод, чтобы найти то, что нужно. Поэтому я решил рассказать, как наваять подобный костыль (только религиозно-православный) родными средствами PowerShell и необходимыми COM интерфейсами. Для работы с БД сервера CA нам понадобится лишь один интерфейс: ICertView2.
Каждая БД состоит из столбцов и рядов. То же самое относится и к БД CA (которая, кстати говоря, является стандартной встроенной Jet БД, что позволяет с ней работать стандартными тулзами типа eseutil). Ряды представляют собой набор свойств каждого запроса и для каждого запроса выделяется ровно один ряд. Столбцы представляют собой сами свойства. Каждому столбцу соответствует одно свойство. CA использует БД с заранее созданной (и неизменяемой) схемой, т.е. набором поддерживаемых столбцов. Давайте попробуем вывести схему БД. Для начала создадим объект ICertView2:
$CaView = New-Object -ComObject CertificateAuthority.View
Далее нужно подключиться к какому-то CA серверу при помощи метода OpenConnection. В качестве аргумента следует указать конфигурационную строку CA сервера вида CAComputerName\CAName.
$CaView.OpenConnection("dc1\Contoso CA")
Если ошибок не высыпало, значит, подключение произошло удачно. Теперь мы хотим посмотреть схему и это можно сделать при помощи метода EnumCertViewColumn. В качестве аргумента указываем CVRC_COLUMN_SCHEMA (которое имеет значение 0).
$CaView.EnumCertViewColumn(0)
В результате мы молучим другой объект интерфейса IEnumCERTVIEWCOLUMN. А теперь методом Next будем шагать по каждому столбцу и собирать сведения о нём, как: имя столбца, локализованное имя, тип данных и максимальный размер данных для этого столбца при помощи набора методов, описанных на страничке ICertView2. Вот как это выглядит:
PS C:\> $CaView = New-Object -ComObject CertificateAuthority.View PS C:\> $CA = "dc1\Contoso CA" PS C:\> $CaView.OpenConnection($CA) PS C:\> $Columns = $CaView.EnumCertViewColumn(0) PS C:\> $Columns.Next() 0 PS C:\> $Columns.GetName() Request.RequestID PS C:\> $Columns.GetDisplayName() Request ID PS C:\> $Columns.GetType() 1 PS C:\> $Columns.GetMaxLength() 4 PS C:\> $Columns.Next() 1 PS C:\> $Columns.GetName() Request.RawRequest PS C:\> $Columns.GetDisplayName() Binary Request PS C:\> $Columns.GetType() 3 PS C:\> $Columns.GetMaxLength() 65536 PS C:\>
После метода Next мы видим текущий индекс столбца. Что касается вывода метода GetType(), то вместо типа данных мы видим только циферки. Это нормальное поведение для CryptoAPI интерфейсов. Каждому числу соответствует свой тип данных (их поддерживается всего 4) и вот как они расшифровываются:
Если всё это затолкать в цикл DO…UNTIL, можно всё это дело автоматизировать. По правде говоря некоторые столбцы могут содержать массив данных и это можно проверить при помощи метода IsIndexed. Давайте теперь напишем окончательный скрипт, который нам по одному клику выдаст всю схему БД:
function Get-CADataBaseSchema { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string]$CAConfig ) # создаём объект ICertView try {$CaView = New-Object -ComObject CertificateAuthority.View} catch {Write-Error "Unable to instantiate ICertView object"; return} # открываем подключение к CA $CaView.OpenConnection($CAConfig) # говорим, что мы хотим посмотреть схему $Columns = $CaView.EnumCertViewColumn(0) # начинаем итерацию по столбцам БД [void]$Columns.Next() do { # создаём временный объект, чтобы получить красивый вывод и наполняем его данными $Column = "" | Select Name, DisplayName, Type, MaxLength $Column.Name = $Columns.GetName() $Column.DisplayName = $Columns.GetDisplayName() $Column.Type = switch ($Columns.GetType()) { 1 {"Long"} 2 {"DateTime"} 3 {"Binary"} 4 {"String"} } [string]$Column.MaxLength = $Columns.GetMaxLength() if ($Columns.IsIndexed() -eq 1) {$Column.MaxLength += ", Indexed"} $Column } until ($Columns.Next() -eq -1) # закрываем подключение к БД $Columns.Reset() }
А вывод весьма простенький:
PS C:\> Get-CADataBaseSchema "dc1\Contoso CA" Name DisplayName Type MaxLength ---- ----------- ---- --------- Request.RequestID Request ID Long 4, Indexed Request.RawRequest Binary Request Binary 65536 Request.RawArchivedKey Archived Key Binary 65536 Request.KeyRecoveryHashes Key Recovery Agent Hashes String 8192 Request.RawOldCertificate Old Certificate Binary 16384 Request.RequestAttributes Request Attributes String 32768 Request.RequestType Request Type Long 4 Request.RequestFlags Request Flags Long 4 Request.StatusCode Request Status Code Long 4 Request.Disposition Request Disposition Long 4, Indexed Request.DispositionMessage Request Disposition Message String 8192 Request.SubmittedWhen Request Submission Date DateTime 8, Indexed Request.ResolvedWhen Request Resolution Date DateTime 8, Indexed Request.RevokedWhen Revocation Date DateTime 8 Request.RevokedEffectiveWhen Effective Revocation Date DateTime 8, Indexed Request.RevokedReason Revocation Reason Long 4 Request.RequesterName Requester Name String 2048, Indexed Request.CallerName Caller Name String 2048, Indexed Request.SignerPolicies Signer Policies String 8192 Request.SignerApplicationP... Signer Application Policies String 8192 Request.Officer Officer Long 4 <...>
В следующей части (или частях, пока ещё не придумал) поговорим о том, как блуждать по самой БД.