Contents of this directory is archived and no longer updated.

Posts on this page:

Я вчера в статье Certificate Trust List (CTL) в PowerShell показал основные принципы, как работать с CTL в PowerShell. Сегодня я продолжу тему и покажу, как можно вытащить доверенные корневые сертификаты из ресурсов Crypt32.dll и из интернетов. Но для начала, Crypt32.dll

Вот как они выглядят в каком-нибудь редакторе ресурсов:


Read more →

Подумалось, что стоит сделать итоговый пост по серии статей, в которых мы рассматривали принцип работы с p/invoke в Windows PowerShell на примере создания самоподписанных сертификатов. Здесь я оставляю ссылки на все части:

В этой части рассказывается об основных принципах работы с p/invoke, описание неуправляемых функций и структур.

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

эта часть повествует о работе с криптопровайдерами (CSP) и процессе генерации криптографических ключей.

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

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

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

 

Для решения этой задачи можно использовать различные API. Например, можно каждое расширение создавать средствами неуправляемых функций CryptoAPI, что значительно увеличит размер кода. А можно сделать 50/50 и какие-то вещи делать при помощи .NET и потом их заворачивать в неуправляемый код. Мы уже применяли такой ход, когда конструировали Subject сертификата. Поскольку для многих расширений (часто используемых) сертификатов есть соответствующие классы в .NET, при помощи которых мы получим значение расширений в виде байтового массива, записанного в нотации ASN.1. Сначала мы создадим пустую коллекцию расширений на основе класса X509ExtensionCollection и потом будем в неё добавлять наши расширения:

$Extensions = New-Object Security.Cryptography.X509Certificates.X509ExtensionCollection

Basic Constraints

Basic Constraints — это самое простое расширение и говорит о типе получателя сертификата — CA сервер или конечный потребитель (пользователь, служба или устройство). Для этого расширения у нас есть класс X509BasicConstraintsExtension и конструктор X509BasicConstraintsExtension(Boolean, Boolean, Int32, Boolean):

[void]$Extensions.Add((New-Object Security.Cryptography.X509Certificates.X509BasicConstraintsExtension $false,$false,0,$false))

В таком виде у нас расширение будет для конечного потребителя (CA будет промежуточным):

[↓] [vPodans] $Extensions[0].format(1)
Subject Type=End Entity
Path Length Constraint=None

[↓] [vPodans]

Enhanced Key Usage

Довольно понятное расширение, которое отвечает за целевое назначение сертификата. Мы договорились, что будем делать сертификат для цифровой подписи. Вот его и создадим при помощи класса X509EnhancedKeyUsageExtension:

$OIDs = New-Object Security.Cryptography.OidCollection
[void]$OIDs.Add("code signing")
[void]$Extensions.Add((New-Object Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension -ArgumentList $OIDs, $false))

Key Usages

Key Usages отвечает за политику применения ключа — цифровая подпись, шифрование, обмен ключами и т.д. В нашем случае всё просто, т.к. мы делаем сертификат для цифровых подписей, следовательно, будет Signature:

[void]$Extensions.Add((New-Object Security.Cryptography.X509Certificates.X509KeyUsageExtension -ArgumentList "DigitalSignature", $true))

Subject Key Identifier

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

  1. Извлечь открытый ключ в виде байтового массива при помощи функции CryptExportPublicKeyInfo;
  2. Посчитать хеш (SHA1) экспортированного открытого ключа при помощи функции CryptHashPublicKeyInfo;
  3. Записать полученное значение в нотации ASN.1 при помощи функции CryptEncodeObject.

Вот как будет выглядеть код:

# инициализируем переменную для хранения длины открытого ключа в байтах
$pcbInfo = 0
# вычисляем размер открытого ключа в байтах при помощи функции CryptExportPublicKeyInfo
if (([Quest.PowerGUI]::CryptExportPublicKeyInfo($phProv,2,1,[IntPtr]::Zero,[ref]$pcbInfo))) {
    # выделяем область памяти в неуправляемой памяти для хранения открытого ключа.
    $pbInfo = [Runtime.InteropServices.Marshal]::AllocHGlobal($pcbInfo)
    # вызываем функцию ещё раз, но при этом указываем куда экспортировать открытый ключ.
    # в данном случае мы его экспортируем в зарезервированный участок памяти.
    $Return = [Quest.PowerGUI]::CryptExportPublicKeyInfo($phProv,2,1,$pbInfo,[ref]$pcbInfo)
    # снова инициализируем переменную для хранения длины полученного хеша в байтах
    $pcbComputedHash = 0
    # вычисляем размер посчитанного хеша SHA1
    if (([Quest.PowerGUI]::CryptHashPublicKeyInfo([IntPtr]::Zero,0,0,1,$pbInfo,[IntPtr]::Zero,[ref]$pcbComputedHash))) {
        # выделяем кусок памяти для хранения этого хеша
        $pbComputedHash = [Runtime.InteropServices.Marshal]::AllocHGlobal($pcbComputedHash)
        # повторно вызываем функцию CryptHashPublicKeyInfo и указывем куда записывать
        # полученный хеш
        [void][Quest.PowerGUI]::CryptHashPublicKeyInfo([IntPtr]::Zero,0,0,1,$pbInfo,$pbComputedHash,[ref]$pcbComputedHash)
        # создаём структуру CRYPTOAPI_BLOB для описания местоположения хеша в памяти.
        # в cbData указываем размер полученного хеша, а в pbData указываем указатель
        # на область памяти, где этот хеш хранится
        $uSKI = New-Object Quest.PowerGUI+CRYPTOAPI_BLOB -Property @{
            cbData = $pcbComputedHash;
            pbData = $pbComputedHash
        }
        # инициализируем переменную для хранения длины значения расширения, записанного в нотации ASN.1
        $pcbEncoded = 0
        # вычисляем размер значения расширения в байтах
        if (([Quest.PowerGUI]::CryptEncodeObject(1,"2.5.29.14",[ref]$uSKI,$null,[ref]$pcbEncoded))) {
            # поскольку нам нужно будет получить сам массив байтов для инициализации конструктора
            # X509SubjectKeyIdentifierExtension(AsnEncodedData, Boolean), мы не будем резервировать
            # место в неуправляемой памяти, а создадим байтовый массив в упарвляемой памяти
            $pbEncoded = New-Object byte[] -ArgumentList $pcbEncoded
            # и экспортируем наше расширение прямо в этот массив
            $Return = [Quest.PowerGUI]::CryptEncodeObject(1,"2.5.29.14",[ref]$uSKI,$pbEncoded,[ref]$pcbEncoded)
            # повторяем процедуру, чтобы создать класс AsnEncodedData. Этот класс нам нужен только для того, чтобы
            # использовать конструктор X509SubjectKeyIdentifierExtension(AsnEncodedData, Boolean). Фвктически
            # ничего пересчитываться не будет, просто мы сделаем приведение типов.
            $AsnEncodedData = New-Object Security.Cryptography.AsnEncodedData -ArgumentList "2.5.29.14", $pbEncoded
            # и вот теперь создаём управляемое расширение и добавляем его в нашу коллекцию.
            [void]$Extensions.Add((New-Object Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension -ArgumentList $AsnEncodedData, $false))
        }
    }
}

И вот результат работы:

[↓] [vPodans] $Extensions[1].format(0)
9f b8 3a ea 14 86 50 6c 28 f3 7b 6b 01 ed e5 91
[↓] [vPodans]

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

Теперь у нас есть массив расширений, представленных в виде объектов .NET. Но это нам не годится, поскольку для неуправляемых функций нужно использовать неуправляемые структуры. Для начала мы каждое расширение из объектов .NET переведём в массив структур CERT_EXTENSION:

# создаём пустой массив
$uExtensionCollection = @()
# начинаем итерацию каждого расширения в массиве расширений X509ExtensionCollection
foreach ($mExt in $Extensions) {
    # создаём объект структуры CERT_EXTENSION
    $uExtension = New-Object Quest.PowerGUI+CERT_EXTENSION
    # переносим OID расширения и флаг критичности расширения
    $uExtension.pszObjId = $mExt.Oid.Value
    $uExtension.fCritical = $mExt.Critical
    # создаём структуру CRYPTOAPI_BLOB, которая будет хранить значение расширения, которое
    # у нас в виде байтового массива
    $value = New-Object Quest.PowerGUI+CRYPTOAPI_BLOB
    # сразу записываем длину значения расширения в байтах в свойство cbData
    $value.cbData = $mExt.RawData.Length
    # выделяем область памяти для хранения значения расширения
    $value.pbData = [Runtime.InteropServices.Marshal]::AllocHGlobal($value.cbData)
    # копируем байтовый массив в только что выделенную память
    [Runtime.InteropServices.Marshal]::Copy($mExt.RawData,0,$Value.pbData,$Value.cbData)
    # добавляем свойство Value структуры CERT_EXTENSION
    $uExtension.Value = $value
    # и добавляем полученную структуру в массив
    $uExtensionCollection += $uExtension
}

Вот так мы получили массив структур CERT_EXTENSION. Однако, в p/invoke и неуправляемом мире массивы структур, как и любые массивы представляются в виде нескольких последовательных блоков в памяти. Следовательно, нам нужно выполнить последовательность действий:

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

Структура CERT_EXTENSIONS очень похожа на CRYPTOAPI_BLOB и свойство cExtensions содержит количество расширений, а rgExtensions указатель на начальную точку в памяти, где хранятся эти расширения. Вот как массив управляемых объектов превращается в массив неуправляемых:

# создаём объект структуры CERT_EXTENSIONS
$uExtensions = New-Object Quest.PowerGUI+CERT_EXTENSIONS
# при помощи Marshal.SizeOf вычисляем размер каждой структуры CERT_EXTENSION и умножаем на количество
# расширений, которое у нас будет
$ExtensionSize = [Runtime.InteropServices.Marshal]::SizeOf([Quest.PowerGUI+CERT_EXTENSION]) * $Extensions.Count
$uExtensions.cExtension = $Extensions.Count
# выделяем последовательный блок памяти для хранения всех расширений
$uExtensions.rgExtension = [Runtime.InteropServices.Marshal]::AllocHGlobal($ExtensionSize)
# начинаем итерацию с каждым расширением в массиве X509ExtensionCollection
for ($n = 0; $n -lt $Extensions.Count; ++$n) {
    # вычисляем начальный адрес, в котором будет храниться расширение.
    # допустим, размер каждого расширения у нас будет 32 байта (размер структуры CERT_EXTENSION
    # значит, для первого расширения начальный адрес будет 0. Для второго расширения начальный
    # адрес будет 32, для третьего - 64 и т.д.
    $offset = $n * [Runtime.InteropServices.Marshal]::SizeOf([Quest.PowerGUI+CERT_EXTENSION])
    # поскольку у нас выделен последовательный блок памяти, мы сдвигаемся на этот размер (32 байта)
    # относительно нашего указателя памяти.
    $next = $offset + $uExtensions.rgExtension.ToInt64()
    [IntPtr]$NextAddress = New-Object IntPtr $next
    # и при помощи Marshal.StructureToPtr мы копируем неуправляемую структуру в указанный адрес
    [Runtime.InteropServices.Marshal]::StructureToPtr($uExtensionCollection[$n],$NextAddress,$false)
}

Чтобы более чётко понимать, что мы делаем, просто представьте себе ленту неограниченного размера. На этой ленте нам надо разместить несколько отрезков фиксированной длины по 32 см. При этом использовать ленту максимально эффективно. Что мы делаем? Мы выясняем количество отрезков. Зная количество отрезков и длину каждого, мы просто перемножаем эти значения и получаем суммарную длину ленты, которая нам понадобится. А потом мы просто последовательно размещаем наши отрезки к выделенной длине ленты.

Конвертирование времени

В .NET у нас есть замечательный класс DateTime, но который не поддерживается неуправляемыми функциями. Они охотнее понимают или FileTime или SystemTime. Засада с SystemTime заключается в том, что есть только одна функция, которая что-то корректно переводит в структуру SystemTimeFileTimeToSystemTime. Но нам повезло, что у DateTime есть метод DateTime.ToFileTime. Поэтому мы стандартный DateTime конвертируем в FileTime и функцией FileTimeToSystemTime преобразовываем FileTime в SystemTime. Выглядит как изврат, но более гуманного метода я не знаю. Итак, нам осталось указать начальный и конечный срок действия сертификата:

# создаём объект структуры, которая будет представлять собой начало действия сертификата
$pStartTime = New-Object Quest.PowerGUI+SystemTime
# конвертируем время объекта DateTime в SystemTime
[void][Quest.PowerGUI]::FileTimeToSystemTime([ref]$ValidFrom.ToFileTime(),[ref]$pStartTime)
# создаём объект структуры, которая будет представлять собой конец действия сертификата
$pEndTime = New-Object Quest.PowerGUI+SystemTime
# конвертируем время объекта DateTime в SystemTime
[void][Quest.PowerGUI]::FileTimeToSystemTime([ref]$ValidTo.ToFileTime(),[ref]$pEndTime)

Это очень просто.

Создание сертификата

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

[Quest.PowerGUI]::CertCreateSelfSignCertificate($phProv,$ptrName,0,$PrivateKey,[IntPtr]::Zero,$pStartTime,$pEndTime,$uExtensions)
[↓] [vPodans] [Quest.PowerGUI]::CertCreateSelfSignCertificate($phProv,$ptrName,0,$PrivateKey,[IntPtr]::Zero,$pStartTime,
$pEndTime,$uExtensions)
4803696
[↓] [vPodans]

Фуцк! Эти циферки явно не похожи на сертификат. Однако, на самом деле, это очень похоже на сертификат:

The CertCreateSelfSignCertificate function builds a self-signed certificate and returns a pointer to a CERT_CONTEXT structure that represents the certificate.

У нас есть указатель на сертификат в памяти. Идём на MSDN и находим конструктор у класса X509Certificate2X509Certificate2(IntPtr) (Initializes a new instance of the X509Certificate2 class using an unmanaged handle.). Это то, что нам нужно:

[↓] [vPodans] New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 ([IntPtr]4803696)

Thumbprint                                Subject
----------                                -------
BAE4769BDEA62AC1222A5A92A283BF356E3B78BE  CN=PowerGUI User


[↓] [vPodans] New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 ([IntPtr]4803696) | fl *


Archived           : False
Extensions         : {System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, System.Security.Cryptography.
                     Oid, System.Security.Cryptography.Oid}
FriendlyName       :
IssuerName         : System.Security.Cryptography.X509Certificates.X500DistinguishedName
NotAfter           : 09.06.2012 20:38:20
NotBefore          : 09.06.2011 20:38:13
HasPrivateKey      : True
PrivateKey         : System.Security.Cryptography.RSACryptoServiceProvider
PublicKey          : System.Security.Cryptography.X509Certificates.PublicKey
RawData            : {48, 130, 3, 7...}
SerialNumber       : 68A85F62F9626E8443D6642C2BBBAF19
SubjectName        : System.Security.Cryptography.X509Certificates.X500DistinguishedName
SignatureAlgorithm : System.Security.Cryptography.Oid
Thumbprint         : BAE4769BDEA62AC1222A5A92A283BF356E3B78BE
Version            : 3
Handle             : 4803696
Issuer             : CN=PowerGUI User
Subject            : CN=PowerGUI User



[↓] [vPodans] (New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 ([IntPtr]4803696)).Extensions |
 %{$_.format(1)}
Subject Type=End Entity
Path Length Constraint=None

Code Signing (1.3.6.1.5.5.7.3.3)

9f b8 3a ea 14 86 50 6c 28 f3 7b 6b 01 ed e5 91

Digital Signature (80)

[↓] [vPodans]

Эпик! :rock: мы его сделали! Я даже показал расширения сертификатов и вы можете убедиться, что они отвечают нашим требованиям.

Освобождение ресурсов

Но это ещё не всё. Мы использовали много разной памяти и она нам более не нужна и следует вернуть:

foreach ($uExt in $uExtensionCollection) {[void][Runtime.InteropServices.Marshal]::FreeHGlobal($uExt.Value.pbData)}
[void][Runtime.InteropServices.Marshal]::FreeHGlobal($ptrSubject)
[void][Runtime.InteropServices.Marshal]::FreeHGlobal($uExtensions.rgExtension)
[void][Runtime.InteropServices.Marshal]::FreeHGlobal($pbInfo)
[void][Runtime.InteropServices.Marshal]::FreeHGlobal($pbComputedHash)
[void][Quest.PowerGUI]::CryptDestroyKey($phKey)
[void][Quest.PowerGUI]::CryptReleaseContext($phProv,0)

Работая с p/invoke всегда следите за тем, что если где-то выделяете ресурсы, их нужно потом высвобождать. После этого вы можете смело делать с объектом X509Certificate2 что хотите. Хотите, экспортируйте в PFX, хотите — устанавливайте в хранилище, он ваш. И напоследок, финальный скрипт (правда, без комментариев):

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

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

Обращение к криптопровайдеру

Итак, снова посмотрим на описание функции CertCreateSelfSignCertificate и посмотрим, что нам надо в качестве аргументов функции. Во-первых, это hCryptProvOrNCryptKey. Хоть и написано, что этот аргумент опциональный, мы его будем использовать. Чтобы получить хэндл криптопровайдера (CSP) мы воспользуемся универсальной функцией CryptAcquireContext. На выходе этой функции (в аргументе phProv) мы получим наш хэндл. Чтобы запустить функцию нужно указать имя контейнера (контейнер криптопровайдера, а не хранилища сертификатов. В качестве имени можно использовать что угодно, лишь бы это имя было уникальным для конкретного CSP. В качестве уникального имени я предпочитаю использовать рандомный GUID. Далее, нам нужно указать имя криптопровайдера. Мы не будем использовать что-то космическое и обойдёмся вполне стандартным Microsoft Base Cryptographic Provider v1.0. Тип провайдера у нас будет RSA_FULL — 0x1 (RSA Full (Signature and Key Exchange) ). В dwFlags мы будем использовать CRYPT_NEWKEYSET — 0x8 (мы всё же будем создавать ключи). Итак, определяем аргументы и выполняем функцию:

$pszContainer = [Guid]::NewGuid().ToString()
[IntPtr]$phProv = [IntPtr]::Zero
$Provider = "Microsoft Base Cryptographic Provider v1.0"
$Result = [Quest.PowerGUI]::CryptAcquireContext([ref]$phProv,$pszContainer,$Provider,0x1,0x8)
[↓] [vPodans] $pszContainer = [Guid]::NewGuid().ToString()
[↓] [vPodans] [IntPtr]$phProv = [IntPtr]::Zero
[↓] [vPodans] $Provider = "Microsoft Base Cryptographic Provider v1.0"
[↓] [vPodans] $Result = [Quest.PowerGUI]::CryptAcquireContext([ref]$phProv,$pszContainer,$Provider,0x1,0x8)
[↓] [vPodans] $Result
True
[↓] [vPodans] $phProv
484709040
[↓] [vPodans]

Мы создали нужные аргументы и запустили функцию. Я проверил, что статус вызова True, т.е. с высокой долей вероятности всё прошло успешно. И заглянул в переменную $phProv, чтобы убедиться, что там есть наш хэндл. Если там 0 или отрицательное число, можете быть уверены, что это не хэндл и что-то сделано неправильно. Всё, с этой функцией закончили, у нас есть хэндл, все счастливы и довольны.

Получение имени сертификата (Subject и Issuer)

Второй аргумент для CertCreateSelfSignCertificate является pSubjectIssuerBlob. Это должно быть имя в виде байтового массива и записанного в нотации ASN.1. На MSDN нам предлагают функцию CertStrToName. Но мы не будем её использовать, потому что у нас есть замечательный класс X500DistinguishedName, который это всё сделает:

[↓] [vPodans] $Subject = [System.Security.Cryptography.X509Certificates.X500DistinguishedName]"CN=Hello World"
[↓] [vPodans] $Subject

Name                                    Oid                                     RawData
----                                    ---                                     -------
CN=Hello World                          System.Security.Cryptography.Oid        {48, 22, 49, 20...}


[↓] [vPodans]

Мы видим кусочек данных в свойстве RawData. Это как раз и есть тот самый массив байтов в нотации ASN.1. Осталось только перекинуть этот массив в неуправляемую память. По предыдущей статье мы уже знаем как при помощи маршалинга это сделать:

# выделяем область памяти для хранения нашего массива
$ptrSubject = [Runtime.InteropServices.Marshal]::AllocHGlobal($Subject.RawData.Count)
# копируем наш массив байтов в неуправляемую память
[Runtime.InteropServices.Marshal]::Copy($Subject.RawData,0,$ptrSubject,$Subject.RawData.Length)
# создаём простой объект CRYPTOAPI_BLOB и указываем адрес наших данных в памяти и сколько
# байт наших данных
$ptrName = New-Object Quest.PowerGUI+CRYPTOAPI_BLOB -Property @{
    cbData = $Subject.RawData.Length;
    pbData = $ptrSubject
}

Всё, объект в $ptrName содержит данные о поле Subject и Issuer.

Генерация ключевой пары

Идём дальше по аргументам нашей функции. dwFlags мы пропускаем, мы его смело поставим в 0 и забудем как кошмарный сон. Далее идёт pKeyProvInfo. Здесь деваться некуда, а ключевую пару нужно генерировать и собирать структуру CRYPT_KEY_PROV_INFO. Для этого у нас есть функция CryptGenKey. Функция достаточно несложная:

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

В качестве первого аргумента указываем хэндл нашего криптопровайдера. AlgId у нас будет означать KeySpec, т.е. AT_Exchange или AT_Signature. Поскольку мы договорились, что будем делать сертификат для подписи, это будет AT_Signature — 0x2.

dwFlags у нас будет определять длину ключа и дополнительные флаги. Поскольку это тестовый сертификат, мы будем использовать флаг CRYPT_EXPORTABLE — 0x1. Этот аргумент мы слепим из 2-х половинок по 4 октета (чтобы в сумме получить 8 октетов). Первые 4 октета (старшие) указывают на длину ключа (1024 или 2048 бит), но записанных в шестнадцатеричной форме (т.е. 0x0400 или 0x0800, соответственно). А младшие 4 октета будут представлять собой флаги. Недостающие октеты заполняем нулями. Следовательно, dwFlags для ключа в 1024 бит у нас будет 0x04000001, а для 2048 бит — 0x08000001.

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

[↓] [vPodans] [IntPtr]$phKey = [IntPtr]::Zero
[↓] [vPodans] $Result = [Quest.PowerGUI]::CryptGenKey($phProv,2,0x08000001,[ref]$phKey)
[↓] [vPodans] $Result
True
[↓] [vPodans] $PrivateKey = New-Object Quest.PowerGUI+CRYPT_KEY_PROV_INFO -Property @{
>>     pwszContainerName = $pszContainer;
>>     pwszProvName = $Provider;
>>     dwProvType = 1;
>>     dwKeySpec = 2
>> }
>>
[↓] [vPodans] $PrivateKey


pwszContainerName : b2909b8f-4145-445a-8816-cc37c672a532
pwszProvName      : Microsoft Base Cryptographic Provider v1.0
dwProvType        : 1
dwFlags           : 0
cProvParam        : 0
rgProvParam       : 0
dwKeySpec         : 2



[↓] [vPodans] $phKey
37910656
[↓] [vPodans]

Сначала мы сгенерировали ключевую пару (открытый ключ длиной 2048 бит) и создали объект структуры CRYPT_KEY_PROV_INFO. Почему мы никуда не вписали хэндл на закртый ключ? Он сейчас не нужен по простой причине, что он создан в контейнере криптопровайдера и функция CertCreateSelfSignCertificate сможет самостоятельно найти ключ.

Я предлагаю на этом пока остановиться, поскольку мы сделали самый необходимый минимум. В принципе, уже сейчас можно вызывать CertCreateSelfSignCertificate и на выходе мы получим примитивный сертификат. Минус этого сертификата — в нём не будет расширений. А нам нужны будут расширения. Как минимум Enhanced Key Usage, Key Usage, Subject Key Identifier, Subject Alternative Name и Basic Constraints. Т.е. это будет по сути самым правильным самоподписанным сертификатом для цифровой подписи. Но об этом в следующий раз.

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

В первой части мы кратенько рассмотрели процесс трансформации неуправляемых функций и структур в управляемые методы и объекты классов .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. Лёгкой жизни никто никому не обещал, но если понять описанные здесь моменты, можно значительно облегчить себе жизнь. В следующей части я расскажу ещё что-нибудь интересное :-)