Contents of this directory is archived and no longer updated.

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 поддерживает не одну, а несколько таблиц:

  1. Таблица запросов (что мы видим в оснастке certsrv.msc);
  2. Таблица списков отзывов (CRL Table);
  3. Таблица атрибутов;
  4. Таблица расширений.

Мы уже знаем, как обращаться к таблице запросов. По схожему принципу можно обращаться и к другим таблицам. Для переключения между ними используется метод SetTable интерфейса ICertView2. По умолчанию всегда используется таблица CVRC_TABLE_REQCERT. Вот какие кодовые номера у таблиц:

  • CVRC_TABLE_ATTRIBUTES = 0x4000
  • CVRC_TABLE_CRL = 0x5000
  • CVRC_TABLE_EXTENSIONS = 0x3000
  • CVRC_TABLE_REQCERT = 0

Метод 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. Вот значения для этих параметров:

  • CV_COLUMN_QUEUE_DEFAULT = –1
  • CV_COLUMN_LOG_DEFAULT = –2
  • CV_COLUMN_LOG_FAILED_DEFAULT = –3

Как видите, при использовании отрицательных значений мы задаём фильтр на уровне таблиц. Если хотим фильтровать на уровне столбцов (т.е. значение определённого столбца соответствует чему-то), значение этого аргумента должно быть натуральным и это значение должно быть равно номеру столбца. Как мы уже знаем, номер столбца можно получить при помощи метода GetColumnIndex.

Дальнейшие аргументы имеют смысл только если ColumnIndex является натуральным числом. SeekOperator задаёт уровень сравнения и они достаточно понятно расписаны в таблице. Единственное, что тут хочу отметить — это числовые значения операторов сравнения:

  • CVR_SEEK_NONE = 0x0 (должно быть указано, если ColumnIndex не является натуральным числом)
  • CVR_SEEK_EQ = 0x1
  • CVR_SEEK_LT = 0x2
  • CVR_SEEK_LE = 0x4
  • CVR_SEEK_GE = 0x8
  • CVR_SEEK_GT = 0x10

Далее идёт аргумент, задающий порядок сортировки:

  • CVR_SORT_NONE = 0
  • CVR_SORT_ASCEND = 1
  • CVR_SORT_DESCEND = 2

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

Возьмём пример, мы хотим получить все строки БД, у которых столбец 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 и вот какие полезные значения может принимать:

  • 9 – запрос ожидает одобрения (находится в папке Pending Requests);
  • 12 – архивированный сторонний сертификат;
  • 15 – обновление сертификата CA;
  • 16 – цепочка сертификатов текущего CA;
  • 20 – сертификат успешно выдан (находится в папке Issued Certificates);
  • 21 – сертификат отозван (находится в папке Revoked Certificates);
  • 30, 31 — запросы, хранящиеся в папке Failed Requests.

Поэтому если мы хотим получить выданные и неотозванные сертификаты на имя 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) и вот как они расшифровываются:

  • 1 = Long
  • 2 = DateTime
  • 3 = Binary
  • 4 = String

Если всё это затолкать в цикл 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
<...>

В следующей части (или частях, пока ещё не придумал) поговорим о том, как блуждать по самой БД.