主要是今天必须这么复杂,因为昨天这么复杂,没有人想出更简单的方法。
我无法在这里提出线性叙述,所以请忍受需要的来回编织。
什么是 PFX/PKCS#12 文件?
虽然我不能完全说明 PFX 的起源是什么,但在 Windows 函数的名称中有一条线索可以读取和写入它们:PFXImportCertStore和PFXExportCertStore。它们包含许多可以使用属性标识符相互关联的独立实体(证书、私钥和其他东西)。它们似乎被设计为整个证书存储的导出/导入机制,就像所有 CurrentUser\My. 但由于一种存储是“内存存储”(任意集合),.NET 导入/导出是有意义的,但会出现一些复杂情况(从 .NET 之前开始)。
Windows 私钥
Windows 支持许多不同位置的私钥,但对于旧版加密 API,它们归结为 4 部分寻址方案:
- 加密提供者的名称
- 密钥容器的名称
- 这是机器相关密钥还是用户相关密钥的标识符
- 这是一个“签名”密钥还是“交换”密钥的标识符。
这被简化为 CNG 的 3 部分方案:
- 存储引擎的名称
- 钥匙的名字
- 这是机器相关密钥还是用户相关密钥的标识符。
为什么需要机器或非机器标识符?
CAPI 和 CNG 都支持直接与命名键交互。因此,您创建了一个名为“EmailDecryption”的密钥。系统上的另一个用户创建了一个同名的密钥。那应该工作吗?我们可能会。所以,huzzah,确实如此!单独的键,因为它们保存在与创建它们的用户相关的上下文中。
但是现在您需要一个可供多个用户使用的密钥。这不是您通常想要的东西,所以它不是默认设置。这是一个选择。CRYPT_MACHINE_KEYSET
国旗诞生了。
我将继续在这里说,我听说现在不鼓励直接使用命名键;CAPI/CNG 团队更喜欢以 GUID 命名的密钥,并且您可以通过证书存储与它们进行交互。但这是进化的一部分。
导入 PFX 有什么作用?
PFXImportCertStore 将所有证书从 PFX 复制到提供的存储中。它还导入(CryptImportKey或BCryptImportKey,取决于它认为需要什么)。然后,对于它导入的每个密钥,它(通过 PFX 中的属性值)找到匹配的证书,并在证书存储表示上为“这是我的 4 部分标识符”设置一个属性(CNG 密钥只是设置了第四个部分为 0); 这实际上是证书对其私钥的全部了解。
(PFX 是一种非常复杂的文件格式,如果没有使用任何“奇怪的部分”,则此描述是正确的)
密钥寿命
Windows 私钥永远存在,或者直到有人删除它们。
因此,当 PFX 导入它们时,它们将永远存在。如果您要导入 CurrentUser\My,这是有道理的。如果您正在做一些暂时的事情,那就没有意义了。
.NET 颠倒关系 / 让它“太容易”
Windows 设计(主要)是您与证书存储交互,并从证书存储中获取证书。.NET 后来出现了,并且(根据了解应用程序真正在做什么来推测)使证书成为顶级对象,并存储某种次要的东西。
因为 Windows 证书(实际上是“存储证书元素”)“知道”它们的私钥是什么,所以 .NET 证书“知道”它们的私钥是什么。
哦,但是 MMC 证书管理器说它可以使用其私钥导出证书(到 PFX 中),为什么证书构造函数不能接受除了“只是一个证书”格式之外的那些字节?好的,现在可以了。
调和一生
您打开一些字节作为 X509Certificate/X509Certificate2。这是一个“无密码”的 PFX(通过任何可能是真的方式)。您发现它是错误的,然后您将证书交给了垃圾收集器。该私钥永远存在,因此您的硬盘驱动器会慢慢填满,并且密钥存储访问变得越来越慢。然后你生气了,重新格式化你的电脑。
这看起来很糟糕,所以 .NET 所做的是,当证书的(一个字段)被垃圾收集(实际上是最终确定的)时,它会告诉 CAPI(或 CNG)删除密钥。现在事情按预期工作,对吧?好吧,只要程序没有异常终止。
哦,您将其添加到持久存储中?但是在新的证书存储实体“知道”如何找到私钥之后,我将删除私钥。这似乎很糟糕。
进入X509KeyStorageFlags.PersistKeySet
PersistKeySet 说“不要做那个删除的事情”。当您打算将证书添加到 X509Store 时。
如果您希望在不指定标志的情况下获得相同的行为,请在导入后调用Environment.FailFast或拔下计算机。
关于那个机器或用户位
在 .NET 中,您可以轻松地在集合中获取一袋证书并调用Export
它。如果有些人有机器密钥,而另一些人有用户密钥怎么办?PFXExportCertStore 进行救援。导出机器密钥时,它会记下一个标识符,表明它是机器密钥,因此导入会将其放回同一位置。
嗯,通常。也许您从一台机器上导出了机器密钥,而您只想在另一台机器上以非管理员身份检查它。好的,您可以指定CRYPT_USER_KEYSET
aka X509KeyStorageFlags.UserKeySet
。
哦,您在一台机器上以用户身份创建它,但希望它作为另一台机器上的机器密钥?美好的。 CRYPT_MACHINE_KEYSET
/ X509KeyStorageFlags.MachineKeySet
。
我真的需要“临时”文件吗?
如果您只是检查 PFX 文件,或者想临时使用它们,为什么还要费心将密钥写入磁盘呢?好的,Windows Vista 说,我们可以直接将私钥加载到加密密钥对象中,然后我们会告诉您指针。
PKCS12_NO_PERSIST_KEY
/X509KeyStorageFlags.EphemeralKeySet
我想如果 Windows 在 NT4 中具有此功能,那么这将是 .NET 的默认设置。它现在不能成为默认值,因为太多的事情取决于“正常”导入如何检测私钥是否可用的内部机制。
最后两个呢?
PFXImportCertStore 的默认模式是私钥不应该是可重新导出的。要告诉它这是错误的,您可以指定CRYPT_EXPORTABLE
/ X509KeyStorageFlags.Exportable
。
CAPI 和 CNG 都支持一种机制,其中软件密钥在使用私钥之前可能需要同意或密码(如智能卡的 PIN 提示),但您必须在首次创建(或导入)密钥时声明. 因此 PFXImportCertStore 允许您指定CRYPT_USER_PROTECTED
(并且 .NET 将其公开为X509KeyStorageFlags.UserProtected
)。
最后两个实际上只对“一个私钥”PFX 有意义,因为它们适用于所有密钥。它们也不包含原始密钥可能具有的全部选项...... CNG 和 CAPI 都支持“可归档”密钥,这意味着“可导出一次”。机器密钥上的自定义 ACL 在 PFX 中也得不到任何支持。
概括
对于证书(或证书集合),一切都很简单。一旦涉及到私钥,事情就会变得一团糟,Windows 证书(存储)的抽象变得有点薄,您需要了解持久性模型和存储模型。