主要原因是今天的复杂程度源于昨天的复杂程度,没有人提出更简单的解决方案。
我无法提供线性叙述,所以请耐心忍受必要的来回交织。
PFX/PKCS#12文件是什么?
虽然我无法完全说出PFX的起源,但Windows读取和写入它们的函数名称中有一个提示:PFXImportCertStore和PFXExportCertStore。它们包含许多不同的实体(证书、私钥和其他内容),可以使用属性标识符进行关联。它们似乎被设计为整个证书存储的导入/导出机制,例如所有CurrentUser\My。但由于一种存储类型是"内存存储"(任意集合),.NET的导入/导出是有意义的,但也存在一些复杂性(来自.NET之前)。
Windows私钥
Windows支持许多不同的私钥存储位置,但对于旧的加密API,它们归结为4部分寻址方案:
- 加密提供程序的名称
- 密钥容器的名称
- 标识符,指示这是机器相关密钥还是用户相关密钥
- 标识符,指示这是"签名"密钥还是"交换"密钥。
在CNG中,这被简化为一个三部分的方案:
- 存储引擎的名称
- 密钥的名称
- 标识符,指示这是一个机器相关密钥还是用户相关密钥。
为什么需要机器或非机器标识符?
CAPI和CNG都支持直接与命名密钥交互。因此,您可以创建一个名为“EmailDecryption”的密钥。系统上的另一个用户创建了相同名称的密钥。那么这能行吗?很可能可以。因此,它确实起作用!这是因为不同的密钥保存在与创建它们的用户相关联的上下文中。
但现在您想要一个可以由多个用户使用的密钥。这不是通常需要的东西,因此不是默认选项。这是一种选择。于是就有了CRYPT_MACHINE_KEYSET
标志。
我在这里要说的是,我听说现在不鼓励直接使用命名密钥; CAPI/CNG团队更喜欢GUID命名密钥,并建议您通过证书存储与其进行交互。但这是演变的一部分。
导入PFX做了什么?
PFXImportCertStore将PFX中的所有证书复制到提供的存储中。它还会导入(
CryptImportKey或
BCryptImportKey,具体取决于它认为需要什么)。然后,对于它导入的每个密钥,它通过PFX中的属性值找到匹配的证书,并在证书存储表示中设置一个属性,表示“这是我的4部分标识符”(CNG密钥只将第4部分设置为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
,又称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证书(存储)上的抽象变得有点薄弱,您需要了解持久性模型和存储模型。