Windows 服务证书与私钥配置
Windows 服务证书与私钥配置
服务报“找不到证书”和“无法使用私钥”时,处理的位置不同。先确认服务实际身份和打开的证书存储,再确认它能否用关联私钥完成一次操作。管理员在证书管理器里看到了证书,不能代替这两步。
示例环境为 Windows Server 2022 Datacenter、PowerShell 5.1.20348.5386。先在 SYSTEM 下比较 CurrentUser/My 与 LocalMachine/My 的查找,再用另一张计算机证书检查普通账户的私钥权限。存储位置与密钥授权是两项独立检查。
1. 定位失败阶段
| 结果 | 先查什么 |
|---|---|
| 指定存储 Found=0 | 实际身份、StoreLocation/StoreName、指纹及筛选条件 |
| Found=1,HasPrivateKey=False | 是否仅导入公钥,应用需要的私钥是否已正确部署 |
| HasPrivateKey=True,打开/签名失败 | 私钥关联、提供程序、密钥实际存在、授权及运行上下文 |
| 本地签名验签成功,TLS 仍失败 | EKU、有效期、证书链、远端信任、协议与应用配置 |
HasPrivateKey 是关联信息,不是“任意身份已经成功使用该私钥”的证明。反过来,一次 Invalid provider type specified 也不能只按字面就重建密钥,必须结合具体提供程序和对照结果。
2. 证书存储
存储查找同时受位置与名称约束。应用显式打开 CurrentUser/My,不会自动到 LocalMachine/My 补搜;CurrentUser 也跟随应用实际身份,SYSTEM 看到的用户存储不是登录管理员的用户存储。
第一组使用短期 RSA 2048 自签名证书,指纹为 2A44F88401A4001547AD6B8F834E40FB01C8446A。同一个本地签名函数得到:
| 阶段 | 打开的存储 | Found | HasPrivateKey | 签名/本地验签 |
|---|---|---|---|---|
| 基线 | SYSTEM 的 CurrentUser/My | 1 | True | 256 字节 / True |
| 故障 | LocalMachine/My | 0 | — | CertificateNotFound |
| 对照 | 原 CurrentUser/My | 1 | True | 256 字节 / True |
| 加入机器存储后 | LocalMachine/My | 1 | True | 256 字节 / True |
函数里的 Accepted=True 只是固定负载的本地签名、公钥验签通过,不是真正 HTTP 请求已被远端接受,也不是 TLS 客户端认证实测。证书没有损坏,查找范围才是当次变化。
在服务本身的上下文取身份与候选证书,避免拿部署账户的列表解释运行账户:
[Security.Principal.WindowsIdentity]::GetCurrent().Name
[Security.Principal.WindowsIdentity]::GetCurrent().User.Value
Get-ChildItem Cert:\CurrentUser\My,Cert:\LocalMachine\My |
Select-Object PSParentPath,Subject,Thumbprint,HasPrivateKey,NotAfter服务可以使用按其身份管理的用户存储,也可以使用计算机存储,取决于应用支持的配置和部署模式;不能强制所有服务都必须 LocalMachine,或所有交互应用都必须 CurrentUser。LocalMachine 方便机器级部署,但并不自动给所有服务身份私钥权限。
3. PFX 部署
原实验直接把带关联的证书对象 Add 到 LocalMachine/My,SYSTEM 随后仍能解析关联密钥并签名。这是同机、同 SYSTEM 环境下观察到的行为,不证明 X509Store.Add 会通用地把用户私钥迁移为机器私钥,更不证明复制一个 .cer 就携带私钥。
真正部署 PFX 时用 Import-PfxCertificate,公钥证书文件用 Import-Certificate。部署 PFX 前,先确认应用确实配置读取 LocalMachine/My,PFX 来自正确来源,目标已有证书和关联密钥有备份:
$pfx = Read-Host 'PFX file path'
$password = Read-Host 'PFX password' -AsSecureString
$imported = Import-PfxCertificate -FilePath $pfx `
-CertStoreLocation 'Cert:\LocalMachine\My' -Password $password
$imported | Select-Object Subject,Thumbprint,HasPrivateKey,NotAfter
Remove-Variable password不要默认加 Exportable,也不要把 PFX 密码写进命令行。证书导入、密钥持久化与可导出策略是部署选择,随后还要按应用身份验证。硬件密钥可能根本不适合 PFX 文件迁移。Import-PfxCertificate
4. 私钥打开失败
第二组证书位于 LocalMachine/My,指纹为 931116F267D3BE2C676C7EEF07759DDE1A3EC0DE,使用 Microsoft Software Key Storage Provider。普通本地账户 KBEW06Signer 的对照如下:
| 阶段 | 目标私钥文件授权 | 证书/关联 | 业务结果 |
|---|---|---|---|
| 基线 | 有该用户 Read Allow | 找到,HasPrivateKey=True | 签名 256 字节 |
| 故障 | 仅移除该用户 Allow ACE | 仍找到,仍 True | GetRSAPrivateKey 失败 |
| 修复 | 恢复此前 ACL | 相同指纹、相同身份 | 签名 256 字节,验签 True |
故障文本为:
Exception calling "GetRSAPrivateKey" with "1" argument(s): "Invalid provider type specified.
"SYSTEM 与 Administrators 的管理权限仍在,管理员可以操作同一密钥。恢复后用公钥验证基线和新签名均为 True。这组对照把故障定位到该普通账户的私钥访问路径,而不是证书链或签名算法。
任务外层 LastTaskResult 当时仍为 0,因为脚本捕获了签名异常、记录结果后正常退出。业务脚本必须把失败传给调度器或监控;“任务成功”不能代替签名成功。
5. 定位密钥文件
微软软件提供程序下,计算机共享 CNG 密钥与传统 CSP MachineKeys 路径不同。第三方、智能卡或硬件提供程序应使用其支持的授权机制,不能在文件系统上猜一个私钥路径。CNG 存储模型
在管理员 Windows PowerShell 中由证书关联取得准确的提供程序与 UniqueName,再读单个文件 ACL:
$ErrorActionPreference='Stop'
$Thumbprint='931116F267D3BE2C676C7EEF07759DDE1A3EC0DE'
$cert=Get-Item "Cert:\LocalMachine\My\$Thumbprint"
$cert|Select-Object Subject,Thumbprint,HasPrivateKey,NotAfter
$rsa=[Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
try {
if ($rsa -isnot [Security.Cryptography.RSACng]) { throw 'Expected an RSA CNG key' }
$key=$rsa.Key
if (-not $key.IsMachineKey) { throw 'Expected a machine key' }
if ($key.Provider.Provider -ne 'Microsoft Software Key Storage Provider') {
throw 'Use the actual provider management tool'
}
$KeyPath=Join-Path "$env:ProgramData\Microsoft\Crypto\Keys" $key.UniqueName
if (-not (Test-Path -LiteralPath $KeyPath -PathType Leaf)) { throw 'Key file missing' }
[pscustomobject]@{Provider=$key.Provider.Provider;UniqueName=$key.UniqueName;KeyPath=$KeyPath}
Get-Acl -LiteralPath $KeyPath|Select-Object Owner,Sddl,Access
} finally {
if ($rsa) { $rsa.Dispose() }
}代码明确要求机器级 RSACng 和 Microsoft Software Key Storage Provider;其他类型应停止,改用相应机制。不要按目录里最新修改时间猜密钥,也不要在用户私钥、RSA/CNG 路径间盲目套用。管理员也打不开时,继续检查公钥/私钥部署、关联、提供程序及密钥存在性,而不是先追加普通账户权限。
6. 配置密钥权限
应用日志要记录真实身份。服务、任务、IIS 应用池和管理员交互窗口可能都不一样;需要授权的是签名进程使用的身份。
下面沿用已确认的 KeyPath,为本地测试账户增加 Read,并备份 DACL。它不会重置父目录或其他密钥:
$Account="$env:COMPUTERNAME\KBEW06Signer"
$BackupFile=Join-Path $env:TEMP ('private-key-acl-'+[guid]::NewGuid().ToString('N')+'.sddl')
$identity=New-Object Security.Principal.NTAccount $Account
$sid=$identity.Translate([Security.Principal.SecurityIdentifier])
$acl=Get-Acl -LiteralPath $KeyPath
$sections=[Security.AccessControl.AccessControlSections]::Access
$before=$acl.GetSecurityDescriptorSddlForm($sections)
if (Test-Path -LiteralPath $BackupFile) { throw 'Backup already exists' }
$before|Set-Content -LiteralPath $BackupFile -Encoding ASCII
$rule=New-Object Security.AccessControl.FileSystemAccessRule $sid,'Read','Allow'
$acl.AddAccessRule($rule)
Set-Acl -LiteralPath $KeyPath -AclObject $acl
Get-Acl -LiteralPath $KeyPath|Select-Object Sddl,Access
$BackupFile备份应转移到仅管理员可改的管理位置。执行前检查相同 ACE 是否已经存在,以及是否有该账户/组的 Deny;已有授权不属于本次新增项。Read 使该身份能够使用关联密钥,必须对应确实允许签名的业务身份,不能为了绕过错误授 Everyone 或把账号加入 Administrators。
证书续期可能使用另一把私钥。部署流程应在新证书、新指纹、新密钥上重新确认授权,不能凭旧证书曾经工作推断续期完成。
7. 签名与验签
避免沿用先前打开的私钥句柄。用原服务/任务身份重跑下面相同指纹与固定负载的测试:
$ErrorActionPreference='Stop'
$Thumbprint='931116F267D3BE2C676C7EEF07759DDE1A3EC0DE'
$cert=Get-Item "Cert:\LocalMachine\My\$Thumbprint"
$rsa=$null
$pub=$null
try {
$rsa=[Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
if (-not $rsa) { throw 'RSA private key unavailable' }
$data=[Text.Encoding]::UTF8.GetBytes('KBEW06 payload')
$hash=[Security.Cryptography.HashAlgorithmName]::SHA256
$padding=[Security.Cryptography.RSASignaturePadding]::Pkcs1
$sig=$rsa.SignData($data,$hash,$padding)
$pub=[Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPublicKey($cert)
$verified=$pub.VerifyData($data,$sig,$hash,$padding)
if (-not $verified) { throw 'Signature verification failed' }
[pscustomobject]@{
Identity=[Security.Principal.WindowsIdentity]::GetCurrent().Name
Thumbprint=$cert.Thumbprint
HasPrivateKey=$cert.HasPrivateKey
SignatureBytes=$sig.Length
Verified=$verified
}
} finally {
if ($rsa) { $rsa.Dispose() }
if ($pub) { $pub.Dispose() }
}本例修复后 SignatureBytes=256、Verified=True。长度来自 RSA 2048,不能用它当所有算法的固定判据。此代码只覆盖 RSA/SHA256/PKCS#1 v1.5;ECDSA 等要用相应 API。
若实际场景是 TLS 客户端认证,还要重试原请求,核对应用选中的证书、远端接受结果和链验证。签名能做出来不代表服务端信任这张证书;根证书导入、关闭证书验证和扩大私钥 ACL 是不同层面的动作,不能互相替代。
8. 恢复授权
在没有并发变更、确认仍为同一个密钥文件的情况下,可恢复已保存 DACL:
$acl=Get-Acl -LiteralPath $KeyPath
$saved=(Get-Content -LiteralPath $BackupFile -Raw).Trim()
$acl.SetSecurityDescriptorSddlForm($saved,[Security.AccessControl.AccessControlSections]::Access)
Set-Acl -LiteralPath $KeyPath -AclObject $acl
$restored=(Get-Acl -LiteralPath $KeyPath).GetSecurityDescriptorSddlForm([Security.AccessControl.AccessControlSections]::Access)
if ($restored -ne $saved) { throw 'DACL restoration mismatch' }
$restored这会撤销快照之后对这个密钥 DACL 的修改,不能覆盖别人新增的授权。Windows 可能规范化安全描述符,原实验初次恢复曾因 CREATOR OWNER 处理导致字符串不一致;遇到 mismatch 保留备份,比较 SID、Allow/Deny、权限、继承与有效访问,不要反复整份重置。
测试证书按准确指纹清理;只有本次创建且确认没有其他用途的关联密钥才删除。一个密钥可能被多份证书关联,不能因为清掉一个证书副本就随意 DeleteKey。业务故障修复通常只改正确存储或授权,保留生产证书及历史证据。
