Contents of this directory is archived and no longer updated.

Posts on this page:

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

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

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

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

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

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

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

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


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

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


[↓] [vPodans]

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

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

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


[↓] [vPodans]

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

$PrivateKey = New-Object -ComObject X509Enrollment.CX509PrivateKey

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

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

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

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

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

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

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

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

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

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

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

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

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

$EKU = New-Object -ComObject X509Enrollment.CX509ExtensionEnhancedKeyUsage

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

$OIDs = New-Object -ComObject X509Enrollment.CObjectIDs

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

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

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

$Cert.X509Extensions.Add($EKU)

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

[↓] [vPodans] $cert


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



[↓] [vPodans]

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

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

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

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

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

$endCert = $Request.CreateRequest(0x0)

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

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

[↓] [vPodans]

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

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

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

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

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

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

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

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

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

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

В предыдущем посте я показал, как можно легко и удобно подписывать скрипты и как усилить безопасность их исполнения, а так же предупредить несанкционированное изменение кода. Но, как я уже отметил, подписывать каждый раз скрипт из консоли не особо удобно. Например, если вы занимаетесь финальной отладкой скрипта в редакторе, как PowerGUI или PowerShell ISE, то в конце отладки вы просто сохраняете скрипт и закрываете редактор. Понятно, что ещё отдельно запускать консоль PowerShell будет неудобно. Поэтому я написал небольшой скрипт с установщиком, который позволит из проводника по правому нажатию на PS1 файл вы сможете подписывать скрипты.

Чтобы поместить свой элемент в контекстное меню при нажатии на PS1 файлы (на других типах файлов его не будет) нам нужно добавить некоторые значения в реестр по пути:

HKLM\Software\Classes\Microsoft.PowerShellScript.1\Shell

подключ с названием нашего элемента – Sign It и в нём уже команду, которая будет отрабатываться при нажатии на него. Я решил не изобретать велосипед и воспользовался приёмами, которые использовал ещё в старом блоге: Полезная безделушка Hash SHA1 на PowerShell. Шапка по сути осталась та же, только тело (исполняемый модуль) изменился. И вот, что получилось для PowerShell 1.0 (для V2 опубликую ниже):

#####################################################################
# Sign PS1 Script.ps1
# Version 1.5
#
# Automatically sign PowerShell script from .PS1 file context menu
# Fully compatible with PowerShell 1.0
#
# Vadims Podans (c) 2009
# http://www.sysadmins.lv/
#####################################################################

$FilePath = Read-Host "Specify path to hold SignIt.PS1"
if (Test-Path $FilePath) {
    $FilePath = $FilePath + "\" + "SignIt.ps1"
    $RegPath = "Registry::HKLM\Software\Classes\Microsoft.PowerShellScript.1\Shell\Sign It\command"
    $RegValue = "C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe -noninteractive -noprofile -command $FilePath '%1'"
    New-Item -Path $RegPath -Force -ErrorAction SilentlyContinue
    if (Test-Path $RegPath) {
        New-ItemProperty -Path $RegPath -Name "(Default)" -Value $RegValue
        New-Item -ItemType file -Path $FilePath -Force
        if (Test-Path $FilePath) {
            $exefile = 'param ($file)
$cp = new-object Microsoft.CSharp.CSharpCodeProvider
$cpar = New-Object System.CodeDom.Compiler.CompilerParameters
$HideWindow = 0x0080
$ShowWindow = 0x0040
$Code = @"
using System;
using System.Runtime.InteropServices;
namespace Win32API
{
    public class Window
    {
        [DllImport("user32.dll")]
        public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
    }
}
"@
$cp.CompileAssemblyFromSource($cpar, $code)
$PSHandle = (Get-Process –id $pid).MainWindowHandle
[Win32API.Window]::SetWindowPos($PSHandle, 0, 0, 0, 0, 0, $HideWindow)
function _msgbox_ ($title, $text, $type = "None") {
    [void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms")
    $msg = [Windows.Forms.MessageBox]::Show($text, $title, [Windows.Forms.MessageBoxButtons]::ok, 
    [Windows.Forms.MessageBoxIcon]::$type)
}
$cert = @(dir cert:\currentuser\my -codesigning)[0]
if (!$cert) {
    _msgbox_ -title "Error" -text "Here is no valid signing certificate!" -type "Error"
    exit
}
$status = Set-AuthenticodeSignature "$file" $cert -TimestampServer "http://timestamp.verisign.com/scripts/timstamp.dll"
if ($status.status -eq "Valid") {
    _msgbox_ -title "Success" -text "Script is now signed! Enjoy!"
} else {
    _msgbox_ -title "Error" -text $status.StatusMessage -type "Error"
}
exit'
            Set-Content -Path $FilePath -Value $exefile
            &$filepath $filepath
        }
        else {
            Write-Warning "Unable to create file in: $FilePath. Path is incorrect or you haven't sufficient permissions"
        }
    }
    else {
        Write-Warning "Unable to create registry key. May be you haven't sufficient permissions to HKLM hive"
    }
}

Данный скрипт является установщиком. Для реализации всех функций достаточно запустить этот скрипт и по требованию указать путь, где будет находиться рабочий скрипт (содержимое переменной $exefile).

Примечание: такой метод установки справедлив только для V2. При использовании PowerShell 1.0 установщик следует запускать непосредственно из консоли PowerShell.

Заметка: Здесь следует обратить внимание на блок кода, который идёт после PARAM в переменной $exefile. Данный блок кода скрывает саму консоль PowerShell и показывает только графическую часть, которая исполняется из скрипта. Функция скрытия консоли честно взята из блога Васи Гусева: Win32 API из PowerShell 1.0. Данный метод работает как в PowerShell V2 CTP3, так и в PowerShell 1.0.

И вариант скрипта для PowerShell V2:

#####################################################################
# Sign PS1 Script.ps1
# Version 1.5
#
# Automatically sign PowerShell script from .PS1 file context menu
#
# Require PowerShell V2, not compatible with PowerShell 1.0
#
# Vadims Podans (c) 2009
# http://www.sysadmins.lv/
#####################################################################
#requires -Version 2.0

$FilePath = Read-Host "Specify path to hold SignIt.PS1"
if (Test-Path $FilePath) {
    $FilePath = $FilePath + "\" + "SignIt.ps1"
    $RegPath = "Registry::HKLM\Software\Classes\Microsoft.PowerShellScript.1\Shell\Sign It\command"
    $RegValue = "C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe -WindowStyle Hidden -noprofile -command $FilePath '%1'"
    New-Item -Path $RegPath -Force -ErrorAction SilentlyContinue
    if (Test-Path $RegPath) {
        New-ItemProperty -Path $RegPath -Name "(Default)" -Value $RegValue
        New-Item -ItemType file -Path $FilePath -Force
        if (Test-Path $FilePath) {
            $exefile = 'param ($file)
function _msgbox_ ($title, $text, $type = "None") {
    [void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms")
    $msg = [Windows.Forms.MessageBox]::Show($text, $title, [Windows.Forms.MessageBoxButtons]::ok,
    [Windows.Forms.MessageBoxIcon]::$type)
}
$cert = @(dir cert:\currentuser\my -codesigning)[0]
if (!$cert) {
    _msgbox_ -title "Error" -text "Here is no valid signing certificate!" -type "Error"
    exit
}
$status = Set-AuthenticodeSignature "$file" $cert -TimestampServer "http://timestamp.verisign.com/scripts/timstamp.dll"
if ($status.status -eq "Valid") {
    _msgbox_ -title "Success" -text "Script is now signed! Enjoy!"
} else {
    _msgbox_ -title "Error" -text $status.StatusMessage -type "Error"
}
exit'
            Set-Content -Path $FilePath -Value $exefile
            &$filepath $filepath
        }
        else {
            Write-Warning "Unable to create file in: $FilePath. Path is incorrect or you haven't sufficient permissions"
        }
    }
    else {
        Write-Warning "Unable to create registry key. May be you haven't sufficient permissions to HKLM hive"
    }
}

Так же, после копирования файла установщик попытается сразу подписать рабочий скрипт, чтобы этого не пришлось делать потом вручную. После этого в контекстном меню PS1 файлов будет элемент Sign It:

Context menu

И если у пользователя есть сертификат для подписи скриптов, то при нажатии на этот элемент получите сообщение:

Success signing message

В противном случае получите ошибку:

Failed signing message

Вот так можно значимо упростить процесс подписывания скриптов не открывая консоль PowerShell. Но если файлов будет много, то ничего не мешает основную часть кода засунуть в цикл Foreach. Например:

$cert = @(dir cert:\currentuser\my -codesigning)[0]
dir -Include *.ps1 -Recurse | %{Set-AuthenticodeSignature "$_" $cert}

На этом пока о подписывании скриптов всё, что я хотел рассказать.

Update 30.08.2009: немного переделал скрипты с учётом следующих поправок:

  • Добавлена возможность подписывания скриптов с использованием сервера времени VeriSign, который в подписи ставит подписанную VeriSign' ом цифровую метку времени.
  • Исправлены ошибки с выводом сообщений о результате операции. Например, если пользователь отменял операцию подписи, либо происходила другая ошибка в процессе подписывания, то появлялось сообщение о том, что файл подписан (хотя это было не так). Сейчас сделана проверка поля Status.
  • в связи с этим изменил код в посте на обновлённый.

И, собственно, кнопки для скачивания скриптов:

  • для PowerShell 1.0
  • для PowerShell 2.0

Как известно практически всем пользователям PowerShell, в целях безопасности была введена политика запуска скриптов. Которая имеет 4 режима:

  • Restricted – запрещено исполнять любые скрипты, возможна работа только в интерактивной консоли
  • AllSigned – все скрипты должны быть криптографически подписаны
  • RemoteSigned – только полученные из недоверенных источников (например, интернет) скрипты должны быть подписаны
  • Unrestricted – наименее безопасный уровень, допускается исполнение любых скриптов

В версии V2 (пока ещё CTP3) скрипты PowerShell приравняли к исполняемым файлам и эти файлы стали неотключаемо мониториться политикой Software Restriction Policies. Я об этом уже писал ранее: PowerShell V2 и Software Restriction Policies. Поначалу меня это сильно напрягало, но после пришёл к мнению, что это правильно. Неправильно только то, что мы этого не видим (ps1 расширение нигде не фигурирует). С этим бороться можно двумя методами – явно указывать пути, откуда разрешён запуск .ps1 файлов или подписать их все.

Если компьютеров в сети больше одного, то самое идеальное для решения задачи будет наличие домена Active Directory и, по возможности, Enterprise Certification Authity (CA). Наличие домена решит массу задач, как распространение политики SRP в пределах домена, распространение сертификата в пределах домена, контроль версии сертификата, которым подписаны скрипты.

Итак, для начала нам нужно получить сертификат, которым будут подписываться скрипты. В целях безопасности следует создать ограниченную учётную запись пользователя из под которой администратор (в большинстве случаев) будет подписывать скрипты. В книге PowerShell In Action для генерации сертификата предлагается использовать makecert.exe, который входит в состав Visual Studio SDK, но как мне кажется более правильным будет использование CA. В Windows Server CA для этих целей есть уже готовый шаблон, который называется Code Signing. Но принципиальной разницы нету, каким инструментом вы будете генерировать сертификат и я опишу процесс получения сертификата с использованием доменного CA.

Если CA у нас уже установлен, то открываем оснастку Certification Authority и переходим в раздел Certificate Templates. Нажимаем правой кнопкой и выбираем Manage. Откроется редактор шаблонов. Если у вас Enterprise или Datacenter редакции Windows Server, то вы можете создать свой настроенный шаблон. Но я не вижу в этом необходимости. На данном этапе нам необходимо разрешить ограниченному пользователю запрашивать сертификаты этого шаблона. Для этого в закладке Security шаблона Code Signing нужно разрешить чтение и запрос сертификата ограниченному пользователю. Когда эта процедура проделана, редактор шаблонов можно закрыть. После чего в оснастке Certification Authority снова нажать правой кнопкой на разделе Certificate Templates –> New –> Certificate Template to Issue и в списке выбрать шаблон Code Signing.

После чего нужно залогиниться этим пользователем, запустить оснастку Certificate Manager (Start –> Run… –> certmgr.msc) и выполнить запрос сертификата. В списке шаблонов должен быть и добавленный нами Code Signing. Когда сертификат будет запрошен не надо закрывать оснастку сертификатов. Далее нам потребуется экспортировать открытую часть сертификата в x509 файл (с расширением .cer). Экспорт открытой части нам потребуется для проверки подписи и организации доверия подписи в пределах домена. Экспортированный сертификат необходимо теперь доставить администратору(-ам), который отвечает за групповую политику.

В групповых политиках (чаще всего в доменной политике) необходимо создать новую политику Software Restriction Policies и в Additiona Rules добавить правило сертификата (Certificate Rule) и указать экспортированный сертификат. Это необходимо затем, что PowerShell для проверки доверия сертификата ищет его в контейнере Trusted Publishers и только Software Restriction Policies позволяет централизовано распространять сертификаты в этот контейнер.

Теперь можно приступать к подписыванию скриптов. Для этих целей используется командлет Set-AuthenticodeSignature и синтаксис его такой:

Set-AuthenticodeSignature $file $cert

Где $file – путь к скрипту и $cert – объект сертификата, который получается следующим образом:

$cert = @(dir cert:\CurrentUser\My -codesigning)[0]

здесь мы явно указываем, что нам нужен сертификат, у которого в EKU (Enchanced Key Usage) указан Code Sgining. В нашем случае он будет всего 1. Но если их окажется несколько, то мы выберем самый первый. Вот как это будет выглядеть на практике:

[Desktop] Set-ExecutionPolicy allsigned [Desktop] Get-ExecutionPolicy AllSigned [Desktop] .\uptime.ps1 File C:\Documents and Settings\Signer\Desktop\uptime.ps1 cannot be loaded. The file C:\Documents and Settings\Signer \Desktop\uptime.ps1 is not digitally signed. The script will not execute on the system. Please see "get-help about_signing" for more details.. At line:1 char:13 + .\uptime.ps1 < +="" categoryinfo="" :="" notspecified:="" (:)="" [],="" pssecurityexception="" +="" fullyqualifiederrorid="" :=""> [Desktop] $cert = @(dir cert:\currentuser\my -codesigning)[0] [Desktop] $cert Directory: Microsoft.PowerShell.Security\Certificate::currentuser\my Thumbprint Subject ---------- ------- 42E5B32A19885C6ADCF9683BDA1C871E0FE5E0DB CN=Signer, CN=Users, DC=contoso, DC=com [Desktop] Set-AuthenticodeSignature uptime.ps1 $cert Directory: C:\Documents and Settings\Signer\Desktop SignerCertificate Status Path ----------------- ------ ---- 42E5B32A19885C6ADCF9683BDA1C871E0FE5E0DB Valid uptime.ps1 [Desktop] .\uptime.ps1 System Uptime for DC1 is: 1 days 4 hours 19 minutes 20 seconds [Desktop]

Я наглядно показал, как это работает. Мы сначала перевели политику исполнения скриптов в AllSigned и убедились, что неподписанный скрипт не исполняется. После чего я подписал этот скрипт и попробовал снова. Как видите, скрипт теперь исполнился.

Если не будет выполнено условие распространения сертификата посредством политики SRP в контейнер Trusted Publishers, то вы получите вот такое сообщение:

[Desktop] .\uptime.ps1 Do you want to run software from this untrusted publisher? File C:\Documents and Settings\Signer\Desktop\uptime.ps1 is published by CN=Signer, CN=Users, DC=contoso, DC=com and is not trusted on your system. Only run scripts from trusted publishers. [V] Never run [D] Do not run [R] Run once [A] Always run [?] Help (default is "D"):d File C:\Documents and Settings\Signer\Desktop\uptime.ps1 cannot be loaded because you have elected to no t run this software now. At line:1 char:12 + .\uptime.ps1 [Desktop]

Вот таким образом мы решаем задачу исполнения только проверенного набора скриптов. В этом смысле PowerShell 1.0 менее безопасный и удобный, поскольку мы не можем политикой SRP блокировать исполнение PS1 файлов как класс и имеем только один выход – принудительное подписывание скриптов. В версии V2 политика исполнения скриптов удобно интегрируется с SRP. Удобство интегрирования в том, что SRP помимо распространения сертификата в пределах домена так же на основе этого правила разрешает исполнять эти скрипты в обход общего ограничения на PS1 файлы.

В следующем посте я расскажу, как можно упростить процесс подписывания скриптов. Так что не отключаемся :-)