5

我正在构建一个用于分发由不同组织创建的包(.zip 档案)的系统。我想要一种方法来验证一个包的发布者确实是他们声称的那个人,并且该文件没有被篡改。

为了验证发布者,需要一个类似于 Web 浏览器使用的系统 - 例如,我的应用程序联系根证书颁发机构,后者验证身份。换句话说,“绿条”:)

我猜包创建会像这样工作:

  1. 作者创建 zip 包
  2. 作者哈希包并签名哈希
  3. 它被重新包装,带有:
    • 包含签名哈希和公共证书的标头
    • 包含 zip 文件内容的正文

包装打开将像这样工作:

  1. 获取数据主体
  2. 使用相同的算法对其进行散列
  3. 使用证书中的公钥解密包的哈希
  4. 比较两个哈希 - 我们现在有完整性
  5. 联系根 CA 以验证身份

这样,我验证了身份,也验证了内容(内容本身不需要加密——目的是验证,不是隐私)。

所以我的问题是:

  1. 以上是处理它的正确方法吗?
  2. 人们通常使用什么哈希算法?我认为它应该是单向的。你会选择一个(MD5、SHA1、SHA2?)还是更正常的支持多种,让包作者告诉你他们使用的是哪一个(例如,文档的标题包含散列函数的名称)。
  3. 您如何使用根 CA?这是 X509Store 类的工作,还是涉及其他步骤?
  4. 这里涉及什么样的证书?用于签署 .NET 程序集的相同类型的证书?(代码签名证书?)

最后,如果一个组织没有付费证书,而是决定使用自行颁发的证书,我假设我仍然可以验证哈希(为了数据完整性),而无需将东西安装到计算机的证书存储中或任何类似的魔法(在这些情况下,我只会显示:“由 XYZ 公司发布(未验证)”。这是正确的吗?

我找到了很多关于如何使用 X509 和 RSACryptoServiceProvider 的链接,所以我大概可以弄清楚代码,我想我对这个过程更感兴趣,并且知道我正在使用正确的技术。

4

2 回答 2

18

我尝试了aku对 System.IO.Packaging 的建议并且非常喜欢它,尽管让签名工作非常困难且不直观(无论如何,对于我的小脑袋来说)。这是我采取的步骤,以防其他人需要这样做。

主要问题是文档只是说“证书”,但我必须创建一个受密码保护的 PFX 才能使其工作。两个有用的链接是:

  1. MSDN:开放打包约定的数字签名框架
  2. 创建自签名 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:一个有用的注释。要查看和使用计算机上的证书,请执行以下操作:

  1. 开始->运行并输入“mmc”
  2. 在 MMC 窗口中,单击 File->Add/Remove SnapIn...
  3. 点击证书,添加->
  4. 选择其中一个帐户
  5. 点击确定

就我而言,当我双击上面的 .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 组规范的一部分。创建它们的一种更简单的方法,而不是我上面展示的方法,是使用开源OpenSSL ,它具有可用于 Windows的二进制文件。

安装 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

现在您可以双击证书并安装它,它将被视为受信任的证书。

于 2009-01-30T17:18:41.110 回答
16

有一个标准 API 可以创建签名的 ZIP 包。

System.IO.Packaging命名空间包含必要的类来创建符合 OPS(开放包装规范)的带有数字签名的 ZIP 包。

于 2009-01-30T14:45:22.043 回答