如何从pfx / cer创建snk?

43

微软似乎创造了一个难以理解的认证丛林。

  • Microsoft X.509 证书 (.cer)
  • 个人信息交换格式 (.pfx)
  • 程序集签名密钥属性 (.snk)

    1. 是否建议基于pfx或cer创建snk文件? (不确定是否可行,如果可行,如何操作?)

    2. 虽然程序集可以用有密码保护的pfx进行签名,但似乎不是强名称,除非使用snk进行签名。但snk没有密码保护。哪种方法更安全? 由于我是该项目中唯一的开发人员,所以没有多开发人员安全性问题,但仍想知道哪种方式最好。

非常感谢。

3个回答

43

关于您提到的文件类型的澄清:

  • .cer文件是X.509证书。
  • .pfx文件是使用基于密码的对称密钥加密的X.509证书,也可以参见PKCS#12(维基百科)
  • .snk文件仅包含RSA密钥(公钥/私钥或仅公钥)。

无论您使用.pfx文件还是.snk文件对程序集进行签名,它都将得到强名称。 使用加密证书(.pfx)存储RSA密钥当然比仅存储未加密密钥(.snk)更安全。

您可以使用类System.Security.Cryptography.X509Certificates.X509Certificate2在代码中轻松从这些文件中提取密钥。

.pfx文件中提取密钥:

/// <summary>
/// Converts .pfx file to .snk file.
/// </summary>
/// <param name="pfxData">.pfx file data.</param>
/// <param name="pfxPassword">.pfx file password.</param>
/// <returns>.snk file data.</returns>
public static byte[] Pfx2Snk(byte[] pfxData, string pfxPassword)
{
    // load .pfx
    var cert = new X509Certificate2(pfxData, pfxPassword, X509KeyStorageFlags.Exportable);

    // create .snk
    var privateKey = (RSACryptoServiceProvider)cert.PrivateKey;
    return privateKey.ExportCspBlob(true);
}

使用 privateKey.ExportCspBlob(false) 提取公钥! (例如用于程序集的延迟签名)


我已经在Github上发布了一个基于命令行的工具的源代码形式(https://github.com/aarnott/pfx2Snk)。我只是出于兴趣用VB完成了它。 - Andrew Arnott
当我尝试在VS2013中使用时,它对我不起作用,出现了加密错误。 - Aaron Stainback
编辑队列已满,否则我会添加上面的内容: 使用 System.Security.Cryptography.X509Certificates; 使用 System.Security.Cryptography; - Denise Skidmore

33

从 pfx 文件中仅生成公钥的 snk 文件:

sn -p keypair.pfx key.snk

我一直喜欢使用snk文件而不是.pfx文件,因为它们似乎更少出错。


5
这让我在VS2010中遇到了错误:密钥文件'key.snk'不包含公钥/私钥对。 - Julien
你正在处理的pfk可能是不完整的,我在使用Verisign提供的.pfk、.cer和.pfx文件时遇到了这个问题,它们是分开提供的。 - punkcoder
16
-p参数只会在snk文件中存储证书的公共部分。由于您没有私钥,因此无法使用生成的snk进行签名,除非进行延迟签名,这不会改变最终结果。 - Mathieu Chateau
2
你可以使用 sn -k keypair.snk 来生成公钥和私钥。我不知道它从哪里获取私钥,但至少对于我的目的来说它是有效的。(对于挖坟这个帖子我很抱歉) - Slampisko
2
@Slampisko FYI,从MSDN上可以看到,选项-k "生成新的...密钥"`。这就是为什么你不需要引用现有的.pfx文件。 - p.s.w.g

22

这里是同样由@Sir Kill A Lot 在他的答案中提供的方法,但已转换为PowerShell脚本(pfx2snk.ps1)。

Param(
    [Parameter(Mandatory=$True,Position=1)]
    [string] $pfxFilePath,
    [string] $pfxPassword
)

# The path to the snk file we're creating
[string] $snkFilePath = [IO.Path]::GetFileNameWithoutExtension($pfxFilePath) + ".snk";

# Read in the bytes of the pfx file
[byte[]] $pfxBytes = Get-Content $pfxFilePath -Encoding Byte;

# Get a cert object from the pfx bytes with the private key marked as exportable
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(
    $pfxBytes,
    $pfxPassword,
    [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable);

# Export a CSP blob from the cert (which is the same format as an SNK file)
[byte[]] $snkBytes = ([Security.Cryptography.RSACryptoServiceProvider]$cert.PrivateKey).ExportCspBlob($true);

# Write the CSP blob/SNK bytes to the snk file
[IO.File]::WriteAllBytes($snkFilePath, $snkBytes);

只需运行该脚本,提供pfx文件路径和密码即可,在与pfx文件相同的目录下生成一个snk文件(其名称与扩展名不同)。

powershell.exe -File pfx2snk.ps1 -pfxFilePath cert.pfx -pfxPassword "pfx password"

或者,如果您的PFX没有密码(真是太丢人了):

powershell.exe -File pfx2snk.ps1 cert.pfx

如果你不幸在一个不允许执行PowerShell脚本的环境中工作(即只允许交互式PowerShell会话),那么你可以从标准的cmd.exe命令行中执行这个丑陋的单行命令(根据需要替换文件路径和pfx密码)。

powershell.exe -Command "[IO.File]::WriteAllBytes('SnkFilePath.snk', ([Security.Cryptography.RSACryptoServiceProvider](New-Object System.Security.Cryptography.X509Certificates.X509Certificate2((Get-Content 'PfxFilePath.pfx' -Encoding Byte), 'PfxPassword', [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)).PrivateKey).ExportCspBlob($true));"

实际上,我将这个一行代码作为我的 Visual Studio 预构建过程的标准部分,以自动化使用我们的 Authenticode 签名证书(pfx 文件)中相同的密钥进行强名称签名的过程。这并非必需,但在我看来似乎是有道理的,而且符合我的强迫症倾向。

(我使用 snk 文件而不是原始的 pfx,因为我曾经遇到使用 pfx 文件进行强名称签名的“错误”体验,就像 @punkcoder 在他的回答中提到的一样)

如果你感兴趣的话,我有类似下面的东西作为我的 Visual Studio 后构建过程的一部分,以添加 Authenticode 签名到项目输出中(至少是在“Release”项目配置中)。

powershell.exe -Command "Set-AuthenticodeSignature -FilePath '$(TargetPath)' -Certificate '$(SolutionDir)MyCert.pfx' -TimestampServer http://timestamp.verisign.com/scripts/timstamp.dll -HashAlgorithm sha256;"

1
谢谢!由于某种原因,最后一行没有为我输出文件,但是将其替换为:Set-Content -Path "$snkFilePath" -Value $snkBytes -Encoding Byte 解决了这个问题。 - Jon List
1
文件似乎被放置在我运行脚本的上级目录中。我将脚本的最后一行更改为: [IO.File] :: WriteAllBytes((Join-Path-Path(Get-Location)-ChildPath $ snkFileName),$ snkBytes); - Corey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接