Contents of this directory is archived and no longer updated.

Posts on this page:

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

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

Когда вы работаете с провайдером сертификатов (Cert:\) очень часто нужно прочитать какое-то расширение. Однако, на первый взгляд, они кажутся совершенно нечитабельными:

[↓] [vPodans] $cert = (dir cert:\CurrentUser\My)[1]
[↓] [vPodans] $cert.Extensions

EnhancedKeyUsages                                  Critical Oid                           RawData
-----------------                                  -------- ---                           -------
{Encrypting File System}                              False System.Security.Cryptograp... {48, 12, 6, 10...}
                                                      False System.Security.Cryptograp... {48, 31, 160, 29...}
                                                      False System.Security.Cryptograp... {48, 0}


[↓] [vPodans]

Если с первым ещё более-менее понятно, то что означают другие — не совсем понятно. Расширение в X509Certificate2 (не всегда, но чаще всего) состоят из 3-х свойств: флаг критичности расширения, OID расширения (т.е. что это за расширение) и значение этого расширения закодированном в ASN.1 формате:

[↓] [vPodans] $cert.Extensions[1].Oid

Value                                                       FriendlyName
-----                                                       ------------
2.5.29.17                                                   Subject Alternative Name


[↓] [vPodans] $cert.Extensions[1].RawData
48
31
160
29
6
10
43
6
1
<...>

Я взял второе расширение и посмотрел его OID — Subject Alternative Name. Чтобы прочитать значение этого расширения можно извратиться через COM интерфейсы CertEnroll (доступные только начиная с Windows Vista и выше). Но можно использовать метод Format() класса X509Extension:

[↓] [vPodans] $cert.Extensions[1].Format(1)
Other Name:
     Principal Name=vPodans@Thor

[↓] [vPodans] $cert.Extensions[1].Format(0)
Other Name:Principal Name=vPodans@Thor
[↓] [vPodans]

Метод может принимать аргумент $true или $false. Если $true, добавляется возврат каретки в каждую строку. Это очень удобно, если значение расширения многострочное (как SAN, CDP, AIA и т.д.). В противном случае весь текст будет в одну строку и он будет нечитабелен. Кстати говоря, вывод будет форматироваться точно так же, как вы его видите в графическом сертификате (со всеми отступами и т.д.):

[↓] [vPodans] $cert = Get-Item cert:\CurrentUser\CA\2BAC956C4EE47F9D5C1E05AE8ED7F95D47C21F80
[↓] [vPodans] $cert.Extensions[3].Format(1)
[1]CRL Distribution Point
     Distribution Point Name:
          Full Name:
               URL=http://EVSecure-crl.verisign.com/pca3-g5.crl

[↓] [vPodans]

Надеюсь, кому-то пригодится.

Частенько появляются вопросы про просмотр хранилищ сертификатов на удалённых компьютерах из PowerShell. Встроенный провайдер CERT:\ не умеет такого. Командлетов для этой задачи тоже нет (хотя PowerShell вообще не содержит ни одного даже самого примитивного командлета для работы с сертификатами). Как быть и что делать? Ведь MMC умеет ходить по хранилищам удалённых компьютеров. Ответ на самом деле простой до безобразия, но о нём почти ничего не написано.

Как известно для особой работы с хранилищем сертификата (в смысле не только посмотреть, что там есть, но и добавить туда ещё сертификатов) мы используем класс X509Store. Сначала мы создаём соответствующий объект хранилища требуемого контекста (тип хранилища и название контейнера) и методом Open() открываем его. Но в списке конструкторов класса X509Store нет ничего похожего, что бы указывало на возможность указания другого компьютера. Чаще всего используется вот этот: X509Store(String, StoreLocation). В качестве первого параметра как правило указываем название контейнера, а в качестве второго указываем само хранилище — CurrentUser или LocalMachine. Однако, для этого конструктора есть один трюк: если к первому аргументу приписать имя компьютера, мы получим доступ к хранилищу удалённого компьютера и вот как это выглядит:

# создаём объект класса X509Store с указанием удалённого компьютера в формате:
# \\ИмяКомпьютера\НазваниеКонтейнера. Второй аргумент всегда должен быть LocalMachine.
$store = New-Object Security.Cryptography.X509Certificates.X509Store(\\ComputerName\ContainerName, "LocalMachine")
# открываем соответствующий контейнер в режиме ReadOnly. Если открываем для записи, тогда
# флаг должен быть ReadWrite.
$store.Open("ReadOnly")
# просматриваем содержимое контейнера.
$store.Certificates
# вытворяем гадости с сертификатами удалённой машины

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

Даже встроенным ремотингом PowerShell'а не нужно заморачиваться. С одной стороны это крутой чит — можно не отходя от кассы доставить сертификатов куда надо. Но тут есть один нюанс. Даже имея права стандартного пользователя можно получить такой доступ, если ремотинг .NET'а не зарезан фиреволом (что внутри периметра мало практикуется) и стащить компьютерный сертификат с закрытым ключом со всеми вытекающими последствиями. Правда, тут стоит оговориться, что стащить удастся только сертификат, у которого закрытый ключ помечен как Exportable. Но, учитывая массовые рекомендации вайтпеперов МСа, в подавляющем случае это будет так. Защититься от этого можно тремя способами (расположены в порядке возрастания стоимости решения):

  1. Не помечать закрытый ключ компьютерных сертификатов как разрешённый для экспорта;
  2. Зафиреволить наглухо все серверы в периметре;
  3. Приобрести для этих серверов HSM (Hardware Security Module).

Первый метод самый дешёвый и простой. Он не требует каких-то специальных знаний от администратора. Второй относительно дешёв, но требует достаточно глубоких знаний от администратора, т.к. некорректно настроенный фиревол может привести к потере удалённого контроля над сервером. А третий прост примерно как и первый способ, но требует определённых капиталовложений (порядка нескольких килобаксов за единицу девайса). Об этом можно дискутировать долго и упорно, моя же задача здесь предельно проста: рассказать как решить задачу и поведать о возможных последствиях :-)

Самоподписанные сертификаты — это зло, за исключением сертификатов корневых CA. Я об этом говорил, говорю и буду говорить. Но в данном случае мы не преследуем цель создания самоподписанного сертификата. Нас по сути будет интересовать немного другое — рассмотрение принципа, который заложен во многих популярных тулзах как MakeCert или OpenSSL. Лично я не фанат ни первого, ни второго по своим сугубо личным причинам. Но, кроме этих двоих есть ещё утилита CertReq.exe, которая достаточно православная и вряд ли ей грозит вымирание (а жаль). Вобщем, сегодня предлагаю ещё раз поковырять CryptoAPI.

Как мы уже знаем, CryptoAPI обладает большим количеством всяческих COM интерфейсов, при помощи которых мы можем работать практически с любыми аспектами цифровых сертификатов. Некоторые из них бажные, а некоторые — не очень :-), но функционал у них впечатляющий. В настоящее время существует 2 основных набора API, которые реализуют клиентскую часть энроллмента — XEnroll и CertEnroll. Первый доступен только в системах начиная с Windows 2000 и до Windows Server 2003 включительно. В более новых версиях XEnroll был вырезан вместе с CAPICOM'ом полностью (куски CAPICOM'а ещё можно найти в висте) за ненадобностью. Семейство интерфейсов CertEnroll было значительно переработано и расширено, что делает его крайне гибким. Я не буду рассказывать про XEnroll, потому что это неинтересно и трупов пинать нехорошо.

Многие считают, что CryptoAPI — это очень сложно. Я могу возразить им. Я не программист совсем, но могу достаточно свободно их использовать. Нашей отправной точкой будет MSDN по адресу: Certificate Enrollment API Reference. Эта секция содержит всё самое необходимое — описание интерфейсов и перечисления. И самый первый интерфейс, который мы видим — IX509Enrollment. Этот интерфейс реализует нечто промежуточное между клиентом и сервером. Мы можем по описанию найти то, что нам нужно, а именно первую секцию — Out-of-band-enrollment. И мы видим, что для него надо сначала вызвать метод CreateRequest(). Но прежде чем вызывать метод, нам надо создать форму сертификата, на основе которой будет создан запрос. Как я уже упоминал, мы будем делать самоподписанный сертификат, поэтому следующий интерфейс подойдёт нам как нельзя кстати — IX509CertificateRequestCertificate2. Вот давайте с него и начнём.

Все указанные здесь и далее интерфейсы являются COM интерфейсами семейства X509Enrollment и эти объекты создаются следующим образом:

$Cert = New-Object -ComObject X509Enrollment.CX509CertificateRequestCertificate.1

Примечание: как строятся такие команды? Поскольку это COM интерфейс, первую букву I в названии интерфейса меняем на букву C. Далее, если мы видим цифру 2 в конце названия интерфейса, в команде мы ставим точку и пишем число на единцу меньшее. Вот такие нехитрые правила.

Прежде чем его начать использовать, нам надо инициализировать его. К сожалению документация на MSDN далеко не полная, поэтому будем искать нужные методы через PowerShell и командлет Get-Member:

[↓] [vPodans] $cert | gm -MemberType methods


   TypeName: System.__ComObject#{728ab35a-217d-11da-b2a4-000e7bbb2b09}

Name                             MemberType Definition
----                             ---------- ----------
CheckPublicKeySignature          Method     void CheckPublicKeySignature (IX509PublicKey)
CheckSignature                   Method     void CheckSignature (Pkcs10AllowedSignatureTypes)
Encode                           Method     void Encode ()
GetCspStatuses                   Method     ICspStatuses GetCspStatuses (X509KeySpec)
GetInnerRequest                  Method     IX509CertificateRequest GetInnerRequest (InnerRequestLevel)
Initialize                       Method     void Initialize (X509CertificateEnrollmentContext)
InitializeDecode                 Method     void InitializeDecode (string, EncodingType)
InitializeFromCertificate        Method     void InitializeFromCertificate (X509CertificateEnrollmentContext, string...
InitializeFromPrivateKey         Method     void InitializeFromPrivateKey (X509CertificateEnrollmentContext, IX509Pr...
InitializeFromPrivateKeyTemplate Method     void InitializeFromPrivateKeyTemplate (X509CertificateEnrollmentContext,...
InitializeFromPublicKey          Method     void InitializeFromPublicKey (X509CertificateEnrollmentContext, IX509Pub...
InitializeFromTemplate           Method     void InitializeFromTemplate (X509CertificateEnrollmentContext, IX509Enro...
InitializeFromTemplateName       Method     void InitializeFromTemplateName (X509CertificateEnrollmentContext, string)
IsSmartCard                      Method     bool IsSmartCard ()
ResetForEncode                   Method     void ResetForEncode ()


[↓] [vPodans]

Из всех методов нам по сути доступен только InitializeFromPrivateKey(), поскольку остальные методы инициализации требуют наличие доступа к Certification Authority. Посмотрим что требуется для этого метода:

[↓] [vPodans] $cert | gm -MemberType methods | ?{$_.name -eq "InitializeFromPrivateKey"} | select definition

Definition
----------
void InitializeFromPrivateKey (X509CertificateEnrollmentContext, IX509PrivateKey, string)


[↓] [vPodans]

В качестве аргументов метода нам надо указать контекст энроллмента и объект закрытого ключа. Значения контекста находятся здесь: X509CertificateEnrollmentContext (просто включаете поиск на MSDN по названию перечисления). В качестве контекста мы можем выбрать контекст текущего пользователя или компьютера (остальное нас сейчас не волнует совсем). Контекст пользователя имеет значение 0x1. Так и запишем. Но этого мало. Надо ещё создать объект закрытого ключа:

$PrivateKey = New-Object -ComObject X509Enrollment.CX509PrivateKey

Этот интерфейс позволяет задавать различные параметры закрытого ключа, но мы обойдёмся лишь самым необходимым:

# во-первых надо указать CSP. Выберем самый простой криптопровайдер
$PrivateKey.ProviderName = "Microsoft Base Cryptographic Provider v1.0"
# закрытый ключ будет использоваться для подписи (Digital Signature)
# http://msdn.microsoft.com/en-us/library/aa379409(VS.85).aspx
$PrivateKey.KeySpec = 0x2
# длина вполне стандартная
$PrivateKey.Length = 1024
# ключ будем хранить в пользовательском хранилище
$PrivateKey.MachineContext = 0x0
# и генерируем ключ
$PrivateKey.Create()

Ура! Мы сгенерировали ключ. Теперь вернёмся к предыдущему интерфейсу и инициализируем его из закрытого ключа:

$Cert.InitializeFromPrivateKey(0x1,$PrivateKey,"")

Мы указываем контекст текущего пользователя и объект закрытого ключа. Там есть ещё один аргумент, который называется String. Я не знаю, что они этим хотели сказать, поэтому оставляем пустую строку. А теперь вернёмся к интерфейсу IX509CertificateRequestCertificate2 и посмотрим, что мы можем сделать сейчас. Например, используя свойства NotBefore и NotAfter мы зададим срок действия сертификата. Например, 1 год с сегодняшнего дня:

$Cert.NotBefore = [datetime]::Now
$Cert.NotAfter = $Cert.NotBefore.AddDays(365)

Теперь нам надо добавить следующие свойства: EncancedKeyUsage (т.е. для каких целей вообще будет использоваться сертификат), Subject (на кого будет выписан сертификат) и Issuer (кто выдал этот сертификат). Поскольку у нас самоподписанный сертификат, поле Subject и Issuer будут одинаковые. На MSDN'е не хватает документации по свойствам Issuer и Subject, но у нас есть поиск, который нас приведёт сюда: IX500DistinguishedName. Поля Subject и Issuer должны заполняться в формате Distinguished Name и доступные префиксы для DN достаточно понятно расписаны в таблице. Нам нужно как-то активировать этот объект. Методов для инициализации здесь нет, поэтому будем использовать метод Encode().

Лирическое отступление: в подавляющем большинстве случаев вы не можете присваивать значения свойствам объектов после создания самих объектов. Предварительно их надо «активировать» одним из двух способов. Если у объекта есть метод Initialize или производное от него, необходимо сначала воспользоваться одним из доступных методов инициализации. Если объект не содержит явных методов инициализации, нужно воспользоваться методом Encode, который кодирует объект или строку в ASN.1 DER строку и инициализирует объект. Единственным исключением из этого правила являются коллекции объектов. Они как правило используют метод Add() для добавления уже инициализированных объектов.

Уже с главной страницы IX500DistinguishedName видно, что Encode кодирует строку, которая записана в DN формате. Поэтому вызываем этот метод:

$SubjectDN.Encode("CN=Some Subject,DC=lucernepublishing,DC=COM", 0x0)

После строки нужно ещё указать флаг, в котором указана строка DN. Ставим дефолтный флаг. Теперь у нас готово поле Subject и Issuer (как мы договаривались, они будут одинаковые). Давайте их прицепим к нашему шаблону сертификата:

$Cert.Subject = $SubjectDN
$Cert.Issuer = $Cert.Subject

Что нам осталось сделать? Нам надо создать расширение Enchanced Key Usage. Для этого нам надо использовать следующий интерфейс: IX509ExtensionEnhancedKeyUsage:

$EKU = New-Object -ComObject X509Enrollment.CX509ExtensionEnhancedKeyUsage

Данный объект инициализируется из коллекции объектов IObjectIds. Давайте создадим эту коллекцию:

$OIDs = New-Object -ComObject X509Enrollment.CObjectIDs

В эту коллекцию с использованием метода Add() надо добавить один или несколько объектов IObjectId, каждый из которых представляет конкретное предназначение сертификата. Например, Server Authentication, Client Authentication, Smart Card Logon, Secure e-mail и т.д. Но мы сделаем сертификат для Code Signing. OID этого EKU = 1.3.6.1.5.5.7.3.3. Вот и сделаем его:

# создаём объект IObjectID
$OID = New-Object -ComObject X509Enrollment.CObjectID
# инициализируем его с использованием Code Signing
$OID.InitializeFromValue("1.3.6.1.5.5.7.3.3")
# добавляем наш OID в коллекцию OID'ов
$OIDs.Add($OID)
# добавляем коллекцию в объект IX509ExtensionEnhancedKeyUsage
$EKU.InitializeEncode($OIDs)

объект EKU у нас готов, теперь его надо добавить в наш шаблон сертификата. Поскольку это не стандартное поле сертификата, а расширение, добавляем этот объект в свойство X509Extensions, которое является аналогом интерфейса IX509Extensions и, который в свою очередь, является коллекцией расширений. Поэтому добавляем наше расширение методом Add():

$Cert.X509Extensions.Add($EKU)

Всё, мы собрали все минимально необходимые поля и расширения:

[↓] [vPodans] $cert


Type                        : 4
EnrollmentContext           : 1
Silent                      : False
ParentWindow                :
UIContextMessage            :
SuppressDefaults            : False
ClientId                    :
CspInformations             : System.__ComObject
HashAlgorithm               : System.__ComObject
AlternateSignatureAlgorithm : False
TemplateObjectId            :
PublicKey                   : System.__ComObject
PrivateKey                  : System.__ComObject
NullSigned                  : False
ReuseKey                    : False
Subject                     : System.__ComObject
CspStatuses                 : System.__ComObject
SmimeCapabilities           : False
SignatureInformation        : System.__ComObject
KeyContainerNamePrefix      : lp
CryptAttributes             :
X509Extensions              : System.__ComObject
CriticalExtensions          : System.__ComObject
SuppressOids                : System.__ComObject
Issuer                      : System.__ComObject
NotBefore                   : 16.04.2010 18:25:22
NotAfter                    : 16.04.2011 18:25:22
SignerCertificate           :
PolicyServer                :
Template                    :



[↓] [vPodans]

Теперь мы можем превращать наш шаблон сертификата в настоящй сертификат.

Лирическое отступление: а что такое запрос в техническом смысле? На самом деле запрос ничем не отличается от сертификата. Когда вы запрашиваете сертификат у CA, клиент использует эти же интерфейсы для генерации запроса. При этом получается самый настоящий самоподписанный сертификат, где Subject и Issuer одинаковые и равны имени текущего пользователя или компьютера, а так же содержит все необходимые расширения. Сам запрос подписывается закрытым ключом, который мы сгенерировали. По большому счёту, его уже можно использовать как настоящий самоподписанный сертификат. Если его отправить на сервер CA, то последний просто подменяет значения необходимых полей (как Issuer, в котором он ставит себя) и расширений, удаляет старую подпись и подписывает сертификат новой подписью. Вы можете убедиться в этом очень просто. Сгенерируйте запрос для сертификата, откройте оснастку Certificates и разверните секцию Certificate Enrollment Requests. Там будет этот самый запрос в виде уже готового сертификата. Просто там он ждёт, пока какой-нибудь CA не подпишет его.

Давайте вернёмся в самое начало текущего поста и вспомним про «исходный предмет» — IX509Enrollment. Вот этот интерфейс нам сконвертирует шаблон сертификата в настоящий сертификат с использованием метода CreateRequest(). Но прежде чем использовать метод, нам надо инициализировать объект:

$Request = New-Object -ComObject X509Enrollment.CX509enrollment
$Request.InitializeFromRequest($Cert)

И генерируем файл запроса, который ничем не отличается от самоподписанного сертификата:

$endCert = $Request.CreateRequest(0x0)

В аргументах метода указываем кодировку согласно этой страничке: EncodingType Enumeration. Мы выбираем Base64 с заголовками. $endCert будет содержать сам сертификат (открытую его часть). Фактически запрос хранится в контейнере Certificate Enrollment Requests. Поскольку этот интерфейс не был задуман специально для самоподписанных сертификатов мы проходим стандартную процедуру установки сертификата. Мы просто берём открытую часть нашего же сертификата и устанавливаем её. Вот, кстати, как он выглядит:

[↓] [vPodans] $endcert
-----BEGIN CERTIFICATE-----
MIICaTCCAdKgAwIBAgIQEzCS/mFIxLBAiGjz7+n0dDANBgkqhkiG9w0BAQUFADBP
MRMwEQYKCZImiZPyLGQBGRYDQ09NMSEwHwYKCZImiZPyLGQBGRYRbHVjZXJuZXB1
Ymxpc2hpbmcxFTATBgNVBAMMDFNvbWUgU3ViamVjdDAeFw0xMDA0MTgxMTI5MDla
Fw0xMTA0MTgxMTI5MDlaME8xEzARBgoJkiaJk/IsZAEZFgNDT00xITAfBgoJkiaJ
k/IsZAEZFhFsdWNlcm5lcHVibGlzaGluZzEVMBMGA1UEAwwMU29tZSBTdWJqZWN0
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBGa+PnrhnOFO5+76c5zX5/+xh
Kb2hUYl/pRuIKzYcqrmkvqjpPK/McusibT1h70emUkED0TSZsAlSivdIFK6WSxn6
HsTCaGIHhyOSKAvzQkBsZ74BPEydGT5LiX0+MOTyxwFAHhb+bqfbkdkXqUSkJAHK
Z6p+fgX8uaJkKjL/kwIDAQABo0YwRDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNV
HQ4EFgQUzIFwoRTY6KjUiqmjWjSjlkxbNncwDgYDVR0PAQH/BAQDAgeAMA0GCSqG
SIb3DQEBBQUAA4GBACizodCpl/cF3OGLUx8HVag0yhr1e1P8+CLPc31FmCPAY1CO
T0yxyJPoafkbXKRjclevNJdvxE3ys9fyYigFUhgswh3oWmjanDaatPKa0kE4147k
SQHvN8JP20KeDDCJBk/FbS3xCn3jTix90ddzTa1uFoqBbBNbKOaDHIrqypTY
-----END CERTIFICATE-----

[↓] [vPodans]

Система приклеит этот сертификат к шаблону сертификата и переложит его уже в контейнер Personal:

$Request.InstallResponse(0x2,$endCert,0x0,"")

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

  • Генерировать тестовый самоподписанный сертификат для подписи скриптов PowerShell
  • Устанавливать сертификат с закрытым ключом в контейнер Personal
  • Устанавливать открытую часть сертификата в Trusted Root CAs для обеспечения доверия этому сертификату
  • Устанавливать открытую часть сертификата в Trusted Publishers для задания явного доверия цифровым подписям, сделанные этим сертификатом.
#####################################################################
# Create PowerShell cert.ps1
# Version 1.0
#
# Creates self-signed signing certificate and install it to certificate store
#
# Note: Requires at least Windows Vista. Windows XP/Windows Server 2003
# are not supported.
#
# Vadims Podans (c) 2010
# http://www.sysadmins.lv/
#####################################################################
#requires -Version 2.0

function New-SigningCert {
<#
.Synopsis
    Creates self-signed signing certificate and install it to certificate store
.Description
    This function generates self-signed certificate with some pre-defined and
    user-definable settings. User may elect to perform complete certificate
    installation, by installing generated certificate to Trusted Root Certification
    Authorities and Trusted Publishers containers in *current user* store.
    
.Parameter Subject
    Specifies subject for certificate. This parameter must be entered in X500
    Distinguished Name format. Default is: CN=PowerShell User, OU=Test Signing Cert.

.Parameter KeyLength
    Specifies private key length. Due of performance and security reasons, only
    1024 and 2048 bit are supported. by default 1024 bit key length is used.

.Parameter NotBefore
    Sets the date in local time on which a certificate becomes valid. By default
    current date and time is used.

.Parameter NotAfter
    Sets the date in local time after which a certificate is no longer valid. By
    default certificate is valid for 365 days.

.Parameter Force
    If Force switch is asserted, script will prepare certificate for use by adding
    it to Trusted Root Certification Authorities and Trusted Publishers containers
    in current user certificate store. During certificate installation you will be
    prompted to confirm if you want to add self-signed certificate to Trusted Root
    Certification Authorities container.
#>
[CmdletBinding()]
    param (
        [string]$Subject = "CN=PowerShell User, OU=Test Signing Cert",
        [int][ValidateSet("1024", "2048")]$KeyLength = 1024,
        [datetime]$NotBefore = [DateTime]::Now,
        [datetime]$NotAfter = $NotBefore.AddDays(365),
        [switch]$Force
    )
    
    $OS = (Get-WmiObject Win32_OperatingSystem).Version
    if ($OS[0] -lt 6) {
        Write-Warning "Windows XP, Windows Server 2003 and Windows Server 2003 R2 are not supported!"
        return
    }
    # while all certificate fields MUST be encoded in ASN.1 DER format
    # we will use CryptoAPI COM interfaces to generate and encode all necessary
    # extensions.
    
    # create Subject field in X.500 format using the following interface:
    # http://msdn.microsoft.com/en-us/library/aa377051(VS.85).aspx
    $SubjectDN = New-Object -ComObject X509Enrollment.CX500DistinguishedName
    $SubjectDN.Encode($Subject, 0x0)
    
    # define CodeSigning enhanced key usage (actual OID = 1.3.6.1.5.5.7.3.3) from OID
    # http://msdn.microsoft.com/en-us/library/aa376784(VS.85).aspx
    $OID = New-Object -ComObject X509Enrollment.CObjectID
    $OID.InitializeFromValue("1.3.6.1.5.5.7.3.3")
    # while IX509ExtensionEnhancedKeyUsage accept only IObjectID collection
    # (to support multiple EKUs) we need to create IObjectIDs object and add our
    # IObjectID object to the collection:
    # http://msdn.microsoft.com/en-us/library/aa376785(VS.85).aspx
    $OIDs = New-Object -ComObject X509Enrollment.CObjectIDs
    $OIDs.Add($OID)
    
    # now we create Enhanced Key Usage extension, add our OID and encode extension value
    # http://msdn.microsoft.com/en-us/library/aa378132(VS.85).aspx
    $EKU = New-Object -ComObject X509Enrollment.CX509ExtensionEnhancedKeyUsage
    $EKU.InitializeEncode($OIDs)
    
    # generate Private key as follows:
    # http://msdn.microsoft.com/en-us/library/aa378921(VS.85).aspx
    $PrivateKey = New-Object -ComObject X509Enrollment.CX509PrivateKey
    $PrivateKey.ProviderName = "Microsoft Base Cryptographic Provider v1.0"
    # private key is supposed for signature: http://msdn.microsoft.com/en-us/library/aa379409(VS.85).aspx
    $PrivateKey.KeySpec = 0x2
    $PrivateKey.Length = $KeyLength
    # key will be stored in current user certificate store
    $PrivateKey.MachineContext = 0x0
    $PrivateKey.Create()
    
    # now we need to create certificate request template using the following interface:
    # http://msdn.microsoft.com/en-us/library/aa377124(VS.85).aspx
    $Cert = New-Object -ComObject X509Enrollment.CX509CertificateRequestCertificate
    $Cert.InitializeFromPrivateKey(0x1,$PrivateKey,"")
    $Cert.Subject = $SubjectDN
    $Cert.Issuer = $Cert.Subject
    $Cert.NotBefore = $NotBefore
    $Cert.NotAfter = $NotAfter
    $Cert.X509Extensions.Add($EKU)
    # completing certificate request template building
    $Cert.Encode()
    
    # now we need to process request and build end certificate using the following
    # interface: http://msdn.microsoft.com/en-us/library/aa377809(VS.85).aspx
    
    $Request = New-Object -ComObject X509Enrollment.CX509enrollment
    # process request
    $Request.InitializeFromRequest($Cert)
    # retrievecertificate encoded in Base64.
    $endCert = $Request.CreateRequest(0x1)
    # install certificate to user store
    $Request.InstallResponse(0x2,$endCert,0x1,"")
    
    if ($Force) {
        # convert Bas64 string to a byte array
         [Byte[]]$bytes = [System.Convert]::FromBase64String($endCert)
        foreach ($Container in "Root", "TrustedPublisher") {
            # open Trusted Root CAs and TrustedPublishers containers and add
            # certificate
            $x509store = New-Object Security.Cryptography.X509Certificates.X509Store $Container, "CurrentUser"
            $x509store.Open([Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
            $x509store.Add([Security.Cryptography.X509Certificates.X509Certificate2]$bytes)
            # close store when operation is completed
            $x509store.Close()
        }
    }
}

С виду кажется сложно, но на самом деле тут ничего сложного нет совсем. Просто представьте себе сертификат как большую матрёшку, в которую вы вкладываете другие маленькие матрёшки, которые представляют собой поля и расширения сертификатов. Начинаете собирать самые маленькие матрёшки, вкладываете в более большие и в конечном итоге собираете настоящий сертификат. Хоть документация на MSDN не очень полная, используя командлет Get-Member вы можете восполнить этот пробел.

Продолжаем серию постов, которые посвящены базовому управлению объектами PKI в Active Directory. На данный момент мы рассмотрели сценарии публикации и просмотра сертификатов в Active Directory:

На данном этапе нам осталось последнее — удаление сертификатов из AD. Логика здесь очень простая: командой Get-ADPKIObject мы получаем коллекцию объектов, которые представляют собой сертификаты и через конвейер командой Remove-ADPKIObject указываем ID объектов, которые необходимо удалить. Если кто-то уже разбирал код предыдущих скриптов, то ему будет совсем нетрудно понять логику скрипта удаления объектов. Вот он, вместе с комментариями:

function Remove-ADPKIObject {
<#
.Synopsis
    Deletes certificates from Active Directory containers
.Description
    Deletes certificates from Active Directory containers by specifying particular ID or IDs
.Parameter ID
    Specifies certificate ID to delete that was set in Get-ADPKIObject command.
.EXAMPLE
    Get-ADPKIObject RootCA | Remove-ADPKIObject 2
    
    deletes certificate with ID = 2 in certificate viewer
.Outputs
    This command provide a resultant of operation.
#>
    param([int[]]$ID = $(throw "you must specify number of the object to delete"))
    # объявляем массив для хранения сертификатов из контейнера NTAuthCertificates
    begin {$sum = @()}
    process {
        # проверяем тип контейнера входящего объекта
        if ($_.Container -ne "NTAuthCertificates") {
            # если это не NTAuthCertificates, то проверяем, что ID текущего объекта
            # совпадает с ID, который нужно удалить
            if (@($ID) -contains $_.Id) {
                # если совпал, то собираем LDAP-запрос
                $ldap = [ADSI]"LDAP://CN=$($_.Container),$script:ConfigContext"
                # и удаляем текущий объект из AD
                $retn = $ldap.Delete("certificationAuthority", "CN=$($_.Subject)")
                if ($?) {
                    Write-Host "`'$($_.Subject)`' certificate was sucessfully deleted from `'$($_.Container)`' container"` 
                   -ForegroundColor Green
                }
            }
        # если контейнер текущего объекта является NTAuthCertificates, то собираем их все в массив
        } else {$sum += $_}
    }
    end {
        # проверяем, что массив непустой (т.е. надо что-то удалять из NTAuthCertificates)
        if ($sum) {
            # если массив непустой, то выбираем те элементы, которые нужно сохранить
            # т.е. ID которых не содержится в аргументах скрипта
            $sum = @($sum |?{$ID -notcontains $_.Id})
            # делаем LDAP-запрос к этому контейнеру
            $ldap = [ADSI]"LDAP://CN=$($_.Container),$script:ConfigContext"
            # проверяем, что после фильтрации, хотя бы один сертификат нужно оставить
            if ($sum.count -ge 1) {
                # записываем первый сертификат. Это необходимо потому что ADSI не поддерживает запись
                # массива сертификатов в свойство cACertificate, а только один сертификат в виде byte[]
                $ldap.put("cACertificate", [byte[]]$sum[0].RawCertificate)
                # а вот простое добавление он поддерживает. Тогда ADSI сам пересоберёт объекты
                # в свойстве в нужный формат данных. На данном этапе я применил маленькую хитрость:
                # как видно, я первый сертификат записываю дважды - предыдущей строкой и в первой итерации
                # текущей строки. Но это не проблема, поскольку метод SetInfo() записывает только уникальные
                # объекты, а дублирующиеся просто отбросит.
                $sum | %{$ldap.cACertificate += ,[byte[]]$($_.RawCertificate)}
                $ldap.SetInfo()
                if ($?) {
                    Write-Host "`'$($_.Subject)`' certificate was sucessfully deleted from `'$($_.Container)`' container"` 
                   -ForegroundColor Green
                }
            # а вот если после фильтрации объектов, у нас ничего не остаётся на запись, то это означает, что все
            # сертификаты из этого контейнера удаляются. Поэтому мы просто удаляем запись NTAuthCertificates.
            } else {
                ([ADSI]"LDAP://$script:ConfigContext").Delete("certificationAuthority", "CN=NTAuthCertificates")
                if ($?) {Write-Host "All certificates was sucessfully deleted from NTAuthCertificates entry ." -ForegroundColor Green
                    Write-Warning "This was last certificate in contaner. NTAuthCertificates entry is removed from Active Directory"
                }
            }
        }
    }
}

И теперь можно подвести краткие итоги. Мы смогли реализовать функционал certutil и других графических утилит (консоли MMC) в PowerShell значительно улучшив читабельность выходных объектов, адаптировали под работу из консоли (синтаксис стал значимо короче и более юзерфрендли) и шаг за шагом делаем из PowerShell единое консольное средство управления различными аспектами PKI.

Можно задать вопрос: а кто целевая аудитория всего этого? Целевая аудитория есть — администраторы PKI. Просто у вас не всегда будет возможность использовать графические консоли для решения этих задач (потому что их функционал далёк от идеального). Можно использовать certutil, который умеет много чего, но тоже имеет свои недостатки. Это и ужасный синтаксис, и вырвиглазный неуправляемый вывод результатов. Вобщем я надеюсь, что рано или поздно PowerShell сможет по-настоящему заменить certutil (который вообще-то ни в чём не виноват) и стать единой консолью всех Windows-администраторов. Вот не знаю на сколько это хорошо или плохо, потому что Microsoft всех насильно переводит на PowerShell (это очень показательно продемонстрировано в MS Exchange, где у вас по сути есть только PowerShell и всё). Обычно, насильно переводят на другой инструмент когда он является УГ и очень тяжело на него перевести людей посредством обычной рекламы. Но является ли PowerShell таким УГ? Я пока не готов ответить на этот вопрос. Моё мнение — PowerShell пока что особой революцией не стал. Даже не смотря на тонны рекламы, пеара и прочего, где восхваляют PowerShell, закидывают ногами CMD/WSH. Это обусловлено тем фактом, что не всегда PowerShell бывает удобней CMD/WSH, особенно в тривиальных задачах. Говорить, что синтаксис стал более простым и компактным тоже нельзя, потому что реально функционала из коробки хватает для решения процентов 10 задач. Всё остальное нужно скриптовать и программировать (да-да!) самому. Благо средств для этого в PowerShell хватает. Во что это обычно выливается? А в то, что в большинстве случаев результирующий объём кода будет не сильно меньше, чем в связке WSH + CMD. В любом случае преимущества PowerShell перед остальными очевидны, но они далеко не определяющие, ведь люди раньше решали свои задачи на WSH/CMD, пирожки продавались, бизнес шёл. С одной стороны Microsoft дал людям простор для творчества, т.е. делать в PowerShell всякие потрясающие штуки и всё такое. Но это не совсем то, что нужно было администраторам. Им нужна одна кнопка на весь экран с надписью «Сделать всё п**дато!». Пока что PowerShell и близко не готов стать такой кнопкой, а является «удочкой». Т.е. удочка у вас уже есть, а что касается конечной рыбы (результата), то дело осталось за малым — написать мега-скрипт. Мне вот интересно, что думают администраторы Exchange (поскольку пока что только они получили полноценную поддержку для своего продукта в PowerShell) — стало ли им жить легче с PowerShell или нет? Если да, то это может быть хорошим знаком, что однажды PowerShell станет такой кнопкой. И не будет более в системе certutil и всеми задачами будет рулить PowerShell (пока что это наиболее логичный сценарий развития событий). А вот если их жизнь не стала легче, то обещанной революции (которая по словам Microsoft уже наступила, как и вендекапец у луноходов) не будет, а будет просто какое-то логическое продолжение предыдущих инструментов для сценариев.

К чему я написал столько букв? К тому, что я ежедневно задаю себе один и тот же вопрос: а зачем я всё это делаю? А ответ найти очень непросто, потому что отмазы вида «проще, удобней, красивее» не годятся для серьёзного аргумента. На самом деле я не ищу ответ на него, а просто говорю себе «так надо» и делаю. Поэтому не надо меня использовать как пример «правильного пользователя PowerShell» и пытаться повторить что-то подобное астрономических масштабов на овер9000 строк — поверьте, оно не стоит того. Используйте его по мере сил. Если чего-то будет не хватать и его решение потребует значительных усилий — посмотрите на готовые утилиты, они наверняка будут уметь то, что вам надо.

Удачи!© One