Contents of this directory is archived and no longer updated.

Posts on this page:

Продолжаем серию постов, которые посвящены базовому управлению объектами 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 :-)

Подоспела ещё одна задачка для PowerShell'а — управление объектами PKI в Active Directory. Active Directory содержит целый раздел посвящённый PKI и вот из чего он состоит:

  • CN=NTAuthCertificates, CN=Public Key Services, CN=Services, CN=Configuration, ForestRootDomain
  • CN={CA name},CN=AIA, CN=Public Key Services, CN=Services, CN=Configuration, ForestRootDomain
  • CN={CA name},CN=CDP, CN=Public Key Services, CN=Services, CN=Configuration, ForestRootDomain
  • CN={CA name},CN=Certification Authority, CN=Public Key Services, CN=Services, CN=Configuration, ForestRootDomain

Вот о них мы сегодня и поговорим. В AD есть ещё несколько контейнеров, которые связаны с PKI, но они сегодня интереса представлять не будут. Как мы уже знаем, в:


Read more →

В продолжении темы исследования COM интерфейсов CryptoAPI для управления службами Certificate Services предлагаю теперь разобрать вопросы KRA — Key Recovery Agents. Сначала напомню уже изученные темы по CryptoAPI:

И на сегодня наш план выглядит достаточно интересно:

  • Получение списка доступных KRA в лесу и просмотр их сертификатов
  • Получение списка KRA назначенных на сервере CA;
  • Назначение новых KRA для CA.

Получение списка доступных KRA в лесу

KRA используется для архивирования закрытых ключей сертификатов в БД CA. В случае если пользователь уничтожил свой закрытый ключ он может его восстановить. Для этого нужно обратиться к администратору CA для извлечения BLOB (Binary Large OBject) в файл. После чего агент восстановления (KRA) использует свой закрытый ключ для расшифровки закрытого ключа из BLOB'а. Учитывая, что я и не только уже рассматривали этот вопрос, я не буду пересказывать принцип работы Key Archival:

Все сертификаты KRA публикуются в forest naming configuration context партиции AD и реплицируется между всеми контроллерами в лесу. Публикация просисходит в точке:

CN=KRA, CN=Public Key Services, CN=Services, CN=Configuration, DC=ForestRootDomain

PS C:\> $ldap = [ADSI]"LDAP://CN=KRA, CN=Public Key Services, CN=Services, CN=Configuration, DC=contoso,dc=com"
PS C:\> $ldap

distinguishedName
-----------------
{CN=KRA,CN=Public Key Services,CN=Services,CN=Configuration,DC=contoso,DC=com}


PS C:\> $kra = $ldap.psbase.children | %{$_}
PS C:\> $kra

distinguishedName
-----------------
{CN=contoso-DC2-CA,CN=KRA,CN=Public Key Services,CN=Services,CN=Configuration,DC=contoso,DC=com}
{CN=Contoso CA,CN=KRA,CN=Public Key Services,CN=Services,CN=Configuration,DC=contoso,DC=com}

Мы получили список CA в лесу и посмотрев каждый из них можем посмотреть сколько KRA сертификатов выпустил каждый CA:

PS C:\> $kra[1] | fl *


objectClass            : {top, msPKI-PrivateKeyRecoveryAgent}
cn                     : {Contoso CA}
userCertificate        : {48 130 6 104 48 130 5 80 160 3 2 1 2 2 10 21 29 22 96 0 0 0 0 0 4 48 13 6 9 42 134 72 134 247
                          13 1 1 5 5 0 48 67 49 19 48 17 6 10 9 146 38 137 147 242 44 100 1 25 22 3 99 111 109 49 23 48
                          21 6 10 9 146 38 137 147 242 44 100 1 25 22 7 99 111 110 116 111 115 111 49 19 48 17 6 3 85 4
                          3 19 10 67 111 110 116 111 115 111 32 67 65 48 30 23 13 48 57 48 50 49 53 49 53 48 53 50 53 9
                         0 23 13 49 49 48 50 49 53 49 53 48 53 50 53 90 48 86 49 19 48 17 6 10 9 146 38 137 147 242 44
                         100 1 25 22 3 99 111 109 49 23 48 21 6 10 9 146 38 137 147 242 44 100 1 25 22 7 99 111 110 116
<...>

Свойство UserCertificate в виде массива может содержать сертификаты KRA в виде байтового массива. Например, указанный CA выпустил 2 сертификата KRA:

PS C:\> $kra[1].usercertificate.count
2

И обратившись к индексам этого свойства можно получить конкретные сертификаты: $kra[1].usercertificate[0]. Если CA не выпускал сертификаты для KRA, то свойство UserCertificate будет содержать число ноль:

PS C:\> $kra[0] | fl *


objectClass            : {top, msPKI-PrivateKeyRecoveryAgent}
cn                     : {contoso-DC2-CA}
userCertificate        : {0}
<...>

Поэтому для получения всех сертификатов KRA нужно пройтись по всем членам CN=KRA и выбрать из них ненулевые значения свойства UserCertificate. Если сертификатов больше, чем 1, то они будут храниться в этом свойстве в виде массива. Чтобы посмотреть сам сертификат, вы можете просто сохранить этот массив байтов в файл:

PS C:\> [System.IO.File]::WriteAllBytes("C:\kra.cer",$kra[1].usercertificate[1])
PS C:\> & .\kra.cer

Получение списка KRA назначенных на сервере CA

А теперь вернёмся к интерфейсу ICertAdmin2. С его помощью мы можем посмотреть количество агентов восстановления, которые назначены на CA и посмотреть их сертификаты. Однако, этот интерфейс имеет какой-то баг с PropID, которые связаны с KRA, о которых я писал в предыдущей статье, поэтому для получения этих данных мы будем использовать интерфейс ICertRequest3.

PS C:\> $CertRequest = New-Object -ComObject CertificateAuthority.Request.1
PS C:\> $CertRequest.GetCAProperty("dc1\contoso ca",0x18,0,1,0)
1
PS C:\> $CertRequest.GetCAProperty("dc2\contoso-dc2-ca",0x18,0,1,0)
0
PS C:\> $CertRequest.GetCAProperty("dc1\contoso ca",0x19,0,1,0)
1
PS C:\> $CertRequest.GetCAProperty("dc2\contoso-dc2-ca",0x19,0,1,0)
0

Мы видим, что на сервере DC1 назначен 1 агент восстановления, а на DC2 ни одного. Сегодня мы добавим агентов восстановления на DC2.

Примечание: чем отличаются KRACERTUSEDCOUNT и KRACERTCOUNT? KRACERTCOUNT показывает сколько всего KRA назначено на конкретном сервере CA. А KRACERTUSEDCOUNT показывает сколько из них будет использовать CA при архивации каждого закрытого ключа сертификата и это число может быть меньше, чем общее количество сертификатов KRA. В таком случае CA рандомно выбирает KRACERTUSEDCOUNT сертификатов и шифрует ими закрытый ключ. Например, KRACERTCOUNT = 5, а KRACERTUSEDCOUNT = 3, то CA при архивации ключа выберет рандомно 3 сертификата KRA из списка и зашифрует ими ключ. И только эти 3 агента восстановления могут восстановить ключ, а остальные 2 уже не смогут.

Получение списка KRA назначенных на сервере CA

Чтобы посмотреть сами сертификаты KRA, мы должны воспользоваться PropID = CR_PROP_KRACERT (0x1A).

И вот что мы увидим:

PS C:\> $CertAdmin.GetCAProperty("dc1\contoso ca",0x1A,0,3,0)
-----BEGIN CERTIFICATE-----
MIIGaDCCBVCgAwIBAgIKFR0WYAAAAAAABDANBgkqhkiG9w0BAQUFADBDMRMwEQYK
CZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHY29udG9zbzETMBEGA1UE
AxMKQ29udG9zbyBDQTAeFw0wOTAyMTUxNTA1MjVaFw0xMTAyMTUxNTA1MjVaMFYx
EzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdjb250b3NvMQ4w
DAYDVQQDEwVVc2VyczEWMBQGA1UEAxMNQWRtaW5pc3RyYXRvcjCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAJG7T5yMninhrkXFQU8WGUr5BYYUeOz10Rkn
<...>

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

$CertAdmin.GetCAProperty("dc1\contoso ca",0x1A,$Index,3,0)

Этот сертификат можно спокойно записать в файл и через GUI просмотреть его свойства. Но вообще это не родной формат сертификата KRA для данного интерфейса, поскольку добавлять сертификаты KRA на сервере CA нужно в ASN1 DER-encoded string формате. Т.е. Flags должен быть не 0, а 2:

$CertAdmin.GetCAProperty("dc1\contoso ca",0x1A,$Index,3,2)

и увидим примерно такое:

PS C:\> $kra = $CertAdmin.GetCAProperty("dc1\contoso ca",0x1a,0,3,2)
PS C:\> $kra
?????A???`  ?????c?♣??????????????????T???????????????????????????????????????T????????????????????????????????????????
??ca♣??????☺???????????????????????????????????????????????????????????????????????????????????????????????????????????
??????????????????????☺????????Є?????????????????☻???????☻??????????????ЎЖ?????????????CA??????Ё????????????h??A???????
??????????????????????????????????????????????╣????????????????????????????????????????????????????????????????????????
?????????????????????C?cЁ??????C???????????????????????????????????????????????????????????????????????????????????????
???????????????????????????????????????Х???CA?????Ё???????CA?????Б????CA??????????????????????a???☺????????????????????
????????????????????????????????????c?????????????????????????????????????H╝??????????????????{?????????????

выглядит весьма уныло, но вот в таком формате и надо записывать сертификаты агентов восстановления в CA.

Назначение новых KRA для CA

Когда мы рассмотрели основные моменты связанные с агентами восстановления в контексте CryptoAPI, мы можем приступить к финальной части — назначению новых агентов восстановления. В принципе, здесь нет ничего сложного, поскольку для добавления KRA нужно сделать 5 вещи:

  1. Получить массив байтов, которые бы представляли сертификат агента восстановления откуда угодно. Хоть из AD, хоть из файла сертификата;
  2. Используя специальный конвертер, разобрать этот массив на пары байтов, сконвертировать эти пары в little-endian hex строку, получить десятичное представление этой hex-строки и получить итоговую символьную строку;
  3. Записать эту строку используя метод SetCAProperty();
  4. Задать новое количество используемых агентов восстановления;
  5. Перезапустить службу CA.

Первый этап у нас уже пройден, поскольку у нас уже есть такой массив в AD. Если сертификата агента восстановления нет в AD, то можно его получить из файла:

$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$cert.Import(".\desktop\kra.cer")
$bytes = $cert.RawData

и переменная $bytes будет содержать такой же байтовый массив.

А вот с конвертером чуть сложнее. Я не нашёл ни одного готового конвертера, который бы выполнял все шаги из пункта 2, поэтому я написал свой конвертер с блек-джеком и шлюхами:

function ConvertTo-DERstring ([byte[]]$bytes) {
    # создаём объект StringBuilder, который нам будет собирать конечную символьную строку
    $SB = New-Object System.Text.StringBuilder
    # преобразовываем каждый байт в его hex значение
    $bytes1 = $bytes | %{"{0:X2}" -f $_}
    # циклом перебираем каждую пару байт
    for ($n = 0; $n -lt $bytes1.count; $n = $n + 2) {
        # переставляем элементы пары местами, чтобы получить little-endian hex-строку
        # после чего получаем десятичное представление этой строки и получаем Unicode
        # символ, который соответствует этому десятичному значению.
        [void]$SB.Append([char](Invoke-Expression 0x$(($bytes1[$n+1]) + ($bytes1[$n]))))
    }
    $SB.ToString()
}

А дальше уже по накатанной дороге:

[Administrator] # создаём COM объект ICertAdmin2
[Administrator] $CertAdmin = new-object -com certificateauthority.admin.1
[Administrator] # выбираем все Enterprise CA
[Administrator] $ldap = [ADSI]"LDAP://CN=KRA, CN=Public Key Services, CN=Services, CN=Configuration, DC=contoso,dc=com"
[Administrator] $kra = $ldap.psbase.children | %{$_}
[Administrator] # сразу конвертируем все сертификаты в DER-encoded string
[Administrator] $kra1 = ConvertTo-DERstring $kra[1].usercertificate[0]
[Administrator] $kra2 = ConvertTo-DERstring $kra[1].usercertificate[1]
[Administrator] # можем убедиться на месте, что строка имеет ожидаемый вид
[Administrator] $kra2
?????A???E  ?????c?♣??????????????????T???????????????????????????????????????T????????????????????????????????????????
??ca♣??????☺????????????????????????????K?????????????????????????????????????????????т????????????????????????????????
??????????????????????☺????????Є?????????????????☻???????☻??????????????ЎЖ?????????????CA??????Ё????????????h??A???????
??????????????????????????????????????????????╣????????????????????????????????????????????????????????????????????????
?????????????????????C?cЁ??????C???????????????????????????????????????????????????????????????????????????????????????
???????????????????????????????????????Х???CA?????Ё???????CA?????Б????CA??????????????????????a???☺????????????????????
????????????????????????????????????????????????????????????????????????????????????????????????????????????
[Administrator] # и записываем первую строку сертификат агента восстанвовления в нулевой индекс
[Administrator] $CertAdmin.SetCAProperty("dc2\contoso-dc2-ca",0x1a,0,3,$kra1)
[Administrator] # все последующие строки сертификатов записываются в последующие индексы массива
[Administrator] $CertAdmin.SetCAProperty("dc2\contoso-dc2-ca",0x1a,1,3,$kra2)
[Administrator] # используем PropID = KRACERTUSEDCOUNT (0x18), чтобы сказать сколько у нас будет использоваться агентов
[Administrator] # я буду их использовать все
[Administrator] $CertAdmin.SetCAProperty("dc2\contoso-dc2-ca",0x18,0,1,2)
[Administrator] # перезапускаем службу CertSvc
[Administrator] $wmi = gwmi win32_service -comp dc2 -filter "name='certsvc'"
[Administrator] [void]$wmi.StopService()
[Administrator] [void]$wmi.StartService()
[Administrator] # и проверяем нашу работу:
[Administrator] $CertReq = new-object -com certificateauthority.request.1
[Administrator] $CertReq.GetCAProperty("dc2\contoso-dc2-ca",0x18,0,1,0)
2
[Administrator] $CertReq.GetCAProperty("dc2\contoso-dc2-ca",0x19,0,1,0)
2
[Administrator]

всё.

Update 05.12.2009: разработчики подтвердили баг в ICertAdmin2 интерфейсе. Проблема заключается в том, что после первого запроса указанных PropID они кешируются и при смене контекста конфигурации (Certification Authority), этот кеш не очищается.


Прежде чем продолжать исследование CryptoAPI COM интерфейсов, предлагаю посмотреть несколько полезных ссылок и поговорить о багах в интерфейсе ICertAdmin2.

При использовании интерфейса ICertAdmin2, ICertRequest2D (этот интерфейс мы ещё посмотрим в будущих постах) мы используем различные аргументы для получения свойст CA с использованием метода GetCAProerty(). И вот их где мы можем получить числовые значения этих аргументов:

А теперь поговорим о багах. Как выяснилось, ICertAdmin2::GetCAProperty(), который определён в библиотеке certadm.dll имеет баги с PropID, которые связаны с количественной информацией. Будь то количество сертификатов CA, количество агентов восстановления ключей (Key Recovery Agents) и т.д. И вот в чём это выражается:

  • DC1 — корневой CA с именем Contoso CA. Имеет 2 сертификата CA и одного назначенного агента восстановления;
  • DC2 — подчинённый CA с именем Contoso-DC2-CA. Имеет 1 сертификат CA и ни одного назначенного агента восстановления.

Мы будем получать данные как с локального CA, так и с удалённого CA запуская код с каждого сервера. Кратко об используемых PropID:

  • 0x0B — количество сертификатов CA;
  • 0x19 — общее количество агентов восстановления на CA;
  • 0x18 — общее количество используемых агентов восстановления;

И вот какие результаты мы видим, когда собираем эти свойства с сервера DC1:

[Administrator] # создаём COM объек:
[Administrator] $CertAdmin = New-Object -com CertificateAuthority.Admin.1
[Administrator] # получаем список сертификатов CA на локальном серве
[Administrator] # мы видим 2 сертификата, как положено
[Administrator] $certadmin.GetCAProperty("dc1\contoso ca",0xb,0,1,0)
2
[Administrator] # и он то же самое показывает и для удалённого сервера, хотя там всего 1 сертификат
[Administrator] $certadmin.GetCAProperty("dc2\contoso-dc2-ca",0xb,0,1,0)
2
[Administrator] # ладно, смотрим, сколько всего KRA у нас на локальном сервере. Их 1, как на самом деле
[Administrator] $certadmin.GetCAProperty("dc1\contoso ca",0x19,0,1,0)
1
[Administrator] # используемых сертификатов тоже 1.
[Administrator] $certadmin.GetCAProperty("dc1\contoso ca",0x18,0,1,0)
1
[Administrator] # как я уже говорил, на DC2 нет агентов восстановления, хотя метод показывает их по одному в обоих случаях
[Administrator] $certadmin.GetCAProperty("dc2\contoso-dc2-ca",0x19,0,1,0)
1
[Administrator] $certadmin.GetCAProperty("dc2\contoso-dc2-ca",0x18,0,1,0)
1
[Administrator]

собираем ту же информацию, запуская код с сервера DC2:

PS C:\> # создаём COM объект:
PS C:\> $CertAdmin = New-Object -com CertificateAuthority.Admin.1
PS C:\> # получаем список сертификатов CA с локального сервера.
PS C:\> # их мы видим ровно 1, как и должно быть
PS C:\> $certadmin.GetCAProperty("dc2\contoso-dc2-ca",0xb,0,1,0)
1
PS C:\> # в предыдущем примере мы видели, что на DC1 2 сертификата CA.
PS C:\> # проверим, так ли это?
PS C:\> $certadmin.GetCAProperty("dc1\contoso ca",0xb,0,1,0)
1
PS C:\> # wow! обломс, метод возвращает тоже 1. А сколько у нас агентов восстановления?
PS C:\> # и снова облом. На сервере DC1 нет агентов восстановления (хотя предыдущий пример говорит об обратном):
PS C:\> $certadmin.GetCAProperty("dc1\contoso ca",0x19,0,1,0)
0
PS C:\> $certadmin.GetCAProperty("dc1\contoso ca",0x18,0,1,0)
0

а теперь посмотрим на результаты с использованием того же метода и тех же PropID, но с использованием интерфейса ICertRequest, который определён в библиотеке certcli.dll. Те же тесты уже возвращают правильные результаты:

с DC1:

[Administrator] $request = New-Object -com CertificateAuthority.Request.1
[Administrator] $request.GetCAProperty("dc1\contoso ca",0xb,0,1,0)
2
[Administrator] $request.GetCAProperty("dc2\contoso-dc2-ca",0xb,0,1,0)
1
[Administrator] $request.GetCAProperty("dc1\contoso ca",0x19,0,1,0)
1
[Administrator] $request.GetCAProperty("dc1\contoso ca",0x18,0,1,0)
1
[Administrator] $request.GetCAProperty("dc2\contoso-dc2-ca",0x19,0,1,0)
0
[Administrator] $request.GetCAProperty("dc2\contoso-dc2-ca",0x18,0,1,0)
0
[Administrator]

с DC2:

PS C:\> $request = New-Object -com CertificateAuthority.Request.1
PS C:\> $request.GetCAProperty("dc2\contoso-dc2-ca",0xb,0,1,0)
1
PS C:\> $request.GetCAProperty("dc1\contoso ca",0xb,0,1,0)
2
PS C:\> $request.GetCAProperty("dc1\contoso ca",0x19,0,1,0)
1
PS C:\> $request.GetCAProperty("dc1\contoso ca",0x18,0,1,0)
1
PS C:\> $request.GetCAProperty("dc2\contoso-dc2-ca",0x19,0,1,0)
0
PS C:\> $request.GetCAProperty("dc2\contoso-dc2-ca",0x18,0,1,0)
0
PS C:\>

Поэтому следует с осторожностью относиться к выводу ICertAdmin2::GetCAProperty(), а ещё лучше — использовать этот метод, который реализован в интерфейсе ICertRequest::GetCAProperty(). Чем это вызвано я пока не знаю, но задал я задал вопрос нужным людям, поэтому я дам знать в случае ответа на вопрос.