PowerShell脚本数字签名实战:自签名证书、代码签名与信任链验证

PowerShell脚本数字签名实战:自签名证书、代码签名与信任链验证

在 Windows 上编写 PowerShell 自动化脚本时,我们经常会遇到一个问题:

脚本明明是自己编写的,但在另一台电脑上运行时,却出现了 PSSecurityException。

例如:

text 复制代码
无法加载文件 xxx.ps1,因为此文件没有数字签名。

PSSecurityException
UnauthorizedAccess

如果将脚本放在 NAS、Windows 网络共享目录,或者通过 Git、压缩包等方式分发给其他电脑,还可能遇到:

  • 脚本已经签名,但 PowerShell 仍然拒绝执行。
  • Get-AuthenticodeSignature 显示 NotTrusted。
  • 脚本修改后,原来的签名失效。
  • 同一个脚本在电脑 A 可以运行,在电脑 B 却无法运行。
  • 签名证书过期后,历史脚本出现验证问题。
  • 已经设置 RemoteSigned,但依然提示 PSSecurityException。

这些问题涉及 PowerShell 的数字签名、证书信任链和执行策略。

本文通过一个完整的本地实验,介绍如何创建代码签名证书、为 PowerShell 脚本签名、验证签名完整性,以及理解证书信任和脚本执行权限之间的关系。

本章的最终目标不是简单地让某个脚本运行起来,而是理解:

如何确认脚本是谁签署的、签署后是否被修改,以及为什么某些电脑不信任这个签名。

适用环境:Windows 10/11、Windows PowerShell 5.1 和 Windows 上的 PowerShell 7.x。

本文使用自签名证书完成隔离实验。自签名证书主要适用于学习和测试,不建议直接用于正式的软件分发。涉及受信任根证书存储区的操作,仅应在经过授权的独立测试环境中完成。


一、为什么PowerShell脚本需要数字签名?

假设编写了一个 PowerShell 脚本:

text 复制代码
Set-PrivateFolder.ps1

它负责设置 Windows 文件夹访问权限。

开发者完成测试后,将脚本保存到文件服务器,供其他计算机使用。

这时会出现三个问题:

第一,如何证明脚本确实来自预期的发布者?

第二,如何判断脚本在分发过程中有没有被修改?

第三,接收脚本的计算机是否信任发布者?

数字签名主要用于解决前两个问题,并为第三个问题提供可验证的身份依据。

1.1 数字签名的工作原理

PowerShell 使用 Windows Authenticode 机制对脚本进行签名。

简化过程如下:
#mermaid-svg-Bm3tO4tlI0fYaPNG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Bm3tO4tlI0fYaPNG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Bm3tO4tlI0fYaPNG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Bm3tO4tlI0fYaPNG .error-icon{fill:#552222;}#mermaid-svg-Bm3tO4tlI0fYaPNG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Bm3tO4tlI0fYaPNG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Bm3tO4tlI0fYaPNG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Bm3tO4tlI0fYaPNG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Bm3tO4tlI0fYaPNG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Bm3tO4tlI0fYaPNG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Bm3tO4tlI0fYaPNG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Bm3tO4tlI0fYaPNG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Bm3tO4tlI0fYaPNG .marker.cross{stroke:#333333;}#mermaid-svg-Bm3tO4tlI0fYaPNG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Bm3tO4tlI0fYaPNG p{margin:0;}#mermaid-svg-Bm3tO4tlI0fYaPNG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Bm3tO4tlI0fYaPNG .cluster-label text{fill:#333;}#mermaid-svg-Bm3tO4tlI0fYaPNG .cluster-label span{color:#333;}#mermaid-svg-Bm3tO4tlI0fYaPNG .cluster-label span p{background-color:transparent;}#mermaid-svg-Bm3tO4tlI0fYaPNG .label text,#mermaid-svg-Bm3tO4tlI0fYaPNG span{fill:#333;color:#333;}#mermaid-svg-Bm3tO4tlI0fYaPNG .node rect,#mermaid-svg-Bm3tO4tlI0fYaPNG .node circle,#mermaid-svg-Bm3tO4tlI0fYaPNG .node ellipse,#mermaid-svg-Bm3tO4tlI0fYaPNG .node polygon,#mermaid-svg-Bm3tO4tlI0fYaPNG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Bm3tO4tlI0fYaPNG .rough-node .label text,#mermaid-svg-Bm3tO4tlI0fYaPNG .node .label text,#mermaid-svg-Bm3tO4tlI0fYaPNG .image-shape .label,#mermaid-svg-Bm3tO4tlI0fYaPNG .icon-shape .label{text-anchor:middle;}#mermaid-svg-Bm3tO4tlI0fYaPNG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Bm3tO4tlI0fYaPNG .rough-node .label,#mermaid-svg-Bm3tO4tlI0fYaPNG .node .label,#mermaid-svg-Bm3tO4tlI0fYaPNG .image-shape .label,#mermaid-svg-Bm3tO4tlI0fYaPNG .icon-shape .label{text-align:center;}#mermaid-svg-Bm3tO4tlI0fYaPNG .node.clickable{cursor:pointer;}#mermaid-svg-Bm3tO4tlI0fYaPNG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Bm3tO4tlI0fYaPNG .arrowheadPath{fill:#333333;}#mermaid-svg-Bm3tO4tlI0fYaPNG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Bm3tO4tlI0fYaPNG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Bm3tO4tlI0fYaPNG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bm3tO4tlI0fYaPNG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Bm3tO4tlI0fYaPNG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bm3tO4tlI0fYaPNG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Bm3tO4tlI0fYaPNG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Bm3tO4tlI0fYaPNG .cluster text{fill:#333;}#mermaid-svg-Bm3tO4tlI0fYaPNG .cluster span{color:#333;}#mermaid-svg-Bm3tO4tlI0fYaPNG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Bm3tO4tlI0fYaPNG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Bm3tO4tlI0fYaPNG rect.text{fill:none;stroke-width:0;}#mermaid-svg-Bm3tO4tlI0fYaPNG .icon-shape,#mermaid-svg-Bm3tO4tlI0fYaPNG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bm3tO4tlI0fYaPNG .icon-shape p,#mermaid-svg-Bm3tO4tlI0fYaPNG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Bm3tO4tlI0fYaPNG .icon-shape .label rect,#mermaid-svg-Bm3tO4tlI0fYaPNG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bm3tO4tlI0fYaPNG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Bm3tO4tlI0fYaPNG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Bm3tO4tlI0fYaPNG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-Bm3tO4tlI0fYaPNG .blue>*{fill:#DCEBFA!important;stroke:#3678B5!important;color:#183B62!important;}#mermaid-svg-Bm3tO4tlI0fYaPNG .blue span{fill:#DCEBFA!important;stroke:#3678B5!important;color:#183B62!important;}#mermaid-svg-Bm3tO4tlI0fYaPNG .blue tspan{fill:#183B62!important;}#mermaid-svg-Bm3tO4tlI0fYaPNG .gray>*{fill:#F3F5F7!important;stroke:#8A97A5!important;color:#303E4D!important;}#mermaid-svg-Bm3tO4tlI0fYaPNG .gray span{fill:#F3F5F7!important;stroke:#8A97A5!important;color:#303E4D!important;}#mermaid-svg-Bm3tO4tlI0fYaPNG .gray tspan{fill:#303E4D!important;}#mermaid-svg-Bm3tO4tlI0fYaPNG .green>*{fill:#E3F2E8!important;stroke:#41966B!important;color:#20573D!important;}#mermaid-svg-Bm3tO4tlI0fYaPNG .green span{fill:#E3F2E8!important;stroke:#41966B!important;color:#20573D!important;}#mermaid-svg-Bm3tO4tlI0fYaPNG .green tspan{fill:#20573D!important;}#mermaid-svg-Bm3tO4tlI0fYaPNG .orange>*{fill:#FFF1D6!important;stroke:#D99A32!important;color:#754C0A!important;}#mermaid-svg-Bm3tO4tlI0fYaPNG .orange span{fill:#FFF1D6!important;stroke:#D99A32!important;color:#754C0A!important;}#mermaid-svg-Bm3tO4tlI0fYaPNG .orange tspan{fill:#754C0A!important;} 通过
失败
PowerShell 脚本
计算脚本摘要
使用签名私钥生成签名
带有 Authenticode 签名的脚本
签名证书中的公钥
验证签名与脚本完整性
验证结果
签名完整性验证通过
签名无效或内容不匹配

图1:PowerShell Authenticode 数字签名的简化工作原理。

数字签名使用非对称密码技术:

组成部分 主要用途
私钥(Private Key) 生成数字签名,必须严格保护
公钥(Public Key) 验证对应的数字签名
数字证书 将公钥与证书主体信息关联
文件摘要 用于验证文件内容完整性
证书信任链 判断证书能否关联到受信任的信任锚
时间戳 证明签名发生在特定时间

签名之后,PowerShell 脚本末尾通常会增加一个签名注释块。

例如:

text 复制代码
# SIG # Begin signature block
#
# ...签名数据...
#
# SIG # End signature block

PowerShell 不会将这些注释作为普通脚本指令执行。

1.2 数字签名不等于脚本安全

需要明确:

签名只能提供身份与完整性验证能力,不能证明脚本没有漏洞或恶意行为。

例如,一个脚本即使由可信发布者签名,也可能存在危险操作。

因此,执行签名脚本之前,仍然需要进行源码审查、权限检查和必要的测试。


二、先区分四种不同的验证结果

在实际排查中,经常有人把"文件有签名"和"文件可以运行"理解成同一回事。

实际上,至少需要区分四个层面。

验证层面 主要问题
签名存在 文件是否包含 Authenticode 签名?
签名完整性 文件是否在签名后被修改?
证书信任 证书链和发布者是否满足信任要求?
执行授权 PowerShell 执行策略和系统安全策略是否允许执行?

这几个判断不能互相替代。

例如:

某个脚本的签名完整性没有问题,但证书来自不受信任的颁发机构,仍可能无法在要求可信发布者签名的环境中运行。

反过来,即使某个签名证书受到信任,如果文件内容已经被修改,签名完整性验证也可能失败。

2.1 RemoteSigned与AllSigned的区别

先查看当前 PowerShell 的执行策略:

powershell 复制代码
Get-ExecutionPolicy

再查看不同作用域:

powershell 复制代码
Get-ExecutionPolicy -List

两种常见策略的区别如下:

策略 基本要求
RemoteSigned 对被识别为来自 Internet 的脚本通常要求可信的有效签名;本地创建的脚本一般可以不签名
AllSigned 要求脚本及相关配置文件由可信发布者签名,包括本地创建的脚本

需要特别说明:

PowerShell 执行策略是一种纵深防御机制,不是完整安全边界。组织还可能通过 AppLocker、App Control for Business(WDAC)等其他安全机制控制脚本执行。

因此,本文实验不会擅自修改系统执行策略。


三、实验准备:检查系统和证书工具

本实验采用 Windows PowerShell 5.1 或 Windows 上的 PowerShell 7。

首先执行:

powershell 复制代码
$PSVersionTable.PSVersion

Get-ExecutionPolicy -List

检查相关命令是否可用:

powershell 复制代码
Get-Command `
    New-SelfSignedCertificate, `
    Set-AuthenticodeSignature, `
    Get-AuthenticodeSignature, `
    Export-Certificate `
    -ErrorAction SilentlyContinue |
    Select-Object Name, Source

这里使用的主要工具如下:

命令 用途
New-SelfSignedCertificate 创建测试证书
Get-ChildItem Cert: 查看 Windows 证书存储区
Set-AuthenticodeSignature 为脚本添加数字签名
Get-AuthenticodeSignature 检查签名状态
Export-Certificate 导出不含私钥的公钥证书
Import-Certificate 在授权环境中导入证书

其中,创建自签名证书和导入证书主要依赖 Windows PKI 模块。

如果某个命令不存在,应先检查 Windows 版本、模块可用性和当前运行环境,不要立即修改执行策略。

3.1 创建独立实验目录

为了避免影响已有项目,先创建一个临时实验目录:

powershell 复制代码
$labDir = Join-Path $env:TEMP (
    "PS-Signing-Lab-" + (Get-Date -Format "yyyyMMdd-HHmmss")
)

New-Item -ItemType Directory `
    -Path $labDir `
    -Force | Out-Null

$labDir

后续实验均在该目录中进行。

建议在同一个 PowerShell 窗口按顺序执行本文命令,以便复用 $labDir、$cert 等变量。


四、创建一个PowerShell代码签名证书

4.1 什么是代码签名证书?

数字证书可以具有不同用途。

例如:

  • HTTPS 服务器身份认证;
  • 客户端身份认证;
  • 邮件保护;
  • 代码签名。

用于 PowerShell Authenticode 签名的证书,需要具有相应的代码签名用途。

代码签名对应的增强型密钥用法(EKU)OID 是:

text 复制代码
1.3.6.1.5.5.7.3.3

普通 HTTPS 服务器证书不应该被直接视为可用于代码签名的证书。

4.2 创建自签名测试证书

在 Windows PowerShell 中执行:

powershell 复制代码
$cert = New-SelfSignedCertificate `
    -Type CodeSigning `
    -Subject "CN=PowerShell Signing Lab" `
    -CertStoreLocation "Cert:\CurrentUser\My" `
    -KeyAlgorithm RSA `
    -KeyLength 2048 `
    -KeyExportPolicy NonExportable `
    -HashAlgorithm SHA256 `
    -NotAfter (Get-Date).AddMonths(3)

这里采用以下配置:

  • CodeSigning:指定代码签名类型;
  • CurrentUser\My:保存到当前用户的个人证书存储区;
  • RSA 2048:用于本地兼容性实验;
  • SHA256:使用 SHA-256;
  • NonExportable:限制通过常规方式导出私钥;
  • NotAfter:将测试证书有效期设置为三个月。

注意,私钥不可导出并不意味着绝对无法被恶意程序滥用。生产环境仍需要更严格的私钥访问控制。

4.3 查看创建结果

powershell 复制代码
$cert |
    Select-Object `
        Subject,
        Issuer,
        Thumbprint,
        NotBefore,
        NotAfter,
        HasPrivateKey

重点关注:

Subject:证书主体。

Issuer:证书颁发者。

Thumbprint:证书指纹,用于精确识别证书。

NotAfter:证书到期时间。

HasPrivateKey:当前证书对象是否关联私钥。

由于使用了自签名证书,Subject 和 Issuer 通常相同。

可以进一步检查代码签名证书:

powershell 复制代码
Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert |
    Where-Object {
        $_.Thumbprint -eq $cert.Thumbprint
    } |
    Select-Object Subject, Thumbprint, HasPrivateKey

这里通过指纹精确选择本次实验创建的证书。

不要在拥有多个代码签名证书的环境中,直接选取列表中的第一张证书进行签名。


五、创建测试脚本并完成第一次签名

5.1 创建无副作用的测试脚本

本次实验的脚本只输出一行文字。

powershell 复制代码
$scriptPath = Join-Path $labDir "HelloSigned.ps1"

Set-Content `
    -LiteralPath $scriptPath `
    -Encoding ASCII `
    -Value "Write-Output 'SIGN_LAB_OK'"

这里故意采用纯 ASCII 内容。

原因是 Windows PowerShell 5.1 和 PowerShell 7 对文本编码的处理存在差异。使用简单 ASCII 测试脚本,可以先排除中文源文件编码对签名实验的干扰。

正式脚本如果包含中文字符,还需要统一文件编码并验证目标 PowerShell 版本。

5.2 签名前检查状态

powershell 复制代码
Get-AuthenticodeSignature `
    -LiteralPath $scriptPath |
    Select-Object Status, StatusMessage

由于刚创建的文件尚未签名,预期状态是:

text 复制代码
NotSigned

这是预期结果,实际以命令输出为准。

5.3 为脚本添加数字签名

执行:

powershell 复制代码
$signResult = Set-AuthenticodeSignature `
    -LiteralPath $scriptPath `
    -Certificate $cert `
    -HashAlgorithm SHA256

$signResult |
    Select-Object Status, StatusMessage

如果签名操作成功,PowerShell 会将 Authenticode 签名块附加到脚本中。

可以查看脚本末尾:

powershell 复制代码
Get-Content -LiteralPath $scriptPath -Tail 8

注意:

签名操作返回的状态不仅受到脚本内容影响,还可能受到证书链和本机信任配置影响。

因此,在尚未建立对自签名证书的信任时,不应直接假定状态一定是 Valid。

如果返回 NotTrusted,需要区分签名是否已经写入文件,以及证书是否未被系统信任。

5.4 重新验证签名

powershell 复制代码
Get-AuthenticodeSignature `
    -LiteralPath $scriptPath |
    Format-List `
        Status,
        StatusMessage,
        SignerCertificate,
        TimeStamperCertificate

这里能够帮助我们回答:

  1. 当前文件是否存在签名?
  2. 使用的是哪张签名证书?
  3. 系统报告的验证状态是什么?
  4. 是否存在时间戳证书信息?

需要特别注意,Status = Valid 也不代表脚本内容经过安全审查,更不应替代对发布者身份和证书信任政策的检查。


六、理解证书存储区与信任链

这部分是整个实验中最容易产生误解的地方。

6.1 Windows证书存储区有哪些?

可以通过:

powershell 复制代码
Get-ChildItem Cert:\CurrentUser

查看当前用户的证书存储区。

本文重点涉及三个位置:

证书存储区 主要用途
CurrentUser\My 保存当前用户的个人证书及关联私钥
CurrentUser\Root 保存当前用户信任的根证书
CurrentUser\TrustedPublisher 保存受信任的软件发布者证书

还存在对应的 LocalMachine 计算机级证书存储区。

两者作用范围不同,不应随意混用。

6.2 什么是证书信任链?

正式的代码签名证书通常由 CA 签发。

一个简化的证书链如下:
#mermaid-svg-IIsMAobxRqf93qhI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IIsMAobxRqf93qhI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IIsMAobxRqf93qhI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IIsMAobxRqf93qhI .error-icon{fill:#552222;}#mermaid-svg-IIsMAobxRqf93qhI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IIsMAobxRqf93qhI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IIsMAobxRqf93qhI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IIsMAobxRqf93qhI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IIsMAobxRqf93qhI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IIsMAobxRqf93qhI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IIsMAobxRqf93qhI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IIsMAobxRqf93qhI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IIsMAobxRqf93qhI .marker.cross{stroke:#333333;}#mermaid-svg-IIsMAobxRqf93qhI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IIsMAobxRqf93qhI p{margin:0;}#mermaid-svg-IIsMAobxRqf93qhI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IIsMAobxRqf93qhI .cluster-label text{fill:#333;}#mermaid-svg-IIsMAobxRqf93qhI .cluster-label span{color:#333;}#mermaid-svg-IIsMAobxRqf93qhI .cluster-label span p{background-color:transparent;}#mermaid-svg-IIsMAobxRqf93qhI .label text,#mermaid-svg-IIsMAobxRqf93qhI span{fill:#333;color:#333;}#mermaid-svg-IIsMAobxRqf93qhI .node rect,#mermaid-svg-IIsMAobxRqf93qhI .node circle,#mermaid-svg-IIsMAobxRqf93qhI .node ellipse,#mermaid-svg-IIsMAobxRqf93qhI .node polygon,#mermaid-svg-IIsMAobxRqf93qhI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IIsMAobxRqf93qhI .rough-node .label text,#mermaid-svg-IIsMAobxRqf93qhI .node .label text,#mermaid-svg-IIsMAobxRqf93qhI .image-shape .label,#mermaid-svg-IIsMAobxRqf93qhI .icon-shape .label{text-anchor:middle;}#mermaid-svg-IIsMAobxRqf93qhI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IIsMAobxRqf93qhI .rough-node .label,#mermaid-svg-IIsMAobxRqf93qhI .node .label,#mermaid-svg-IIsMAobxRqf93qhI .image-shape .label,#mermaid-svg-IIsMAobxRqf93qhI .icon-shape .label{text-align:center;}#mermaid-svg-IIsMAobxRqf93qhI .node.clickable{cursor:pointer;}#mermaid-svg-IIsMAobxRqf93qhI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IIsMAobxRqf93qhI .arrowheadPath{fill:#333333;}#mermaid-svg-IIsMAobxRqf93qhI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IIsMAobxRqf93qhI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IIsMAobxRqf93qhI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IIsMAobxRqf93qhI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IIsMAobxRqf93qhI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IIsMAobxRqf93qhI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IIsMAobxRqf93qhI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IIsMAobxRqf93qhI .cluster text{fill:#333;}#mermaid-svg-IIsMAobxRqf93qhI .cluster span{color:#333;}#mermaid-svg-IIsMAobxRqf93qhI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IIsMAobxRqf93qhI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IIsMAobxRqf93qhI rect.text{fill:none;stroke-width:0;}#mermaid-svg-IIsMAobxRqf93qhI .icon-shape,#mermaid-svg-IIsMAobxRqf93qhI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IIsMAobxRqf93qhI .icon-shape p,#mermaid-svg-IIsMAobxRqf93qhI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IIsMAobxRqf93qhI .icon-shape .label rect,#mermaid-svg-IIsMAobxRqf93qhI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IIsMAobxRqf93qhI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IIsMAobxRqf93qhI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IIsMAobxRqf93qhI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-IIsMAobxRqf93qhI .blue>*{fill:#DCEBFA!important;stroke:#3678B5!important;color:#183B62!important;}#mermaid-svg-IIsMAobxRqf93qhI .blue span{fill:#DCEBFA!important;stroke:#3678B5!important;color:#183B62!important;}#mermaid-svg-IIsMAobxRqf93qhI .blue tspan{fill:#183B62!important;}#mermaid-svg-IIsMAobxRqf93qhI .gray>*{fill:#F3F5F7!important;stroke:#8A97A5!important;color:#303E4D!important;}#mermaid-svg-IIsMAobxRqf93qhI .gray span{fill:#F3F5F7!important;stroke:#8A97A5!important;color:#303E4D!important;}#mermaid-svg-IIsMAobxRqf93qhI .gray tspan{fill:#303E4D!important;}#mermaid-svg-IIsMAobxRqf93qhI .green>*{fill:#E3F2E8!important;stroke:#41966B!important;color:#20573D!important;}#mermaid-svg-IIsMAobxRqf93qhI .green span{fill:#E3F2E8!important;stroke:#41966B!important;color:#20573D!important;}#mermaid-svg-IIsMAobxRqf93qhI .green tspan{fill:#20573D!important;}#mermaid-svg-IIsMAobxRqf93qhI .orange>*{fill:#FFF1D6!important;stroke:#D99A32!important;color:#754C0A!important;}#mermaid-svg-IIsMAobxRqf93qhI .orange span{fill:#FFF1D6!important;stroke:#D99A32!important;color:#754C0A!important;}#mermaid-svg-IIsMAobxRqf93qhI .orange tspan{fill:#754C0A!important;} 是
否
代码签名证书

签署 PowerShell 脚本
中间证书颁发机构

Intermediate CA
根证书颁发机构

Root CA
系统信任存储区
验证证书链
是否满足信任要求?
进一步检查发布者与执行策略
证书链验证失败

图2:代码签名证书信任链的简化验证流程。

在实际系统中,证书链验证还可能涉及:

  • 证书有效期;
  • 证书用途;
  • 信任锚;
  • 证书撤销状态;
  • 证书链构建规则;
  • 系统和组织安全策略。

Trusted Root 和 TrustedPublisher 不是同一个概念。

证书链受到信任,不等于系统已经自动将某个软件发布者批准为可信发布者。

6.3 查看实验自签名证书的信任状态

可以使用 Windows 的 X.509 证书链验证类:

powershell 复制代码
$chain = New-Object `
    System.Security.Cryptography.X509Certificates.X509Chain

$chainOk = $chain.Build($cert)

Write-Host "Chain build result: $chainOk"

$chain.ChainStatus |
    Select-Object Status, StatusInformation

在尚未信任该自签名证书的环境中,通常可能看到类似:

text 复制代码
UntrustedRoot

但实际结果取决于系统已经存在的证书、信任配置和验证环境。

这里也不应为了让结果通过而关闭证书撤销检查。


七、导出公钥证书,并理解如何建立信任

7.1 导出公钥证书

执行:

powershell 复制代码
$cerPath = Join-Path $labDir "PS-Signing-Lab.cer"

Export-Certificate `
    -Cert $cert `
    -FilePath $cerPath |
    Out-Null

该命令导出的是不包含私钥的证书。

可以查看文件:

powershell 复制代码
Get-Item -LiteralPath $cerPath |
    Select-Object FullName, Length

再次强调:

  • .cer 通常用于分发公钥证书;
  • .pfx 可能包含证书及私钥;
  • 私钥不应该随普通 PowerShell 脚本一起公开分发。

本文实验使用不可导出的测试私钥,因此不需要创建 PFX 文件。

7.2 为什么签名证书不一定被信任?

创建自签名证书时,它通常保存在:

text 复制代码
Cert:\CurrentUser\My

但这并不意味着它已经成为受信任的根证书,也不意味着它已经成为受信任的软件发布者。

这就是"自己能签名,但另一台电脑不信任签名"的主要原因之一。

7.3 在隔离测试环境中验证信任变化

这一节会修改当前用户的证书信任存储区,仅适用于你自己管理且明确授权的测试虚拟机或可恢复的实验环境。

不要在日常办公电脑或生产服务器上直接照搬,更不要将来源不明的证书导入受信任根证书存储区。

首先确认准备导入的文件确实是刚才创建的证书,记录其指纹:

powershell 复制代码
$cert.Thumbprint

Get-FileHash `
    -LiteralPath $cerPath `
    -Algorithm SHA256

在独立测试环境中,如果已经确认信任范围和风险,可以分别验证以下两个存储区的作用。

将本次自签名测试证书作为当前用户信任锚:

powershell 复制代码
Import-Certificate `
    -FilePath $cerPath `
    -CertStoreLocation "Cert:\CurrentUser\Root"

将本次证书登记为当前用户受信任发布者:

powershell 复制代码
Import-Certificate `
    -FilePath $cerPath `
    -CertStoreLocation "Cert:\CurrentUser\TrustedPublisher"

这两步不是正式环境的推荐配置方案。

将自签名证书加入 Root 会扩大该用户的证书信任范围,应严格限制在隔离实验中,并在实验结束后撤销。

完成后重新运行:

powershell 复制代码
Get-AuthenticodeSignature `
    -LiteralPath $scriptPath |
    Format-List Status, StatusMessage

然后重新检查证书链:

powershell 复制代码
$chain = New-Object `
    System.Security.Cryptography.X509Certificates.X509Chain

$chainOk = $chain.Build($cert)

Write-Host "Chain build result: $chainOk"

$chain.ChainStatus |
    Select-Object Status, StatusInformation

通过对比导入信任证书之前和之后的结果,就可以理解:

脚本签名本身并没有改变,改变的是验证计算机对证书及发布者的信任状态。

如果仍然不能执行脚本,应继续检查实际执行策略、证书用途以及其他系统安全控制,而不是反复导入证书。


八、实验:修改已签名脚本,签名会发生什么变化?

这是最能体现数字签名完整性保护的实验。

8.1 复制已签名脚本

先复制一份,保留原始签名文件:

powershell 复制代码
$tamperedPath = Join-Path $labDir "HelloSigned-Tampered.ps1"

Copy-Item `
    -LiteralPath $scriptPath `
    -Destination $tamperedPath

先确认副本的签名状态:

powershell 复制代码
Get-AuthenticodeSignature `
    -LiteralPath $tamperedPath |
    Select-Object Status, StatusMessage

8.2 修改脚本内容,但保留原签名块

执行:

powershell 复制代码
$text = [System.IO.File]::ReadAllText(
    $tamperedPath,
    [System.Text.Encoding]::ASCII
)

if (-not $text.Contains("'SIGN_LAB_OK'")) {
    throw "Expected test content not found."
}

$text = $text.Replace(
    "'SIGN_LAB_OK'",
    "'SIGN_LAB_CHANGED'"
)

[System.IO.File]::WriteAllText(
    $tamperedPath,
    $text,
    [System.Text.Encoding]::ASCII
)

这里刻意保留签名注释块,只修改被签署的脚本内容。

随后再次检查:

powershell 复制代码
Get-AuthenticodeSignature `
    -LiteralPath $tamperedPath |
    Format-List Status, StatusMessage

预期可能出现:

text 复制代码
HashMismatch

表示文件内容与签名所记录的摘要不一致。

如果文件在编辑过程中被工具移除了签名块,则也可能表现为 NotSigned,需要检查实际文件内容。

8.3 为什么修改一点内容,签名就可能失效?

因为签名保护的是文件内容的完整性。

例如:

powershell 复制代码
Write-Output 'SIGN_LAB_OK'

修改为:

powershell 复制代码
Write-Output 'SIGN_LAB_CHANGED'

即使只有字符串发生变化,文件摘要也会改变。

这正是数字签名的重要作用。

它不能阻止别人修改文件,但可以让验证者发现文件与签署时的内容不一致。

因此,正式发布 PowerShell 脚本时,应该先完成代码修改、测试和审查,再执行签名。

签名后不要随意修改源码、换行符或编码。


九、为什么需要时间戳?证书过期后会怎样?

代码签名证书通常具有有效期。

例如:

text 复制代码
NotBefore:2026-11-01
NotAfter :2027-11-01

假设开发者在证书有效期内完成了签名。

一年之后证书到期,历史签名是否还能继续验证?

这就涉及时间戳服务。

9.1 时间戳的作用

可信时间戳服务可以为签名提供关于签署时间的密码学证据。

在满足相应信任、算法和撤销状态要求时,即使签名证书后来到期,也可以依据可信时间戳判断签名是否在证书有效期内完成。

需要注意:

时间戳不等于签名证书,也不能解决所有证书撤销和信任问题。

9.2 PowerShell如何使用时间戳?

Set-AuthenticodeSignature 提供:

powershell 复制代码
-TimestampServer

参数。

例如,在已经取得经过验证的组织时间戳服务地址后,可以使用:

powershell 复制代码
$approvedTsaUrl = Read-Host "Approved timestamp server URL"

Set-AuthenticodeSignature `
    -LiteralPath $scriptPath `
    -Certificate $cert `
    -HashAlgorithm SHA256 `
    -TimestampServer $approvedTsaUrl

这里需要满足几个前提:

  • 时间戳服务是可信的;
  • 该服务支持当前签名工具所使用的时间戳协议;
  • 当前 PowerShell 版本支持相应参数和 URL 形式;
  • 证书、网络与组织安全策略允许使用该服务。

本文本地离线实验不需要时间戳,因此前面的基本签名过程没有使用这一参数。

在正式代码签名流程中,建议优先采用组织批准、可长期验证的时间戳服务,并明确工具与协议兼容性。

对于更复杂的 Windows 软件发布,可以进一步研究 Windows SDK 中的 SignTool 及 RFC 3161 时间戳。


十、正式项目如何管理签名证书?

前面的自签名证书实验主要用于理解原理。

真正用于团队开发、内部工具发布或者企业部署时,应该建立正式的证书管理流程。

10.1 测试证书与正式证书的区别

对比 自签名测试证书 正式代码签名证书
主要用途 学习、隔离实验 正式软件发布
信任来源 需要显式建立信任 由受信任的 CA 或组织 PKI 建立
身份验证 自行声明 按签发机构的身份审核流程
私钥管理 本地实验管理 应采用严格访问控制
跨机器使用 默认通常不受信任 取决于接收端信任和策略
时间戳 可选实验 建议纳入正式流程

这里不应认为"购买了正式证书,任何 Windows 电脑就必然允许运行脚本"。

系统还可能受到受信任发布者策略、证书撤销检查和应用控制策略约束。

10.2 私钥应该如何保护?

代码签名私钥非常重要。

一旦私钥泄露,攻击者就可能使用对应私钥生成签名。

因此,不建议:

  • 将私钥或包含私钥的 PFX 文件提交到 Git 仓库;
  • 将 PFX 与密码存放在同一个公开目录;
  • 在普通共享文件夹中放置可导出的生产私钥;
  • 让所有自动化进程使用同一个高权限签名账户;
  • 把测试证书当成长期生产证书。

正式项目可以考虑使用组织 PKI、专用签名服务、受控密钥存储、硬件安全模块或符合相关要求的签名设备。

10.3 推荐的签名发布流程

#mermaid-svg-NxXyI4jm2HMBUhP1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NxXyI4jm2HMBUhP1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NxXyI4jm2HMBUhP1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NxXyI4jm2HMBUhP1 .error-icon{fill:#552222;}#mermaid-svg-NxXyI4jm2HMBUhP1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NxXyI4jm2HMBUhP1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NxXyI4jm2HMBUhP1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NxXyI4jm2HMBUhP1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NxXyI4jm2HMBUhP1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NxXyI4jm2HMBUhP1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NxXyI4jm2HMBUhP1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NxXyI4jm2HMBUhP1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NxXyI4jm2HMBUhP1 .marker.cross{stroke:#333333;}#mermaid-svg-NxXyI4jm2HMBUhP1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NxXyI4jm2HMBUhP1 p{margin:0;}#mermaid-svg-NxXyI4jm2HMBUhP1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NxXyI4jm2HMBUhP1 .cluster-label text{fill:#333;}#mermaid-svg-NxXyI4jm2HMBUhP1 .cluster-label span{color:#333;}#mermaid-svg-NxXyI4jm2HMBUhP1 .cluster-label span p{background-color:transparent;}#mermaid-svg-NxXyI4jm2HMBUhP1 .label text,#mermaid-svg-NxXyI4jm2HMBUhP1 span{fill:#333;color:#333;}#mermaid-svg-NxXyI4jm2HMBUhP1 .node rect,#mermaid-svg-NxXyI4jm2HMBUhP1 .node circle,#mermaid-svg-NxXyI4jm2HMBUhP1 .node ellipse,#mermaid-svg-NxXyI4jm2HMBUhP1 .node polygon,#mermaid-svg-NxXyI4jm2HMBUhP1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NxXyI4jm2HMBUhP1 .rough-node .label text,#mermaid-svg-NxXyI4jm2HMBUhP1 .node .label text,#mermaid-svg-NxXyI4jm2HMBUhP1 .image-shape .label,#mermaid-svg-NxXyI4jm2HMBUhP1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-NxXyI4jm2HMBUhP1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NxXyI4jm2HMBUhP1 .rough-node .label,#mermaid-svg-NxXyI4jm2HMBUhP1 .node .label,#mermaid-svg-NxXyI4jm2HMBUhP1 .image-shape .label,#mermaid-svg-NxXyI4jm2HMBUhP1 .icon-shape .label{text-align:center;}#mermaid-svg-NxXyI4jm2HMBUhP1 .node.clickable{cursor:pointer;}#mermaid-svg-NxXyI4jm2HMBUhP1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NxXyI4jm2HMBUhP1 .arrowheadPath{fill:#333333;}#mermaid-svg-NxXyI4jm2HMBUhP1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NxXyI4jm2HMBUhP1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NxXyI4jm2HMBUhP1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NxXyI4jm2HMBUhP1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NxXyI4jm2HMBUhP1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NxXyI4jm2HMBUhP1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NxXyI4jm2HMBUhP1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NxXyI4jm2HMBUhP1 .cluster text{fill:#333;}#mermaid-svg-NxXyI4jm2HMBUhP1 .cluster span{color:#333;}#mermaid-svg-NxXyI4jm2HMBUhP1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NxXyI4jm2HMBUhP1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NxXyI4jm2HMBUhP1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-NxXyI4jm2HMBUhP1 .icon-shape,#mermaid-svg-NxXyI4jm2HMBUhP1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NxXyI4jm2HMBUhP1 .icon-shape p,#mermaid-svg-NxXyI4jm2HMBUhP1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NxXyI4jm2HMBUhP1 .icon-shape .label rect,#mermaid-svg-NxXyI4jm2HMBUhP1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NxXyI4jm2HMBUhP1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NxXyI4jm2HMBUhP1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NxXyI4jm2HMBUhP1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-NxXyI4jm2HMBUhP1 .blue>*{fill:#DCEBFA!important;stroke:#3678B5!important;color:#183B62!important;}#mermaid-svg-NxXyI4jm2HMBUhP1 .blue span{fill:#DCEBFA!important;stroke:#3678B5!important;color:#183B62!important;}#mermaid-svg-NxXyI4jm2HMBUhP1 .blue tspan{fill:#183B62!important;}#mermaid-svg-NxXyI4jm2HMBUhP1 .gray>*{fill:#F3F5F7!important;stroke:#8A97A5!important;color:#303E4D!important;}#mermaid-svg-NxXyI4jm2HMBUhP1 .gray span{fill:#F3F5F7!important;stroke:#8A97A5!important;color:#303E4D!important;}#mermaid-svg-NxXyI4jm2HMBUhP1 .gray tspan{fill:#303E4D!important;}#mermaid-svg-NxXyI4jm2HMBUhP1 .green>*{fill:#E3F2E8!important;stroke:#41966B!important;color:#20573D!important;}#mermaid-svg-NxXyI4jm2HMBUhP1 .green span{fill:#E3F2E8!important;stroke:#41966B!important;color:#20573D!important;}#mermaid-svg-NxXyI4jm2HMBUhP1 .green tspan{fill:#20573D!important;}#mermaid-svg-NxXyI4jm2HMBUhP1 .orange>*{fill:#FFF1D6!important;stroke:#D99A32!important;color:#754C0A!important;}#mermaid-svg-NxXyI4jm2HMBUhP1 .orange span{fill:#FFF1D6!important;stroke:#D99A32!important;color:#754C0A!important;}#mermaid-svg-NxXyI4jm2HMBUhP1 .orange tspan{fill:#754C0A!important;} 是
否
开发 PowerShell 脚本
代码审查
自动化测试
确定发布版本
受控签名
添加可信时间戳
独立验证签名
验证通过?
发布脚本与版本信息
停止发布并调查

图3:PowerShell 脚本从开发到签名发布的推荐流程。

这里有一个容易忽略的问题:

不要在签名之后,再通过自动格式化工具、编码转换工具或构建脚本修改已签署文件。

否则可能破坏签名。

如果需要修改文件,应重新进行审查、测试和签名。


十一、常见签名错误与排查方法

11.1 NotSigned:脚本没有签名

检查:

powershell 复制代码
Get-AuthenticodeSignature `
    -LiteralPath $scriptPath |
    Select-Object Status, StatusMessage

如果是 NotSigned,首先确认是否确实完成了签名,以及文件是否被重新生成或覆盖。

11.2 NotTrusted:发布者或证书不受信任

这时不能只重复执行签名操作。

应检查:

  • 签名证书的指纹;
  • 证书链;
  • 受信任根证书;
  • 受信任发布者;
  • 证书有效期和用途;
  • 组织安全策略。

11.3 HashMismatch:脚本内容不匹配

可能原因包括:

  • 签名后修改了代码;
  • 自动格式化;
  • 换行符转换;
  • 文本编码改变;
  • 文件在分发中被修改。

如果文件包含中文或其他非 ASCII 字符,还应特别关注 Windows PowerShell 5.1 与其他 PowerShell 版本之间的编码兼容性。

对于跨区域、跨机器使用的旧版 Windows PowerShell 签名脚本,微软也记录过由编码和系统区域设置引发的哈希不匹配问题。

11.4 签名有效,但脚本仍然提示PSSecurityException

这种情况尤其值得注意。

签名验证只是脚本执行条件的一部分。

还需要检查:

powershell 复制代码
Get-ExecutionPolicy -List

如果脚本来自 NAS 或 UNC 网络共享路径,还应该检查:

  • UNC 路径的安全区域判断;
  • 文件来源标记;
  • SMB 共享权限;
  • NTFS ACL;
  • 是否存在组织应用控制策略。

因此,PSSecurityException 不一定意味着签名本身无效。

这也是为什么前面的 UNC 脚本排查文章需要单独研究执行策略、文件来源和访问权限。


十二、实验结束后如何清理证书?

如果只完成创建证书和签名的基础实验,通常只需要处理实验目录和本次测试证书。

如果在第七节的隔离测试环境中导入了证书信任,还必须清理相应信任记录。

首先记录本次实验的证书指纹:

powershell 复制代码
$thumbprint = $cert.Thumbprint

$thumbprint

然后检查三个存储区:

powershell 复制代码
$stores = @(
    "My",
    "Root",
    "TrustedPublisher"
)

foreach ($store in $stores) {

    $entry = "Cert:\CurrentUser\$store\$thumbprint"

    if (Test-Path -LiteralPath $entry) {
        Get-Item -LiteralPath $entry |
            Select-Object Subject, Thumbprint
    }
}

确认每一项确实属于本次实验,并且已经不再需要后,才可以在对应存储区删除。

例如,对本次实验导入的受信任发布者记录:

powershell 复制代码
$entry = "Cert:\CurrentUser\TrustedPublisher\$thumbprint"

if (Test-Path -LiteralPath $entry) {
    Remove-Item -LiteralPath $entry -Confirm
}

同样可以针对 CurrentUser\Root 中的本次测试证书进行清理。

最后再处理 CurrentUser\My 中的测试证书及其私钥关联。

不要使用通配符批量删除证书存储区内容。也不要删除任何不能确认属于本次实验的证书。

如果使用了测试虚拟机快照,恢复到实验前的快照通常是更容易审计和控制的清理方式。

实验脚本文件可以在确认不再需要后单独删除。


十三、完整实验验收表

建议在 Windows 上完成以下检查,并保存实际输出。

实验项目 预期结果
创建代码签名证书 成功获得证书对象
检查证书私钥 HasPrivateKey = True
创建测试脚本 文件存在
签名前检查 NotSigned
执行代码签名 脚本出现 Authenticode 签名块
检查签名状态 返回实际验证状态
验证证书链 可以观察链构建结果
隔离环境中建立信任 验证结果按预期变化
修改已签名脚本 签名完整性验证失败
清理实验信任 测试证书不再作为信任锚
清理实验目录 不影响其他项目

这里不应该强行把所有检查都写成 PASS。

例如,基础实验中自签名证书未被信任,返回 NotTrusted 可能正是希望观察到的结果。

真正重要的是理解结果发生的原因,而不是简单地让所有状态都显示为 Valid。


十四、总结

PowerShell 脚本数字签名涉及三个关键问题:

第一,脚本内容是否完整。

数字签名可以帮助验证脚本在签署之后是否被修改。

第二,签名者身份是否受到信任。

数字签名需要结合证书、私钥、公钥及信任链判断。自签名证书可以用于实验,但不应该直接取代正式 PKI 体系。

第三,系统是否允许脚本执行。

即使签名完整且证书受到信任,仍然可能受到执行策略、文件权限或其他安全策略限制。

因此,排查 PowerShell 脚本执行异常时,应该按照以下顺序思考:

检查签名 → 验证完整性 → 检查证书用途与信任链 → 检查发布者信任 → 检查执行策略和其他安全控制。

本篇通过创建自签名证书、签署测试脚本、验证证书链和修改已签名文件,建立了对 PowerShell 代码签名机制的完整理解。

对于企业内部脚本、Windows 自动化工具、Codex 辅助开发脚本和本地 AI Agent 运维脚本,建立可审计的签名与发布流程,比单纯修改执行策略更可靠。


参考资料

  1. Microsoft:about_Signing

    https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_signing

  2. Microsoft:New-SelfSignedCertificate

    https://learn.microsoft.com/powershell/module/pki/new-selfsignedcertificate

  3. Microsoft:Set-AuthenticodeSignature

    https://learn.microsoft.com/powershell/module/microsoft.powershell.security/set-authenticodesignature

  4. Microsoft:Get-AuthenticodeSignature

    https://learn.microsoft.com/powershell/module/microsoft.powershell.security/get-authenticodesignature

  5. Microsoft:about_Execution_Policies

    https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_execution_policies

  6. Microsoft:Import-Certificate

    https://learn.microsoft.com/powershell/module/pki/import-certificate

  7. Microsoft:PowerShell 签名脚本 HashMismatch 故障

    https://learn.microsoft.com/troubleshoot/windows-client/system-management-components/signed-powershell-script-fails-hash-mismatch

  8. Microsoft:Time Stamping Authenticode Signatures

    https://learn.microsoft.com/windows/win32/seccrypto/time-stamping-authenticode-signatures


本文属于《Windows 开发环境与 PowerShell 实战》系列。

上一篇介绍了 PowerShell 5.1 与 PowerShell 7 的安装共存、PATH、Profile 和 UTF-8 编码配置。

本篇重点介绍脚本数字签名机制。后续将继续讨论 RemoteSigned、PSSecurityException 与 UNC 网络共享脚本执行问题。

相关推荐
goehou6 小时前
让 AI 替我操作这台电脑,先把手能伸多远定下来
windows·自动化·权限管理·ai agent
公子小六7 小时前
基于.NET的Windows窗体编程之WinForms数据持久化(1)
windows·microsoft·c#·.net·winforms
2601_967987371 天前
安装 Redis 或 Redis Stack
linux·windows·redis·macos·安装
xbzb1 天前
自定义 yum 软件源:Linux 上 createrepo,Windows 上纯 Python 也能造
linux·运维·windows·python·yum·createrepo
武雄(小星Ai)1 天前
Ollama报llama-server binary not found排坑实录:服务活着引擎没了
人工智能·windows·大模型·ollama·排坑
叼烟扛炮2 天前
[LangChain] 学习笔记(06):突破大模型限制——结构化输出(Structured Output)
windows·笔记·学习·langchain
Thneonl2 天前
桌宠穿透不是一行 SetWindowLong
windows·程序员
艺杯羹3 天前
Windows原生软件锁设计:从暴力杀进程丢数据到零轮询事件驱动架构
windows·系统架构·开源·开源软件·内存管理·开源协议
csdn_aspnet3 天前
Redis 入门:Windows、Mac安装指南
数据库·windows·redis·缓存·mac