Contents of this directory is archived and no longer updated.

Posts on this page:

Ссылки на другие материалы из этой серии:

В первой части мы кратенько рассмотрели процесс трансформации неуправляемых функций и структур в управляемые методы и объекты классов .NET. Прежде чем продолжать рассказывать про создание самоподписанного сертификата, я посчитал нужным поговорить о довольно важной вещи, как управление неуправляемой памятью. В CryptoAPI (равно как и в других подмножествах WinAPI) чаще всего используется 3 основных метода.

Получение данных неизвестного размера

Самый первый и самый простой — выгрузка данных прямо в заранее подготовленную переменную. Вот небольшой пример из моего буржуйского бложека — Get registered CSPs on the system. Выполним фрагмент кода до Add-Type включительно:

$signature = @"
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern bool CryptEnumProviders(
    uint dwIndex,
    uint pdwReserved,
    uint dwFlags,
    ref uint pdwProvType,
    System.Text.StringBuilder pszProvName,
    ref uint pcbProvName
);
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern bool CryptEnumProviderTypes(
    uint dwIndex,
    uint pdwReserved,
    uint dwFlags,
    ref uint pdwProvType,
    System.Text.StringBuilder pszTypeName,
    ref uint pcbTypeName
);
"@
Add-Type -MemberDefinition $signature -Namespace PKI -Name CSP

И посмотрим на функцию CryptEnumProviders, которая использовалась в том примере. Функция, как мы видим, выгружает данные в 3 аргумента сразу (которые помечены как __out и __inout). Если смотреть на определение функции в PowerShell, можно увидеть, что префикс ref установлен только на двух из них: pdwProvType и pcbProvName. А у pszProvName его нет. Почему? А потому что префикс ref можно ставить только для значений имеющих фиксированную длину. Например, для типов char, int, uint, byte и т.д. Для значений имеющих неопределённую длину, например, массивы простых типов данных, int[], char[], byte[]. Фактически в аргумент pszProvName выгружается массив байтов, которые потом StringBuilder сложит в строку. Но в неуправляемом мире всё очень сложно и программист должен заботиться обо всём, включая выделение памяти, изоляцию и освобождение памяти, когда данные больше не нужны. Там нет таких прекрасных товарищей как garbage collector. Следовательно здесь нам придётся вручную позаботиться об этом тоже.

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

Примечание: в CryptoAPI очень легко узнать функции использующие этот метод получения данных. У таких функций последний аргумент будет иметь приставку pcb<имя_аргумента>, cb<имя_аргумента> и определять размер данных, выгружаемых в предпоследний аргумент. А предпоследний будет иметь префикс pb. Вот табличка наиболее популярных префиксов используемых в C++:
http://www.cse.iitk.ac.in/users/dsrkg/cs245/html/Guide.htm

pdwProvType нас сейчас не интересует совсем. Значит, при первом запуске мы должны получить размер данных, которые будут выгружены в pszProvName. Как мы договорились, все переменные, которые используются как [ref] должны быть заранее объявлены:

$pdwProvType = 0
$pszTypeName = New-Object Text.StringBuilder
$pcbTypeName = 0

Т.к. мы первый раз запускаем функцию, мы не знаем сколько данных упадёт в pszTypeName, поэтому выставим его в null

[↓] [vPodans] [PKI.CSP]::CryptEnumProviderTypes(0,0,0,[ref]$pdwProvType,$null,[ref]$pcbTypeName)
True
[↓] [vPodans] $pcbTypeName
76
[↓] [vPodans]

Вот! Функция нам говорит, что в pszTypeName выгрузит 76 байт (хотя, на самом деле меньше, просто функции перестраховываются от переполнения буфера). Вот теперь очень важный момент. Нам нужно заранее выделить память под это дело. Памяти выделять не меньше, чем указано. Следовательно мы задаём размер для StringBuilder'а:

$pszTypeName.Length = $pcbTypeName

Если этого не сделать перед повторным вызовом функции, случится переполнение буфера и приложение, в контексте которого этот код исполняется (в нашем случае консоль PowerShell или редактор) тихо свалятся. Причём конкретное поведение отличается от систем. На моей домашней Windows 7 консоль просто крашится, а на рабочей Windows Server 2003 я получаю эксепшн (вполне себе нормальный), что случилось переполнение буфера и всё резко стало плохо. После того как мы выделили необходимую память под это, можно идти за самими данными. Поскольку данные теперь известной длины, заменяем $null на отмеренный объект StringBuilder:

[↓] [vPodans] $pszTypeName.Length = $pcbTypeName
[↓] [vPodans] [PKI.CSP]::CryptEnumProviderTypes(0,0,0,[ref]$pdwProvType,$pszTypeName,[ref]$pcbTypeName)
True
[↓] [vPodans] $pszTypeName

                               Capacity                             MaxCapacity                                  Length
                               --------                             -----------                                  ------
                                     76                              2147483647                                      37


[↓] [vPodans] $pszTypeName.ToString()
RSA Full (Signature and Key Exchange)
[↓] [vPodans]

На MSDN есть заметка на эту тему. Возможно она более понятна будет, чем моё объяснение: Retrieving Data of Unknown Length.

Если бы функция возвращала последовательный набор байтов, вызов функции был бы следующим:

# вычисляем размер возвращаемых данных в аргумент pbTypeName
[PKI.CSP]::CryptEnumProviderTypes(0,0,0,[ref]$pdwProvType,$null,[ref]$pcbTypeName)
# создаём массив байтов нужного размера
$pbTypeName = New-Object byte[] -ArgumentList $pcbTypeName
# выполняем ещё раз функцию и вместо $null подставляем только что созданный массив байтов
[PKI.CSP]::CryptEnumProviderTypes(0,0,0,[ref]$pdwProvType,$pbTypeName,[ref]$pcbTypeName)

Маршалинг — копирование данных в неуправляемую память

Иногда может потребоваться передать некоторые данные из управляемого кода в неуправляемую функцию. Например, в процессе создания сертификата мы создадим поле Subject. Это поле представляет X.500 distinguished name записанное в нотации ASN.1. Для этого есть соответствующие неуправляемые функции (CertNameToStr и CryptEncodeObject). Но как минимум на одной функции можно сэкономить, потому что у нас есть замечательный класс X500DistinguishedName у которого свойство RawData содержит как раз массив байтов записанных в нотации ASN.1. Однако, неуправляемые функции не поддерживают стандартные массивы .NET и требуется некий механизм, который сможет преобразовать массив байтов .NET в массив байтов пригодный для неуправляемых функций. Эти функции, как правило, используют структуры для хранения массивов данных.

Итак, смотрим описание аргумента pSubjectIssuerBlob для CertCreateSelfSignCertificate:

A pointer to a BLOB that contains the distinguished name (DN) for the certificate subject. This parameter cannot be NULL. Minimally, a pointer to an empty DN must be provided. This BLOB is normally created by using the CertStrToName function. It can also be created by using the CryptEncodeObject function and specifying either the X509_NAME or X509_UNICODE_NAME StructType.

Если речь идёт о BLOB'ах, значит это гарантированно будет структура CRYPTOAPI_BLOB. Как мы знаем, эта структура имеет два свойства: адрес (указатель) в памяти, где находятся данные и размер этих данных в байтах. Вот здесь нам понадобится маршалинг для копирования стандартного массива .NET в последовательность байтов в памяти. Весь этот процесс сводится к трём этапам:

  1. Получение массива байтов (или других данных. Просто в CryptoAPI вы чаще всего будете оперировать байтами);
  2. Выделение места в неуправляемой памяти под эти данные;
  3. Копирование данных в выделенную память.

В .NET есть просто замечательный класс: System.Runtime.InteropServices.Marshal. Можете посмотреть какая там пачка статических методов :-). Давайте проследуем указанную схему поэтапно. Сначала получим массив данных, которые будем потом записывать в неуправляемую память:

[↓] [vPodans] $Subject = [Security.Cryptography.X509Certificates.X500DistinguishedName]"CN=I am a cool user"
[↓] [vPodans] $Subject | ft -a

Name                Oid                              RawData
----                ---                              -------
CN=I am a cool user System.Security.Cryptography.Oid {48, 27, 49, 25...}


[↓] [vPodans]

Вы можете увидеть немного байтов в свойстве RawData. Это как раз те самые байты, записанные в нотации ASN.1 и которые нам нужно передать в функцию CertCreateSelfSignCertificate. Имея сам массив данных мы можем узнать их размер (размер массива) по свойству Count или Length. Следовательно мы можем выделить нужное количество памяти для хранения этого массива. Существует несколько методов выделения неуправляемой памяти, но я использую только один (нет никаких причин, просто мне он нравится) — AllocHGlobal(Int32), где в качестве аргумента говорим, сколько памяти надо выделить:

[↓] [vPodans] $ptrSubject = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($Subject.RawData.Length)
[↓] [vPodans] $ptrSubject
37641136 [↓] [vPodans]

Мы молучили указатель на адрес в памяти. Но, пока самих данных там нет. Мы воспользуемся методом Copy(array Byte [], Int32, IntPtr, Int32), чтобы скопировать .NET'овский массив в неуправляемую память:

[Runtime.InteropServices.Marshal]::Copy(
    $Subject.RawData, # указываем массив, который будем копировать
    0, # указываем стартовый индекс, если мы не весь массив хотим скопировать
    $ptrSubject, # указываем указатель на выделенную под эту операцию память
    $Subject.RawData.Length # количество байтов, которые будем копировать
)

Метод ничего не возвращает, кроме ошибок, следовательно ничего никуда присваивать не надо. Теперь можно сделать наш BLOB. Я уже говорил, как создаются объекты неуправляемых структур в PowerShell:

$ptrName = New-Object Quest.PowerGUI+CRYPTOAPI_BLOB -Property @{
    cbData = $Subject.RawData.Length;
    pbData = $ptrSubject
}

Как уже говорилось выше, в cbData мы указываем размер данных, а в pbData указываем указатель на память, где эти данные находятся. Вот этот объект ($ptrName) мы и подсунем в нашу исходную функцию.

Заметка: чтобы лучше понять смысл этой структуры представьте себе память, как линейную последовательность байтов. Структура CRYPTOAPI_BLOB показывает где находится начало конкретных данных (по указателю) и сколько байтов относятся именно к конкретным данным. Т.е. мы просто нарезаем память на кусочки и выбираем только те, которые нам нужны.

Но это ещё не всё. Поскольку в неуправляемой памяти нет таких товарищей, как garbage collector, вы должны самостоятельно освобождать выделенную память после её использования. Память освобождается в зависимости от метода её выделения. Если мы выделяем память через AllocHGlobal, освобождается она через FreeHGlobal:

[Runtime.InteropServices.Marshal]::FreeHGlobal($ptrSubject)

Этот метод возвращает True или False. Я обычно освобождаю всю память в конце исполняемого кода.

Маршалинг — копирование данных из неуправляемой памяти

Существует и обратная ситуация — скопировать данные из неуправляемой памяти. Например, мы получили некоторую структуру вида CRYPTOAPI_BLOB. В этом случае у нас уже есть фрагмент памяти, содержащий конкретные данные. Свойство pbData скажет нам, где находится начало данных и сколько их. Для этого мы можем воспользоваться методом Copy класса Marshal, но с другим конструктором, а именно — Copy(IntPtr, array<Byte>[], Int32, Int32). Одно из отличий этого метода является следующая ремарка:

Unmanaged, C-style arrays do not contain bounds information, which prevents the startIndex and length parameters from being validated. Thus, the unmanaged data corresponding to the source parameter populates the managed array regardless of its usefulness. You must initialize the managed array with the appropriate size before calling this method.

Я вам предлагаю простой примерчик. Скопировать массив байтов в неуправляемую память и прочитать её обратно. Вот код примера:

$Subject = [Security.Cryptography.X509Certificates.X500DistinguishedName]"CN=I am a cool user"
# запись данных в неуправляемую память
$ptr = [Runtime.InteropServices.Marshal]::AllocHGlobal($Subject.RawData.Length)
[Runtime.InteropServices.Marshal]::Copy($Subject.RawData,0,$ptr,$Subject.RawData.Length)
# чтение данных из неуправляемой памяти
$bytes = New-Object byte[] -ArgumentList $Subject.RawData.Length
[Runtime.InteropServices.Marshal]::Copy($ptr,$bytes,0,$Subject.RawData.Length)
# освобождение памяти
[Runtime.InteropServices.Marshal]::FreeHGlobal($ptr)

в результате в переменной $bytes должен быть точно такой же массив, что и в свойстве RawData переменной Subject.

Я очень допускаю, что с первого раза что-то будет непонятно. Но это самые основы, которые нужно знать, если вы хотите самостоятельно работать с p/invoke. Лёгкой жизни никто никому не обещал, но если понять описанные здесь моменты, можно значительно облегчить себе жизнь. В следующей части я расскажу ещё что-нибудь интересное :-)

Ссылки на другие материалы из этой серии:

Когда-то давно я писал о том, как можно создать самоподписанный сертификат в Windows Vista и более новых системах при помощи интерфейсов CertEnroll: Создание самоподписанного сертификата средствами PowerShell. И недавно для обновления адд-она PowerGUI Script Editor — Script Signing я написал более обновлённый вариант, который использует WinAPI функции. Этот код может работать и в Windows XP.

Функции

Итак, основная функция для этого будет функция CertCreateSelfSignCertificate. Но прежде чем её использовать нам надо получить хэндл к криптопровайдеру, который можно получить при помощи CryptAcquireContext. Так же нам понадобятся следующие функции: CryptGenKey для генерации пары ключей, CryptExportPublicKeyInfo для экспорта открытого ключа (нужен будет для подсчёта хеша для расширения Subject Key Identifier), CryptHashPublicKeyInfo для подсчёта хеша открытого ключа, CryptEncodeObject для кодировки стандартного формата данных в нотацию ASN.1. Так же потребуется одна небольшая функция, которая сконвертирует FileTime в SystemTime (этот формат нужен для CertCreateSelfSignCertificate). И функции освобождения памяти: CryptDestroyKey для выгрузки закрытого ключа из памяти, CryptReleaseContext для выгрузки хэндла (контекста) криптопровайдера. В итоге список функций выглядит так:

Самый первый и очевидный вопрос: как добраться до этих функций? И какой будет синтаксис в PowerShell? Существует система, называемая Platform Invocation Service или просто p/invoke. При помощи неё мы можем вызывать неуправляемые функции в управляемом коде и получать соответствующий профит. В первую очередь каждую функцию нужно соответствующим образом объявить. Объявления многих функций и структур можно найти на p/invoke.net. Если там нет, придётся это делать самим. Объявление, как правило, использует следующий синтаксис:

[DllImport("DllFileName.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern ReturnType FunctionName(
DataType ArgumentName;
DataType2 Argument2Name;
DataType3 Argument3Name;
);

Например, вот как выглядит объявление функции CryptReleaseContext в C++:

BOOL WINAPI CryptReleaseContext(
  __in  HCRYPTPROV hProv,
  __in  DWORD dwFlags
);

BOOL (Boolean) означает тип возвращаемого значения. Как правило функции WinAPI возвращают Boolean (но не всегда) указывая статус вызова функции, успешно или неуспешно. Далее мы видим объявление аргументов функции. __in означает, что это входной аргумент, __out означает, что это выходной аргумент. Т.е. функция в ходе своего выполнения будет что-то записывать в переменную, которая указана в этом аргументе. __inout означает что этот аргумент может быть как входным, так и выходным. __opt означает опциональный аргумент и может быть выставлен в null.

Теперь более сложное — тип данных. Неуправляемые типы значительно отличаются от привычных типов, как int, string, byte, но можно подобрать соответствующий аналог в .NET. В конкретном примере мы видим, что аргумент hProv имеет тип HCRYPTPROV. Соответствующего аналога в .NET вы не найдёте. Следовательно, здесь можно пойти двумя путями:

  1. объявить соответствующую структуру и использовать её в качестве типа данных;
  2. использовать указатель IntPtr. Достаточно часто нам не нужно работать непосредственно с этими структурами и нам достаточно данные этой структуры держать в памяти. IntPtr будет указывать на адрес в памяти, где хранится соответствующая структура, следовательно, объявлять саму структуру не нужно.

Тип DWORD — это 32-разрядное число и имеет соответствующий аналог в .NET — int, который представляет собой тоже 32-разрядное число.

Следовательно объявление этой функции в PowerShell будет иметь следующий вид:

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool CryptReleaseContext(
    IntPtr hProv,
    int flags
);

Давайте посмотрим на ещё одну функцию:

BOOL WINAPI CryptGenKey(
  __in   HCRYPTPROV hProv,
  __in   ALG_ID Algid,
  __in   DWORD dwFlags,
  __out  HCRYPTKEY *phKey
);

В данном случае мы можем смело указать первый аргумент: IntPtr hProv. Второй аргумент должен иметь тип ALG_ID. Здесь нам не удастся использовать указатель IntPtr, потому что нам нужно будет явно указывать значение аргумента. Если мы укажем IntPtr, нам придётся создать соответствующую структуру и скопировать её в память, чтобы получить адрес указателя. Структура ALG_ID достаточно проста и состоит из одного 32-разрядного числа. Следовательно мы можем второй аргумент описать как: int Algid. Третий аргумент нам знаком и опишем его как: int dwFlags.

С четвёртым аргументом тоже всё просто, кроме одного момента. Структура HCRYPTKEY нам не нужна и достаточно знать адрес структуры в памяти, чтобы передать этот адрес в другую функцию. Но это выходной аргумент, т.е. при вызове функции аргумент не должен иметь значения и функция в процессе работы запишет данные в эту функцию. В p/invoke все выходные аргументы помечаются как ref (reference) и переменная, которая будет принимать возвращаемое функцией значение должна быть объявлена заранее и может иметь нулевое значение. Для числовых значений нулевое значение будет 0, для строковых — пустая строка "", для указателей — специальное статическое свойство класса IntPtr::Zero и т.д.

Следовательно мы можем объявить функцию:

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool CryptGenKey(
    IntPtr phProv,
    int Algid,
    int dwFlags,
    ref IntPtr phKey
);

А как же должна быть объявлена функция CertCreateSelfSignCertificate? Вот её объявление в C++:

PCCERT_CONTEXT WINAPI CertCreateSelfSignCertificate(
  __in_opt  HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hCryptProvOrNCryptKey,
  __in      PCERT_NAME_BLOB pSubjectIssuerBlob,
  __in      DWORD dwFlags,
  __in_opt  PCRYPT_KEY_PROV_INFO pKeyProvInfo,
  __in_opt  PCRYPT_ALGORITHM_IDENTIFIER pSignatureAlgorithm,
  __in_opt  PSYSTEMTIME pStartTime,
  __in_opt  PSYSTEMTIME pEndTime,
  __opt     PCERT_EXTENSIONS pExtensions
);

Здесь мы видим, что функция возвращает не Boolean, а структуру PCCERT_CONTEXT, которая содержит наш итоговый сертификат. Здесь так же, нам может быть достаточно указателя на адрес памяти, содержащий эту структуру или объявлять структуру и разбирать её по частям. В нашем случае нам хватает указателя на адрес памяти, поскольку у X509Certificate2 есть хороший конструктор: X509Certificate2(IntPtr). Т.е. выдёргивать данные из памяти придётся в любом случае (чтобы увидеть наш сертификат и его свойства), но мы возложим эту задачу на класс X509Certificate2.

Структуры

В функции используется довольно много структур, которые нам всё-таки придётся объявлять и собирать в коде вручную: CRYPT_KEY_PROV_INFO, CRYPT_ALGORITHM_IDENTIFIER, SYSTEMTIME и CERT_EXTENSIONS. Забегая вперёд, заранее привожу список структур, которые нам потребуются:

  • CRYPT_KEY_PROV_INFO (будет представлять собой набор параметров для генерации ключевой пары)
  • CERT_EXTENSIONS (будет представлять собой массив расширений сертификата и являться аналогом структуры CRYPTOAPI_BLOB)
  • CERT_EXTENSION (будет представлять собой одно расширение)
  • CERT_BASIC_CONSTRAINTS2_INFO (специальная структура для расширения Basic Constraints)
  • CRYPTOAPI_BLOB (универсальная структура для хранения любых типов данных, чаще — массивы байт)
  • CRYPT_BIT_BLOB (структура для хранения только открытого ключа)
  • CERT_PUBLIC_KEY_INFO (структура для хранения открытого ключа, алгоритма и параметров)
  • CRYPT_ALGORITHM_IDENTIFIER (структура представляющая объектный идентификатор, OID)
  • SystemTime (структура представляющая некоторый аналог класса DateTime)

Структуры по сути являются классом с набором свойств и имеют следующий формат объявляения:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct StructureName {
    public PropertyType PropertyName;
    public Property2Type Property2Name;
}

Например, следующая структура (кстати говоря, самая популярная в CryptoAPI) CRYPTOAPI_BLOB объявляется в С++ следующим образом:

typedef struct _CRYPTOAPI_BLOB {
  DWORD cbData;
  BYTE  *pbData;
} CRYPT_INTEGER_BLOB

Звёздочка у свойства pbData означает, что это указатель на адрес в памяти, где находится соответствующий массив байтов. Свойство cbData указывает на размер этого массива в байтах. Следовательно, эта структура будет описываться в PowerShell следующим образом:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CRYPTOAPI_BLOB {
    public int cbData;
    public IntPtr pbData;
}

Некоторые структуры могут содержать свойства, представляющие другие структуры. Например, CERT_PUBLIC_KEY_INFO. Эта структура состоит из двух свойств: алгоритм, используемый при генерации ключа и самого открытого ключа. Алгоритм в свою очередь состоит из двух свойств: OID алгоритма и параметры идентификатора. А открытый ключ представлен структурой с 3-мя свойствами: длина ключа в байтах, адрес памяти, который содержит сам ключ в виде байтового массива и количества неиспользуемых битов (это вообще отдельная история и заморачиваться этим не нужно). Вот как будут объявляться эти структуры:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CRYPT_BIT_BLOB {
    public uint cbData;
    public IntPtr pbData;
    public uint cUnusedBits;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CERT_PUBLIC_KEY_INFO {
    public CRYPT_ALGORITHM_IDENTIFIER Algorithm;
    public CRYPT_BIT_BLOB PublicKey;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CRYPT_ALGORITHM_IDENTIFIER {
    public String pszObjId;
    public CRYPTOAPI_BLOB Parameters;
}

При описании и объявлении функций и структур следует очень внимательно читать справку MSDN, чтобы правильно это сделать. Бывает, что одну и ту же функцию нужно объявлять несколько раз и у каждой из них какой-то аргумент (или несколько) будут иметь разный тип данных. Это как в конструкторах классов .NET. Например, класс X509Certificate2. У этого класса несколько конструкторов, с одним аргументом. Но каждый тип конструктора принимает свой тип данных.

Оформление

В отличии от .NET, где все классы рассортированы по пространствам имён, функции и структуры WinAPI являются линейными, т.е. всё в одной куче без классов и пространств имён. .NET и p/invoke позволяют создать произвольный класс в произвольном пространстве имён и приаттачить объявленные функции и структуры к этому классу. Вы можете функции и структуры сортировать по разным классам. И вот как финально будет выглядеть объявление наших функций и структур в PowerShell:

$signature = @"
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool CryptAcquireContext(
   ref IntPtr phProv,
   string pszContainer,
   string pszProvider,
   uint dwProvType,
   Int64 dwFlags
);
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool CryptReleaseContext(
    IntPtr phProv,
    int flags
);
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool CryptGenKey(
    IntPtr phProv,
    int Algid,
    int dwFlags,
    ref IntPtr phKey
);
[DllImport("Crypt32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool CryptExportPublicKeyInfo(
    IntPtr phProv,
    int dwKeySpec,
    int dwCertEncodingType,
    IntPtr pbInfo,
    ref int pcbInfo
);
[DllImport("Crypt32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool CryptHashPublicKeyInfo(
    IntPtr phProv,
    int Algid,
    int dwFlags,
    int dwCertEncodingType,
    IntPtr pInfo,
    IntPtr pbComputedHash,
    ref int pcbComputedHash
);
[DllImport("Crypt32.dll", SetLastError=true)]
public static extern bool CryptEncodeObject(
    int dwCertEncodingType,
    [MarshalAs(UnmanagedType.LPStr)]string lpszStructType,
    ref CRYPTOAPI_BLOB pvStructInfo,
    byte[] pbEncoded,
    ref int pcbEncoded
);

[DllImport("Crypt32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern IntPtr CertCreateSelfSignCertificate(
    IntPtr phProv,
    CRYPTOAPI_BLOB pSubjectIssuerBlob,
    int flags,
    CRYPT_KEY_PROV_INFO pKeyProvInfo,
    IntPtr pSignatureAlgorithm,
    SystemTime pStartTime,
    SystemTime pEndTime,
    CERT_EXTENSIONS pExtensions
);
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool CryptDestroyKey(
    IntPtr cryptKeyHandle
);
[DllImport("kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool FileTimeToSystemTime(
    [In] ref long fileTime,
    out SystemTime SystemTime
);

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CRYPT_KEY_PROV_INFO {
    public string pwszContainerName;
    public string pwszProvName;
    public int dwProvType;
    public int dwFlags;
    public int cProvParam;
    public IntPtr rgProvParam;
    public int dwKeySpec;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CERT_EXTENSIONS {
    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;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CERT_BASIC_CONSTRAINTS2_INFO {
    public Boolean fCA;
    public Boolean fPathLenConstraint;
    public int dwPathLenConstraint;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CRYPTOAPI_BLOB {
    public int cbData;
    public IntPtr pbData;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CRYPT_BIT_BLOB {
    public uint cbData;
    public IntPtr pbData;
    public uint cUnusedBits;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CERT_PUBLIC_KEY_INFO {
    public CRYPT_ALGORITHM_IDENTIFIER Algorithm;
    public CRYPT_BIT_BLOB PublicKey;
}
[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 SystemTime {
    public short Year;
    public short Month;
    public short DayOfWeek;
    public short Day;
    public short Hour;
    public short Minute;
    public short Second;
    public short Milliseconds;
}
"@

Я их оформил в here-string. Теперь нужно создать класс в пространстве имён и прицепить всё это к этому классу:

Add-Type -MemberDefinition $signature -Namespace Quest -Name PowerGUI

Вызов функций и создание управляемых объектов

Как видно, пространство имён будет Quest и имя класса PowerGUI. Как видно из описания функций (public static blablabla) эти функции станут статическими методами класса и вызываться через двоеточие после названия класса:

[Quest.PowerGUI]::CryptAcquireContext(blablabla)

А вот структуры создаются немного хитрее. Я не знаю почему так (всё же, я не программист), но при создании структуры имя структуры указывается через знак «+» после названия: класса, т.е.:

New-Object Quest.PowerGUI+CRYPT_KEY_PROV_INFO

Структура как тип указывается так же (как и класс):

[Quest.PowerGUI+CERT_EXTENSION]

Кстати говоря, то, что не подсвечивается название структуры — это баг в подсветке PowerGUI (а в ISE этого бага нет).

На сегодня всё. В следующей части я расскажу о принципом управления неуправляемой памятью, включая приём данных из функций. Это, можно сказать, основы практической работы с p/invoke.

Давным давно я мечтал об удобном (ну или не очень, но чтобы стандартном) способе работы с 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) не будет больше, чем текущее время. На сегодня всё.