Contents of this directory is archived and no longer updated.

Подоспела ещё одна задачка для 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, но они сегодня интереса представлять не будут. Как мы уже знаем, в:

  • NTAuthCertificates публикуются все сертификаты CA, которые выдают сертификаты для аутентификации пользователей в домене. В том числе для логона смарт-картой, аутентификации в IIS или для аутентификации EAP-TLS в VPN. Все сертификаты в этой записи хранятся в виде массива;
  • AIA публикуются сертификаты промежуточных (Intermediate) CA, за счёт чего можно значительно сократить время построения цепочек сертификатов. При этом не обязательно должен содержать сертификаты CA, которые зарегистрированы в текущем лесу. Это могут быть сертификаты промежуточных CA сторонних компаний, сертификаты которых вы используете. По большому счёту сюда должны публиковаться все сертификаты;
  • CDP публикуются CRL списки CA для ускорения проверки сертификатов в цепочке. Так же, как и в случае с AIA не обязательно может содержать CRL'ы CA только текущего леса. Сюда могут публиковаться и CRL сторонних CA, сертификаты которых вы используете;
  • Certification Authority публикуются сертификаты корневых CA, которым должен доверять весь лес.

Вы спросите, а зачем это всё, если то же самое можно сделать через групповые политики? Ответ тут достаточно очевиден. Дело в том, что PKI не видит границ доменов и эти сертификаты распространяются по всему лесу вместе с репликацией. Поэтому для добавления нового доверенного корневого CA вам придётся создавать одинаковую политику в каждом домене леса. И вы некоторые объекты (например CRL) не можете распространять через GPO.

Какие у нас есть инструменты для управления данными контейнерами? Их у нас несколько:

  • ADSIEdit.msc — обеспечивает только просмотр содержимого контейнеров в AD (но сами записи там нечитабельны);
  • PKIView.msc — позволяет просматривать содержимое каждого контейнера и удалять оттуда сертификаты. C использованием данной консоли мы можем добавлять сертификаты только в NTAuthCertificates. Остальные — только чтение и удаление;
  • certutil — интерфейс командной строки, который позволяет добавлять, просматривать контейнеры и удалять сертифакты из них.

Кажется, что certutil'а хватит всем. Но у него есть одна большая проблема — ужасный синтаксис. Напрмер, если вы хотите посмотреть CRL'ы в AD, то придётся делать что типа такого:

certutil –viewstore ldap:///CN=MyCA,CN=CRL,CN=CDP,CN=Public%20Key%
20Services,CN=Services,CN=Configuration,DC=contoso,DC=com?certificateRevocationList?base?objectClass=cRLDistributionPoint

этот синтаксис возможно чем-то универсален, но cовершенно неудобный для использования в командной строке, поэтому я поставил задачу решить этот вопрос с помощью PowerShell. И вот как я его решил:

Примечание: к сожалению я не в состоянии объяснять все особенности работы ADSI и PowerShell, поэтому для понимания работы скрипта нужно иметь представление и некоторый опыт скриптования с использованием ADSI и PowerShell.

#####################################################################
# dspublish.ps1
# Version 0.7
#
# Adds certificates in Active Directory containers
#
# Vadims Podans (c) 2009
# http://www.sysadmins.lv/
#####################################################################
#requires -Version 2.0

# любая ошибка будет фатальной, поэтому при её возникновении останавливаем работу
# скрипта, чтобы предотвратить фатальные изменения в Active Directory
trap {break}
# объявляем глобальные переменные, которые будут использоваться всеми функциями скрипта
$script:ConfigContext = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().GetDirectoryEntry().distinguishedName
$script:ConfigContext = "CN=Public Key Services,CN=Services,CN=Configuration," + $script:ConfigContext
$script:Cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2

function Publish-ADPKIObject {
<#
.Synopsis
    Publishes certificates to Active Directory containers
.Description
    Publishes certificates to Public Key Services containers in Active Directory.
.Parameter File
    Specifies the path to certificate file or X509Certificate2 object.
    Certificates may be passed through pipeline.
.Parameter Container
    Specifies the AD PKI container to publish file. For certificates only
    following containers MUST be used:
    
    RootCA - indicates that certificate will be published to Certification Authorities container
    SubCA - indicates that certificate will be published to AIA container
    NTAuthCA - indicates that certificate will be added to NTAuthCertificate directory
    entry. If not exist, entry will be created.
    
    you MAY specify several containers at once. Certificate will be added to all
    specified containers. Entry names are based on certificate subject.
    
.Parameter Force
    forces object rewrite if object already exist in Active Directory
.EXAMPLE
    dir *.cer | Publish-ADPKIObject RootCA, SubCA
    
    will publish all .CER certificates to Certification Authorities and AIA containers
.EXAMPLE
    Publish-ADPKIObject certificate.cer RootCA -Force
    
    will publish 'Certificte.cer' certificate to Certification Authorities container. If
    object already exist, it will rewrited
.Outputs
    This command provide a resultant of operation.
#>
[CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [object[]]$file,
        [Parameter(Mandatory = $true, Position = 1)]
        [string[]]$Container,
        [switch]$Force
    )
    begin {
        # функция, которая будет осуществлять запись сертификатов в AD
        function _ldaproutine_ ($ldap, $CN, $script:Cert, $name, $Force) {
            $path = [ADSI]"LDAP://$ldap"
            # убеждаемся, что такой же объект не существует в AD
            if ($($path.psbase.children | ?{$_.cn -eq $CN})) {
                # если существует, проверяем ключ Force, который позволяет перезаписывать
                # объекты
                if ($force) {
                    $ldap = [ADSI]"LDAP://CN=$CN,$ldap"
                    # если ключ Force указан, то мы просто перезаписываем свойство
                    # cACertificate новым сертификатом
                    $ldap.put("cACertificate", $script:Cert.RawData)
                    # предыдущей строкой мы просто перезаписали объект, который теперь
                    # надо записать в AD методом SetInfo()
                    $retn = $ldap.SetInfo()
                    if ($?) {Write-Host "`'$CN`' certificate is sucessfully rewrited to `'$name`' container" -ForegroundColor Green}
                # если объект уже существует и ключ Force не указан, то просто выводим сообщение, что такой объект уже существует
                } else {Write-Warning "Object already exist in `'$name`' container. Use -Force switch to rewrite"}
            } else {
                # если объект ещё не существует в указанном контейнере, то создаём в нём новый объект
                # с типом certificationAuthority
                $CA = $path.Create("certificationAuthority","CN=$CN")
                # записываем сертификат в свойство cACertificate в бинарном виде из объекта X509Certificate2
                $CA.Put("cACertificate", $script:Cert.RawData)
                # записываем нулями обязательные поля, которые требуются для создания объекта. Использовать как есть.
                $CA.Put("authorityRevocationList", 0)
                $CA.Put("certificateRevocationList",0)
                # когда объект сформирован, записываем его в AD
                $retn = $CA.SetInfo()
                if ($?) {Write-Host "`'$CN`' certificate is sucessfully added to `'$name`' container" -ForegroundColor Green}
            }
        }
    }
    # рабочая секция, которая будет разбирать входные сертификаты и подготавливать необходимые данные
    # которые необходимы для записи 
    process {
        # выбираем текущий объект сертификата
        $script:Certificate = gi $file -ErrorAction Stop
        # проверяем, что объект не является готовым объектом X509Certificate2
        if ($script:Certificate -isnot [System.Security.Cryptography.X509Certificates.X509Certificate2]) {
            # если не является, то это будет файл. Проверяем расширение файла.
            # допускаются только расширения CER и CRT
            if (".cer", ".crt" -contains $script:Certificate.Extension) {
                # если это CER или CRT файл, то конвертируем его в объект X509Certificate2
                $script:Cert.Import($script:Certificate.FullName)
                # в переменную $CN записываем первую часть поля Subject сертификата
                [void]($script:Cert.Subject -match 'CN=([^,]+)')
                $CN = $matches[1]
            }
        # если у нас на конвейер или через аргументы поступил готовый X509Certificate2, то только выбираем
        # поле Subject в отдельную переменную.
        } else {
            $script:Cert = $script:Certificate
            [void]($script:Cert.Subject -match 'CN=([^,]+)')
            $CN = $matches[1]
        }
        # проверяем аргументы, которые указывают на контейнеры, куда надо записывать наши сертификаты.
        # я не делал проверку этих контейнеров в секции param() через использование ValidateSet
        # поскольку данная функция в будущем будет использоваться и для публикации CRL. А для
        # них нужно вручную прописывать имя записи в контейнере CDP и оно может быть произвольным.
        # отфильтровываем все неправильные контейнеры, которые были заданы при вызове функции
        $Container = $Container | ?{"RootCA","SubCA","NTAuthCA" -contains $_}
        # если после фильтрации ни одного валидного контейнера не осталось, то очень сильно ругаемся.
        if (!$Container) {throw "For certificate containers only following values are applicable: RootCA, SubCA, NTAuthCA"}
        # проверяем, что входной сертификат содержит свойство CertificateAuthority равным True.
        # это свойство соответствует расширению Basic Constraints в сертификате, которое может иметь
        # значения: Subject Type=CA - это сертификат CA и Subject Type=End entity, если это конечный
        # сертификат. А в X509Certificate2 конечный сертификат будет иметь значение False в свойстве
        # CertificateAuthority. Но работу скрипта не прерываем, а просто пропускаем текущий сертификат.
        if (!$script:Cert.Extensions | ?{$_.CertificateAuthority -eq $true}) {
            Write-Warning "Input certificate `'$CN`' is not recognized as CA certificate. Skipping"
            return
        }
        # а теперь подготавливаем необходимые данные для записи в зависимости от названия контейнера
        switch ($Container) {
            "RootCA" {
                # указываем название данного контейнера в AD
                $name = "Certification Authorities"
                # получаем LDAP объект этого контейнера
                $ldap = "CN=$name,$script:ConfigContext"
                # и отправляем всё это в функцию записи
                _ldaproutine_ $ldap $CN $script:Cert $name $Force
                $script:Cert.Reset()
            }
            "SubCA" {
                # здесь то же самое, что и для RootCA
                $name = "AIA"
                $ldap = "CN=$name,$script:ConfigContext"
                _ldaproutine_ $ldap $CN $script:Cert $name $Force
                $script:Cert.Reset()
            }
            "NTAuthCA" {
                $name = "NTAuthCertificates"
                $ldap = [ADSI]"LDAP://CN=$name,$script:ConfigContext"
                # поскольку NTAuthCertificates не является контейнером, а отдельной
                # записью, которая содержит массив сертификатов, то правила записи
                # здесь немного иные. Сначала проверяем, что эта запись уже существует в AD.
                if (!$ldap.cn) {
                    # если нет, то создаём её
                    $CA = ([ADSI]"LDAP://$script:ConfigContext").Create("certificationAuthority","CN=$name")
                    # заполняем обязательные свойства объекта и первый сертификат.
                    $CA.Put("authorityRevocationList", 0)
                    $CA.Put("certificateRevocationList",0)
                    $CA.put("cACertificate", $script:Cert.RawData)
                    # когда объект уже готов, то просто записываем его в AD
                    $retn = $CA.SetInfo()
                    if ($?) {Write-Host "`'$CN`' certificate is sucessfully added to `'$name`' container" -ForegroundColor Green}
                    return
                }
                # а вот если эта запись уже есть, то ничего создавать не надо, а просто добавляем
                # новый сертификат вдобавок к существующим. При этом обратите внимание, что объект добавляется
                # как массив (используется запятуя сразу за оператором). Это связано с особенностью работы PowerShell с
                # массивами. Поскольку бинарный сертификат сам по себе является массивом, то при простом добавлении
                # к существующему бинарному массиву, просто сделает ресайз текущего массива. Чтобы новый массив
                # записать как отдельный элемент нового массива - надо при помощи запятой явно это указать
                $ldap.cACertificate += ,$cert.RawData
                # и записываем объект обратно в AD.
                $retn = $ldap.SetInfo()
                if ($?) {Write-Host "`'$CN`' certificate is sucessfully added to `'$name`' container" -ForegroundColor Green}
                $script:Cert.Reset()
            }
        }
    }
}

А примеры использования этого скрипта приведены во встроенном хелпе и сводятся к одной из схем:

Publish-ADPKIObject <certificate> <container> –Force
<certificate> | Publish-ADPKIObject –Force

Причём вы можете указывать несколько контейнеров сразу, например: Publish-ADPKIObject file.cer NTAuthCA, RootCA, SubCA. Я думаю, что этот скрипт получился не такой уж и сложный и его разобрать с помощью моих комментариев не так и сложно. Его можно спокойно расширить под другие контейнеры, например, CDP или KRA. Единственное, что здесь пока не реализовано — санитизация имён объектов. На сколько я знаю, certutil этого тоже не поддерживает. Но сделать её надо. Правда, я пока не нашёл ни одного стандартного механизма, который бы санитизировал имена :(


Share this article:

Comments:

Di

А как этот скрипт использовать? Он каким-то образом должен быть интегрирован в powershell?

Vadims Podāns

можете просто скопировать и вставить код в консоль.

Di

Это понятно. Но я думал есть возможность использовать скрипт по типу командлета - с использованием справки, задания параметров и т.п. В конце поста у вас как раз и идет пример использования. Вопрос, как скрипт "добавить" в powershell, чтобы использовать его подобном образом. PS Сильно не пинайте, в powershell я новичек. Спасибо.

Vadims Podāns

в данном случае подцепить его можно через dot-sourcing. Хотя и после вставки кода в консоль, справка будет доступна. http://mctexpert.blogspot.com/2011/04/dot-sourcing-powershell-script.html

Comments are closed.