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

Вот так можно значимо упростить процесс подписывания скриптов не открывая консоль PowerShell. Но если файлов будет много, то ничего не мешает основную часть кода засунуть в цикл Foreach. Например:
$cert = @(dir cert:\currentuser\my -codesigning)[0]
dir -Include *.ps1 -Recurse | %{Set-AuthenticodeSignature "$_" $cert}
На этом пока о подписывании скриптов всё, что я хотел рассказать.
Update 30.08.2009: немного переделал скрипты с учётом следующих поправок:
- Добавлена возможность подписывания скриптов с использованием сервера времени VeriSign, который в подписи ставит подписанную VeriSign' ом цифровую метку времени.
- Исправлены ошибки с выводом сообщений о результате операции. Например, если пользователь отменял операцию подписи, либо происходила другая ошибка в процессе подписывания, то появлялось сообщение о том, что файл подписан (хотя это было не так). Сейчас сделана проверка поля Status.
- в связи с этим изменил код в посте на обновлённый.
И, собственно, кнопки для скачивания скриптов: