Contents of this directory is archived and no longer updated.

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

Share this article:

Comments:

qik

Как мне изменить скрипт что бы я мог за один присест подписать сразу большое количество файлов? Причем для меня желательно выделить необходимые файлы мышкой нажать пкм, выбрать "Подписать" и запустить подпись скопом. Хочу передавать в SignIt.ps1 не по одному файлу а сразу массив. Прошу помощи.

Alexander Dyakov

Вадим, $cert = @(dir cert:\currentuser\my -codesigning)[0] а ежели у меня там больше одного сертификата лежит? не лучше ли будет выяснить что там конкретно и потом $cert = Get-ChildItem cert:\CurrentUser\My -codesigning |where {$_.Subject -eq "CN=..."} ?

Vadims Podāns

Можно и выяснять, например, через X509Certificate2UI показывать окошко, где пользователь выберет нужный сертификат. На момент написания поста у меня не было такой задачи.

Comments are closed.