Contents of this directory is archived and no longer updated.

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

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

 

Для решения этой задачи можно использовать различные 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, хотите — устанавливайте в хранилище, он ваш. И напоследок, финальный скрипт (правда, без комментариев):


Share this article:

Comments:

Comments are closed.