我正在构建一个用于分发由不同组织创建的包(.zip档案)的系统.我想要一种方法来验证包的发布者确实是他们声称的那个,并且该文件没有被篡改.
要验证发布者,需要一个类似于Web浏览器使用的系统 - 例如,我的应用程序联系验证身份的根证书颁发机构.换句话说,'绿色酒吧':)
我猜测包的创建会像这样工作:
作者创建了zip包
作者哈希包并签署哈希
它被重新包装,包括:
包含签名哈希和公共证书的标头
包含zip文件内容的正文
打开包的方式如下:
拿走数据的主体
使用相同的算法对其进行哈希处理
使用证书中的公钥解密程序包的哈希值
比较两个哈希 - 我们现在有完整性
联系根CA以验证身份
这样,我已经验证了身份,并且还验证了内容(内容本身不需要加密 - 目标是验证,而不是隐私).
所以我的问题是:
以上是接近它的正确方法吗?
人们通常使用什么哈希算法?我认为它应该是单向的.您是否只选择一个(MD5,SHA1,SHA2?)或支持多种更常见,让包工作者告诉您他们使用了哪一个(例如,文档的标题包含散列函数的名称).
你如何使用根CA?这是X509Store类的工作,还是涉及其他步骤?
这里涉及哪种证书?用于签署.NET程序集的相同类型的证书?(代码签名证书?)
最后,如果组织没有付费证书而是决定使用自行颁发的证书,我认为我仍然可以验证哈希值(为了数据完整性),而无需在计算机的证书库中安装内容或任何类似的魔法(在这些情况下,我只是显示:"由XYZ公司出版(未经证实)".这是正确的吗?
我已经找到了很多关于如何使用X509和RSACryptoServiceProvider的链接,所以我可能想出代码,我想我对这个过程更感兴趣并且知道我正在使用正确的技术.
我尝试了aku对System.IO.Packaging的建议,并且非常喜欢它,虽然让签名工作非常困难而且不直观(无论如何我的小脑袋).以下是我采取的步骤,以防其他人需要这样做.
主要问题是文档只是说"证书",但我必须创建一个受密码保护的PFX才能使其正常工作.两个有用的链接是:
MSDN:开放式包装约定的数字签名框架
创建自签名PFX
如第二个链接所示,您有两个选项可用于创建PFX.您可以创建.cer,安装它然后使用GUI导出它,或者您可以从Microsoft 下载pvkimport.
以下是我使用的命令(编辑:您可以使用OpenSSL执行此操作 - 请参阅底部):
makecert -r -n "CN=Paul Stovell" -b 01/01/2000 -e 01/01/2099 -eku 1.3.6.1.5.5.7.3.3 -sv PaulStovell.pvk PaulStovell.cer cert2spc PaulStovell.cer PaulStovell.spc pvkimprt -pfx PaulStovell.spc PaulStovell.pvk
在最后一个命令中,出现一个向导.系统会询问您是否要导出私钥,这需要PFX受密码保护.选择"是".然后在下一页上有一个复选框,询问是否"导出所有扩展属性",我也选择了"是".
这将为您提供受密码保护的自签名PFX文件,您可以使用该文件对文档和包进行签名.
下面是一些代码,用于创建,签名和保存包,然后重新打开并验证它.
private const string _digitalSignatureUri = "/package/services/digital-signature/_rels/origin.psdsor.rels"; static void Main(string[] args) { var certificate = new X509Certificate2(@"T:\Sample\Input\PaulStovell.pfx", "password"); using (var package = Package.Open("T:\\Sample\\MyPackage.zip", FileMode.Create, FileAccess.ReadWrite, FileShare.None)) { CreatePart(package, @"/Files/File2.dll", @"T:\Sample\Input\File2.dll"); CreatePart(package, @"/Files/File2.pdb", @"T:\Sample\Input\File2.pdb"); CreatePart(package, @"/Files/File2.xml", @"T:\Sample\Input\File2.xml"); package.PackageProperties.Creator = "Paul Stovell"; package.PackageProperties.Title = "Paul Stovell's Package"; package.PackageProperties.Description = "My First Package"; package.PackageProperties.Identifier = "MyPackage"; package.PackageProperties.Version = "1.0.0.0"; // Sign the package var toSign = package.GetParts().Select(part => part.Uri).ToList(); var uriPartSignatureOriginRelationship = PackUriHelper.CreatePartUri(new Uri(_digitalSignatureUri, UriKind.Relative)); toSign.Add(uriPartSignatureOriginRelationship); var dsm = new PackageDigitalSignatureManager(package); dsm.CertificateOption = CertificateEmbeddingOption.InSignaturePart; dsm.Sign(toSign, certificate); package.Close(); } Console.WriteLine("Package written"); Console.WriteLine("Reading package"); using (var package = Package.Open("T:\\Sample\\MyPackage.zip", FileMode.Open, FileAccess.Read, FileShare.Read)) { Console.WriteLine(" Package name: {0}", package.PackageProperties.Title); var dsm = new PackageDigitalSignatureManager(package); if (dsm.IsSigned) { var verificationResult = dsm.VerifySignatures(false); var signature = dsm.Signatures[0]; Console.WriteLine(" Signed by: {0}", signature.Signer.Subject); Console.WriteLine(" Issued by: {0}", signature.Signer.Issuer); Console.WriteLine(" Verification: {0}", verificationResult); } else { Console.WriteLine(" Not signed."); } } Console.ReadKey(); } private static void CreatePart(Package package, string relativePath, string file) { var packagePartUri = new Uri(relativePath, UriKind.Relative); var packagePart = package.CreatePart(packagePartUri, "part/" + Path.GetExtension(file)); using (var fileContent = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)) { CopyStream(fileContent, packagePart.GetStream()); } } private static void CopyStream(Stream source, Stream target) { // It is .NET 3.5, surely this kind of thing has gotten easier by now? var bufferSize = 0x1000; var buf = new byte[bufferSize]; int bytesRead = 0; while ((bytesRead = source.Read(buf, 0, bufferSize)) > 0) { target.Write(buf, 0, bytesRead); } }
在我的机器上,输出是:
Package written Reading package Package name: Paul Stovell's Package Signed by: CN=Paul Stovell Issued by: CN=Paul Stovell Verification: Success
最后一行很有意思.它是PackageDigitalSignatureManager.VerifySignatures()的输出.它表示文档未被篡改.如果我在签名后更改或删除文件,则不再返回"成功".
我将.exe,示例文件和.pfx复制到一台新机器并得到完全相同的输出,这似乎表明"验证"仅检查签名的作者,并且不询问任何证书颁发机构.我没有在测试机器上安装任何证书,它在自己的域上运行.
编辑:上面的代码仅验证文档本身,它不使用根CA权限验证证书.为此,请使用以下内容:
在针对我的自签名证书调用时,此调用返回false:
var verified = ((X509Certificate2) dsm.Signatures[0].Signer).Verify();
然后,我双击我用来创建.pfx的.cer,并将其安装到默认位置,以便它成为受信任的证书颁发机构.完成后,上面的调用返回true.这似乎是验证签名者身份的正确方法.
编辑2:一个有用的说明.要查看并使用计算机上的证书,请执行以下操作:
开始 - >运行并输入"mmc"
在MMC窗口中,单击文件 - >添加/删除SnapIn ...
单击证书,添加 - >
选择其中一个帐户
点击OK
在我的情况下,当我双击上面的.cer进行安装时,它被放置在" 受信任的根证书颁发机构"文件夹下的当前用户存储下.然后,您可以将其删除以撤消更改.
我的最终验证逻辑如下所示:
if (dsm.IsSigned) { var verificationResult = dsm.VerifySignatures(false); var signature = dsm.Signatures[0]; var trusted = ((X509Certificate2)dsm.Signatures[0].Signer).Verify(); Console.WriteLine(" Signed by: {0}", signature.Signer.Subject); Console.WriteLine(" Issued by: {0}", signature.Signer.Issuer); Console.WriteLine(" Verified: {0}", verificationResult == VerifyResult.Success); Console.WriteLine(" Trusted: {0}", trusted); } else { Console.WriteLine(" Not signed."); }
总是验证的地方(除非我篡改内容),但只有当证书在商店中时才受信任.
编辑3:我做了一点阅读.PFX文件实际上是PKCS 12文件,是RSA组规范的一部分.创建它们的一种更简单的方法,而不是我上面展示的,是使用具有Windows二进制文件的开源OpenSSL.
安装OpenSSL后,创建公钥/私钥对文件.我发现我必须以管理员身份运行下面的第二个命令,因此以管理员身份启动此命令可能会付出代价.
openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout PaulStovell.pem -out PaulStovell.cer
它将询问有关您和您的组织的其他7个问题,以创建证书.接下来,创建PKCS 12.它将要求您输入密码:
openssl pkcs12 -export -out PaulStovell.pfx -in PaulStovell.pem -name "Paul Stovell"
在提示时输入并重新输入密码.
此时,System.IO.Packaging可以使用您的PFX并验证包签名,但它不会信任该证书.要创建.cer证书,以便可以将其安装到受信任的证书颁发机构存储中,请执行以下操作:
openssl x509 -in PaulStovell.pem -out PaulStovell.cer
现在,您可以双击证书并进行安装,它将被视为可信证书.
有一个标准的API来创建签名的ZIP包.
System.IO.Packaging命名空间包含必要的类,用于创建具有数字签名的 OPS(开放包装规范)符合的ZIP包.