Contents of this directory is archived and no longer updated.

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

Когда-то давно я писал о том, как можно создать самоподписанный сертификат в 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.


Share this article:

Comments:

Comments are closed.