Posts on this page:
MSDN и Microsoft меня однажды доканает и я уйду проповедовать православныйбогомерзкий линукс.
А дело в том, что одна моя задумка с треском проваливалась из-за непонятного и тупого бага в интерфейсе ICertEncodeAltName. Это CryptoAPI интерфейс, который позволяет кодировать строковые значения для расширения Subject Alternative Name в ASN.1 DER encoded строку. На возню с ним я потратил почти неделю (чуть меньше, наверное) и только сегодня я смог найти все ответы. Хотя я это должен был сделать много раньше, но дико тупил на ровном месте. Итак, давайте посмотрим, что из себя представляет этот баг.
Для начала я покажу как собирается этот интерфейс:
$san = New-Object -ComObject CertificateAuthority.EncodeAltName
Далее, его надо инициализировать методом Reset() и в качестве аргумента метода указать размер объекта. Т.е. количество элементов, которые должны содержаться в расширении Subject Alternative Name. Если только один элемент, то и указываете число 1:
$san.Reset(1)
и теперь можно в него заряжать данные методом SetNameEntry():
$san.SetNameEntry(0,0x3,"Custom-Name")
Где 0 — индекс массива, в который надо записать строку, 0x3 — тип строки. В данном случае это DNS и последний аргумент указывает само значение строки. Полный список типов можно получить вот здесь: http://msdn.microsoft.com/en-us/library/aa374981(VS.85).aspx. Только учтите, что реально типы начинаются не с нуля (как по ссылке), а с единицы. т.е. 0x3 в нашем случае это DNS (2+1).
После того как мы загнали туда данные, их можно кодировать:
$san.Encode()
И давайте посмотрим, что у нас получилось:
[↓] [vPodans] $san = New-Object -ComObject CertificateAuthority.EncodeAltName [↓] [vPodans] $san.Reset(1) [↓] [vPodans] $san.SetNameEntry(0,0x3,"Custom-Name") [↓] [vPodans] $str = $san.Encode() [↓] [vPodans] $str ???????
выглядит прикольно. Но на самом деле это строка из символов, которые описаны не одним, а двумя байтами:
[↓] [vPodans] $str.ToCharArray() | %{[int][char]$_}
3376
2946
30019
29811
28015
20013
28001
Чтобы убедиться, что в каждом символе по 2 байта, мы преобразуем эти числа в их шестнадцатиричное (hex) представление:
[↓] [vPodans] $str.ToCharArray() | %{"{0:X4}" -f [int][char]$_}
0D30
0B82
7543
7473
6D6F
4E2D
6D61
видите, в каждом символе по 2 байта и все они записаны в little-endian последовательности. Т.е. первый байт находится справа, а не слева, как обычно. Давайте соберём эти байты в последовательную строку, переставив байты местами в каждой строке:
30 0D 82 0B 43 75 73 74 6F 6D 2D 4E 61 6D
И что же означают эти циферки-букавки? А означают они следующее:
Куда-то пропал один очень важный байт, из-за чего сервер CA начинал доставлять шлакоблоки при виде такой строки. В принципе, мы можем посмотреть, что у нас потерялось по дороге:
[↓] [vPodans] $str1 = "43 75 73 74 6F 6D 2D 4E 61 6D".split(" ") [↓] [vPodans] $str1 | %{$a += invoke-expression [char]0x$_} [↓] [vPodans] $a Custom-Nam
Последний символ куда-то провалился. Нет его больше. Т.е. размер вычислен правильно, просто не хватает одного байта. Причём, если попробовать что-то другое, попроще, интерфейс возвращает правильный размер и все байты. Я не знаю точно, в каких условиях этот баг проявляется, но уже очевидно, что если в строке присутствует дефис, уже начинается бяка.
Что можно сделать? Вариантов куча. Как вы видите ASN.1 DER кодировка достаточно простая, её можно сделать самому. Либо использовать православные интерфейсы CertEnroll — IX509ExtensionAlternativeNames. На первый взгляд там багов не обнаружено :-). Примеры использования данных интерфейсов можно посмотреть (а заодно и почитать) в моём буржуйском бложике: How to add FQDN to HP iLO request.
На сегодня всё. Спокночи.
Сегодня пришло письмо, где человек спрашивал о том, можно ли сделать скриншот средствами PowerShell. Я вообще не очень понял зачем это, но это уже не моё дело. Если я пишу этот пост, значит можно :-)
Для этого существует класс Drawing.Graphics у которого есть замечательный метод CopyFromScreen(). Данный метод принимает в качестве аргументов различные параметры, которые определяют координаты области, которую нужно сфоткать. В принципе можно забивать фиксированные значения (см. конструкторы с Int, как этот — CopyFromScreen(Int32, Int32, Int32, Int32, Size)), а можно эти размеры выставлять динамически, в зависимости от размеров экрана. Для этого лучше воспользоваться самым первым методом: CopyFromScreen(Point, Point, Size).
Как получить размеры экрана? Очень просто, достаточно воспользоваться статическим свойством VirtualScreen класса Windows.Forms.SystemInformation:
[Windows.Forms.SystemInformation]::VirtualScreen
[↓] [vPodans] [Windows.Forms.SystemInformation]::VirtualScreen Location : {X=0,Y=0} Size : {Width=1280, Height=800} X : 0 Y : 0 Width : 1280 Height : 800 Left : 0 Top : 0 Right : 1280 Bottom : 800 IsEmpty : False [↓] [vPodans]
Вот размеры экрана на моём нотебуке. Готично. Теперь нужно создать объект Drawing.Graphics. Однако, этот объект нельзя создать через New-Object, поскольку для этого нет ни одного конструктора. Следовательно объект нужно создавать используя статические методы. Статические методы можно посмотреть на странице Drawing.Graphics Members. И там мы можем найти красивый метод — FromImage. В качестве аргумента этого метода нужно указать картинку — http://msdn.microsoft.com/en-us/library/system.drawing.image.aspx. Это может быть файл или объект класса Drawing.Bitmap. Давайте создадим объект класса Drawing.Bitmap с использованием следующего конструктора — Bitmap Constructor (Int32, Int32):
[↓] [vPodans] [void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") [↓] [vPodans] $size = [Windows.Forms.SystemInformation]::VirtualScreen [↓] [vPodans] $bitmap = new-object Drawing.Bitmap $size.width, $size.height [↓] [vPodans] $bitmap Tag : PhysicalDimension : {Width=1280, Height=800} Size : {Width=1280, Height=800} Width : 1280 Height : 800 HorizontalResolution : 96 VerticalResolution : 96 Flags : 2 RawFormat : [ImageFormat: b96b3caa-0728-11d3-9d7b-0000f81ef32e] PixelFormat : Format32bppArgb Palette : System.Drawing.Imaging.ColorPalette FrameDimensionsList : {7462dc86-6180-4c7e-8e3f-ee7333a7a483} PropertyIdList : {} PropertyItems : {} [↓] [vPodans]
Примечание: WinForms по умолчанию не загружаются вместе с PowerShell, поэтому мы их подгружаем в консоль в ходе работы. Мы создали Bitmap и теперь его можем использовать для создания объекта Drawing.Graphics:
$graphics = [Drawing.Graphics]::FromImage($bitmap)
А дальше уже вызывать метод CopyFromScreen(). При этом указываем первым аргументом начало координат (верхний левый угол или 0,0) и в аргумент правый нижний угол. Т.е. максимум точек по ширине и высоте. Их мы можем получить из свойств Width и Height класса Windows.Forms.SystemInformation. Средний аргумент опускаем и делаем его пустым:
[↓] [vPodans] $graphics = [Drawing.Graphics]::FromImage($bitmap) [↓] [vPodans] $graphics.CopyFromScreen($size.Location,[Drawing.Point]::Empty, $size.size)
Всё, мы сфоткали наш экран и загнали картинку обратно в Bitmap. Теперь нужно выгрузить картинку куда-то в файл. Для этого у Bitmap есть метод Save(), в котором достаточно указать путь к файлу:
[↓] [vPodans] $bitmap.save(".\screenshot.jpg") [↓] [vPodans] gi .\screenshot.jpg Directory: C:\Users\vPodans Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 22.04.2010 22:37 286319 screenshot.jpg [↓] [vPodans]
Всё :-)
Примечание: после сохранения картинка остаётся в памяти, поэтому после работы следует освобождать оперативную память от ненужных пруфпиков используя метод Dispose() без аргументов.
И код:
[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") $size = [Windows.Forms.SystemInformation]::VirtualScreen $bitmap = new-object Drawing.Bitmap $size.width, $size.height $graphics = [Drawing.Graphics]::FromImage($bitmap) $graphics.CopyFromScreen($size.location,[Drawing.Point]::Empty, $size.size) $graphics.Dispose() $bitmap.Save(".\screenshot.jpg") $bitmap.Dispose()
Самоподписанные сертификаты — это зло, за исключением сертификатов корневых 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,"")
Всё, теперь мы увидим этот сертификат в нашем хранилище и который готов к использованию. Я немного переработал код и обернул его в красивую функцию, которая будет делать следующее:
##################################################################### # 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
В предыдущем посте мы ознакомились с основными контейнерами с объектами PKI в Active Directory и смогли изучить функциональный аналог ключа dspublish в утилите certutil. Если публикация сертификатов в AD задача простая даже для Certutil, то просмотр содержимого может быть весьма нетривиальным. Например, если вы хотите посмотреть содержимое записи NTAuthCertificates, то придётся выполнить вот такую команду:
certutil –viewstore "CN=NTAuthCertificates,CN=Public Key Services,CN=Services,CN=Configuration, ForestRootDomainDN"
такие вещи совершенно неприспособлены к командной строке, поскольку надо набирать много текста и ошибиться весьма просто. Одно хорошо, команда выводит графическое окошко, где мы можем посмотреть содержимое. Но тут есть несколько неудобных моментов: мы не можем посмотреть несколько контейнеров сразу, для каждого контейнера надо выполнять отдельную команду. В этом окошке мы можем только посмотреть на содержимое контейнера и всё. Ни добавить, ни удалить сертификат мы не можем. Для добавления сертификатов мы можем воспользоваться тем же certutil или моим скриптом, который был опубликован в предыдущем посте. Графика — хорошо и замечательно, но мы можем хотеть автоматизировать какие-то задачи или просто посмотреть информацию в консоли. Вы можете подумать, что это не нужно, но преимущество между консольным выводом и графическим диалоговым окном очевидное: из первого можно копировать информацию в буфер обмена. Есть ещё вариант — для просмотра и удаления сертификатов из AD, пользоваться консолью pkiview.msc. Но мы сразу же теряем главную нить — единое средство управления. Т.е. даже похожие операции мы должны выполнять в разных инструментах! Но с появлением PowerShell мы получили единый (хоть и консольный) инструмент, которым можно автоматизировать абсолютно всё! Даже сам PowerShell :-) Вот, собственно код, который в разы упрощает процесс просмотра содержимого контейнеров:
function Get-ADPKIObject { <# .Synopsis Displays certificates info from Active Directory containers .Description Displays info about certificates that are stored in AD PKI-related containers. .Parameter Container Optional parameter. Specifies particular container to view. May contain one or more value from following possible values: RootCA - retrieves certificates from Certification Authorities container SubCA - retrieves certificates from AIA container NTAuthCA - retrieves certificates from NTAuthCertificate directory entry. if no parameter is set, command will return all certificates from all applicable containers. .EXAMPLE Get-ADPKIObject RootCA Retrieves certificates from Certification Authorities container .EXAMPLE Get-ADPKIObject RootCA, AIA Retrieves certificates from Certification Authorities and AIA containers .Outputs Output AD PKI object collection #> [CmdletBinding()] param([string[]][ValidateSet("RootCA", "SubCA", "NTAuthCA", "")]$Container) # объявляем массив, который будет хранить выходные объекты $script:sum = @() # это весьма крутая штука будет. Каждый объект будет содержать свойство Id или просто # порядковый номер объекта. При дальнейших операциях с этими объектами вам достаточно # будет указать его Id вместо длинных и неудобных LDAP/Thumpbrint значений, как это делается # в certutil и подобных ему утилитах. Нумерацию начнём с единицы $script:n = 1 # итоговая функция, которая будет разбирать бинарные массивы и готовить выходные объекты function _formatter_ ($certs, $type, $name) { # поскольку у нас все объекты в AD находятся в бинарном формате, мы импортируем каждый из них # в X509Certificate2 объект. $certs | %{ $script:Cert.Import($_) # здесь мы создаём образец выходного объекта и обвязываем этот объект необходимыми свойстами и данными $current = "" | select @{n='Id';e={$script:n}}, Subject, @{n='Type';e={$type}}, @{n='Container';e={$name}}, @{n='Thumbprint';e={$script:Cert.Thumbprint}}, @{n='SerialNumber';e={$script:Cert.SerialNumber}}, @{n='ValidFrom';e={$script:Cert.NotBefore}}, @{n='ValidTo';e={$script:Cert.NotAfter}}, @{n='RawCertificate';e={$script:Cert.RawData}} # чтобы не писать полный DN поля Subject, мы будем показывать только первую его часть # (которая отображается в самом сертификате) [void]($script:Cert.Subject -match 'CN=([^,]+)') $current.Subject = $matches[1] # добавляем объект в массив выходных объектов $script:sum += $current # очищаем X509Certificate2 $script:Cert.Reset() # увеличиваем счётчик и обрабатываем следующий элемент $script:n++ } } # ещё одна суб-функция, которая выдёргивает сертификаты из AD в бинарном виде и отправляет # их в _formatter_, который уже сформирует итоговые объекты. function _switcher_ ($name) { # подключаемся к нужному контейнеру $ldap = [ADSI]("LDAP://CN=$name,$script:ConfigContext") # как мы знаем, NTAuthCertificates не является контейнером, поэтому для него код будет немного # отличаться. А отличие будет состоять в том, что мы не будем залезать в контейнер, а сразу читать # свойства объекта NTAuthCA if ($name -eq "NTAuthCertificates") { # убеждаемся, что длина первого элемента свойства cACertificate больше единицы, т.е. содержит ненулевое # значение. Так же проверяем свойство crossCertificatePair, которое содержит Cross-certificates # и если оно не нулевое, то отправляем и его на формирование вывода if ($ldap.cACertificate[0].count -gt 1) { $certs = @($ldap.cACertificate) _formatter_ $certs "CA Certificate" $name } if ($ldap.crossCertificatePair[0].count -gt 1) { $certs = @($ldap.cACertificate) _formatter_ $certs "Cross CA Certificate" $name } # и переходим к следующему контейнеру return } # если контейнер указан как Certification Authority и/или AIA, то заглядываем # внутрь контейнера $ldap.psbase.children | %{ # и заглядываем в каждую запись на исследование свойств cACetificate и crossCertificatePair $certs = @($_.cACertificate) $ccerts = @($_.crossCertificatePair) # проверяем, что свойство имеет ненулевое значение. Если так, то отправляем # содержимое этих свойств на формирование вывода if ($certs[0].count -gt 1) {_formatter_ $certs "CA Certificate" $name} if ($ccerts[0].count -gt 1) {_formatter_ $ccerts "Cross CA Certificate" $name} } } switch ($Container) { # конструкцией switch проверяем содержимое аргумента функции, чтобы определить какие именно # контейнеры надо обследовать. "RootCA" {_switcher_ "Certification Authorities"} "SubCA" {_switcher_ "AIA"} "NTAuthCA" {_switcher_ "NTAuthCertificates"} # если контейнер в аргументе не указан, то проверяем все контейнеры и записи "" { _switcher_ "Certification Authorities" _switcher_ "AIA" _switcher_ "NTAUthCertificates" } } # когда вывод будет полностью сформирован, выбрасываем все объекты в консоль $script:sum }
Примечание: данная функция является частью файла dspublish.ps1, т.к. использует глобально объявленные переменные.
и вывод у него вот такой красивый:
Id : 3 Subject : Contoso CA Type : CA Certificate Container : AIA Thumbprint : BA8FECE99165E68CE27C9F0AF5F0664FDA39F7A2 SerialNumber : 5DD87E4CFFE3B3BC43F608EB57C767F7 ValidFrom : 2009.02.15. 16:31:15 ValidTo : 2014.02.15. 16:40:11 RawCertificate : {48, 130, 4, 98...} Id : 4 Subject : sysadmins-LV-CA Type : Cross CA Certificate Container : AIA Thumbprint : 1A28B582E21803D2BFE0DAEEF4593DE372C8EC3C SerialNumber : 170AD11B0000000000A7 ValidFrom : 2009.11.24. 19:43:10 ValidTo : 2011.03.30. 17:06:53 RawCertificate : {48, 130, 7, 94...}
я считаю его достаточно информативным. Но если хотите посмотреть любой сертификат из этого списка, то можно и сделать просмотр. К сожалению я в программировании не шарю и как работать напрямую с библиотекой просмотрщика сертификатов, поэтому я реализовал просмотр обходным путём:
filter View-ADPKIObject { <# .Synopsis Displays certificates in certificate viewer .Description Displays certificates in certificate viewer by selecting necessary certificates ID. Must be placed after Get-ADPKIObject command only. .Parameter ID Specifies certificate ID that was set in Get-ADPKIObject command. .EXAMPLE Get-ADPKIObject RootCA | ViewADPKIObject 1, 3 displays certificate with ID = 2 in certificate viewer .Outputs Script doesn't generate any output except errors #> # судя по конструкции int[] мы можем указать несколько чисел, тогда все выбранные # сертификаты будут отображены. Номер указывать обязательно. param([int[]]$ID = $(throw "you must specify number of the object to display")) # и проверяем входные объекты с конвейера на предмет их ID. Если ID совпадает с # одним из ID в аргументах, обрабатываем его. Если ID не совпадает, ничего не делаем. if (@($ID) -contains $_.Id) { # генерируем в пользовательской папке Temp временный файл с рандомным расширением $TempFile = [System.IO.Path]::GetTempFileName() + ".cer" # записываем бинарный массив сертификата в файл в виде DER кодировки [System.IO.File]::WriteAllBytes($TempFile, $_.RawCertificate) # запускаем файл (просмоторщик сертификатов) & $TempFile # в санитарных целях ждём пол секунды Start-Sleep 0.5 # и удаляем этот файл, чтобы не копился мусор del $TempFile -Force } }
Как вы видите, ничего сверх-космического или магического в этом коде нет, самое трудное здесь — придумать логику работы. А остальное — накидать несколько строк кода и у нас PowerShell в лёгкую может соперничать с certutil за право называться единой утилитой управления PKI :-)
Чтобы подкрутить рейтинг PowerShell в этой конкуренции, в следующий раз я покажу как мы можем легко и просто удалять сертификаты из контейнеров AD с использованием PowerShell :-)