使用 .NET Framework 2.0 在您的应用程序中支持证书
Dominick Baier
本文讨论:
|
本文使用了以下技术: .NET Framework 2.0 |
目录
证书在 Microsoft® .NET Framework 中应用十分广泛,从安全通信到代码签名再到安全策略。.NET Framework 2.0 改进了对证书的支持,为使用证书进行符合标准的加密操作添加了一个全新的命名空间。在本文中,我将讨论证书和 Windows® 证书存储区的背景知识。同时我还会为您介绍证书 API 的使用方法和 Framework 如何使用这些 API 实现安全功能。
“证书”实际上是一种 ASN.1 (Abstract Syntax Notation One) 编码的文件,它包含一个公钥和其他有关该密钥及其所有者的信息。另外,证书具有有效期,并通过另一密钥(所谓的颁发者)进行签名,该密钥能保证这些属性的真实性,最重要的是,保证公钥本身的真实性。您可以将 ASN.1 看成是一种二进制 XML。与 XML 一样,它也具有编码规则、强类型和标记;但是,这些都是二进制值,通常没有可打印字符与之对应。
要使这种文件能够在系统之间互换,需要一种标准的格式。这种标准格式即 X.509(当前为第 3 版),RFC 3280 (tools.ietf.org/html/rfc3280) 中对其进行了描述。虽然 X.509 并未规定证书中嵌入的密钥类型,但 RSA 算法是目前使用最为普遍的非对称加密算法。
首先让我们回顾一下这种算法的历史由来。“RSA”这一名称是发明该算法的三个人的姓氏首字母缩写:Ron Rivest、Adi Shamir 和 Len Adleman。他们成立了一家名为 RSA Security 的公司,该公司发布了几个名为公钥加密标准 (PKCS) 的标准文档。这些文档对加密技术的几个方面进行了介绍。
其中最流行的文档之一,即 PKCS #7,为已签名和加密的数据定义了一种名为加密消息语法 (CMS) 的二进制格式。目前 CMS 广泛应用于众多流行的安全协议,其中包括安全套接字层 (SSL) 和安全多用途 Internet 邮件扩展 (S/MIME)。由于它是一种标准,因此当应用程序需要在几方之间交换已签名和加密的数据时,它也是一种可供选择的格式。您可以从 RSA Laboratories 网站 (www.rsasecurity.com/rsalabs/node.asp?id=2124) 获得这些 PKCS 文档。
如何获得一个证书
目前有几种方法可以获取证书。在交换文件时,证书通常以两种格式之一出现。扩展名为 .cer 的文件是采用 X.509v3 格式的已签名 ASN.1 文件。这些文件中包含着我之前提到的一个公钥和额外的信息。您要将这些文件中包含的内容提供给业务合作伙伴或朋友,以便他们能够使用公钥为您加密数据。
此外,您可能会遇到扩展名为 .pfx(个人信息交换,Personal Information Exchange)的文件。.pfx 文件包含一个证书和与之对应的私钥(PKCS #12 标准对该格式有所说明)。这类文件是高度敏感的,通常用于导入服务器上的密钥对或用于备份目的。在导出密钥对时,Windows 提供用密码加密 .pfx 文件;而在导入密钥对时,您必须再次提供此密码方可导入。
您还可以生成自己的证书。证书的生成方式通常取决于其使用方式。在对等方身份不明的常规 Internet 环境下,您通常要向某个商业证书颁发机构 (CA) 申请证书。该方法的优点在于这些已知的 CA 已经得到 Windows 和其他任何支持证书及 SSL 的 OS(包括浏览器)的信任。因此不必进行 CA 密钥的交换。
对于 B2B 和 Intranet 环境,您可以使用内部 CA。Windows 2000 和 Windows Server® 2003 中包含了证书服务。配合 Active Directory® 一起使用,此功能允许您在组织内轻松地分发证书。(稍后我将介绍如何从私有 CA 申请证书。)
在开发过程中,有时您可能会发现,刚才提到的方法不起作用了。例如,如果您出于测试的需要希望很快获得一个证书,可以使用 makecert.exe。该工具包随附于 .NET Framework SDK 中,能够生成证书和密钥对。在 IIS 资源工具包中也有一个与之类似的名为 selfssl.exe 的工具。它专门用于创建 SSL 密钥对,而且使用这种密钥,只需一个步骤即可对 IIS 进行配置。
Windows 证书存储区
证书和与之对应的私钥可存储在各种设备上,例如硬盘、智能卡和 USB 令牌。Windows 提供了一个名为证书存储区的抽象层,用于统一证书的访问方式,不管这些证书存储在何处。只要硬件设备具有 Windows 支持的加密服务提供程序 (CSP),就可以使用证书存储区 API 访问其上存储的数据。
证书存储区位于用户配置文件的深处。这样就可以对特定帐户的密钥使用 ACL。每个存储区被划分为若干个容器。例如,其中有一个名为 Personal 的容器,您可以将自己的证书(具有关联私钥的证书)存储在其中。Trusted Root Certification Authorities 容器包含了所有您信任的 CA 的证书。Other People 容器则保存着与您进行安全通信的人员的证书。此外还有其他一些证书。访问证书存储区最简单方法是运行 certmgr.msc。
另外还有一个供 Windows 计算机帐户(NETWORK、LOCAL SERVICE 和 LOCAL SYSTEM) 使用的计算机范围的存储区,如果您希望跨帐户共享证书或密钥,可以使用该存储区。 ASP.NET 应用程序总是使用计算机存储区;而对于桌面应用程序,证书通常安装在用户存储区。
只有管理员才能对计算机存储区和服务帐户存储区进行管理。要实现这一目的,您必须启动 Microsoft 管理控制台 (mmc.exe) 并添加“证书”管理单元,从中可以选择要管理的存储区。图 1 显示了 MMC 管理单元的一个屏幕快照。
0) this.src=small; if (current.indexOf(small) > 0)this.src=large;" alt="" src="http://msdn2.microsoft.com/zh-cn/magazine/cc163454.certificatesfig01.gif">
图 1 “证书”MMC 管理单元 (单击该图像获得较大视图)
除了可以导入、导出和搜索证书外,您还可以通过管理单元从内部企业 CA 申请证书。只需右键单击 Personal 容器并选择 All Tasks | Request Certificate。本地计算机会随后生成一个 RSA 密钥对,并将公钥部分发送给 CA 以进行签名。Windows 将已签名的证书添加到证书存储区,并将对应的私钥添加到密钥容器。证书通过存储属性被链接到密钥容器。
对于对应帐户或 LOCAL SYSTEM,私钥容器受到 ACL 的严密保护。当您要从 ASP.NET 或其他用户帐户访问计算机配置文件中存储的密钥时,这就成为一个问题。为此我编写了一个工具,您可以用它修改容器文件的 ACL (可从 www.leastprivilege.com/HowToGetToThePrivateKeyFileFromACertificate.aspx 下载该工具)。
商业 CA 和 Windows CA 还为申请证书提供了 Web 界面。通常在这些时候,由 Internet Explorer® 中的 ActiveX® 控件生成密钥并将其导入当前用户的存储区中。按照惯例,当您想指定某个证书可以由某个用户或服务访问时,有以下两种选择:一是将该证书导入该用户的存储区,二是在以该用户身份登录时申请证书。
使用证书
证书可用于 .NET Framework 中的各个位置,而且在某种程度上此功能依赖于 System.Security.X509Certificates 命名空间中的 X509Certificate 类。如果您更仔细地观察,还会发现证书类是以 2 结尾。这是因为 .NET Framework 1.x 具有一个名为 X509Certificate 的 X.509 证书表示形式。该类的功能有限,而且不支持加密操作。2.0 版中新添加了一个名为 X509Certificate2 的类。该类派生自 X509Certificate,同时添加了许多功能。您可以根据需要在二者之间来回转换,但无论何时都应尽可能使用最新版本。
访问证书
您可以直接从文件系统检索证书。但更好的办法是从证书存储区进行检索。要从一个 .cer 文件创建一个 X509Certificate2 实例,只需将文件名传递给构造函数即可:
X509Certificate2 cert1 = new X509Certificate2("alice.cer");
您也可以从 .pfx 文件加载证书。但是,如前所述,.pfx 文件可以通过设置密码加以保护,因此您应当将该密码以 SecureString 的形式提供给他人。SecureString 在内部对密码进行加密,并尽可能降低密码在内存、页面文件和崩溃转储期间的暴露几率。因此,您每次只能向字符串添加一个(值类型)字符。如果要从控制台要求用户提供密码,那么图 2 中的代码是非常有用的,这些代码可禁用控制台回显并返回 SecureString。
Figure 2 从控制台申请密码
在“使用 .NET Framework 2.0 进行加密管理”(可从 msdn.microsoft.com/library/en-us/dnnetsec/html/credmgmt.asp 获得)一文中,Kenny Kerr 谈到了将常见 Windows 凭据对话框的结果转换为 SecureString 的代码。不管您以何种方式获得 SecureString,它都可以随后被传递到 X509Certificate2 构造函数,以加载 .pfx 文件,如下所示:
X509Certificate2 cert2 = new X509Certificate2("alice.pfx", password);
要访问 Windows 证书存储区,请使用 X509Store 类。在其构造函数中,您要提供存储区位置(当前用户或计算机)和存储区名称。可以使用字符串或 StoreName 枚举来指定要打开的容器。注意,内部名称并不总是与 MMC 管理单元中找到的名称匹配。Personal 容器映射到名称 My,而 Other People 则变为 AddressBook。
获得有效的 X509Store 实例后,就可以搜索、检索、删除和添加证书。除非在部署情况下,否则使用最为频繁的恐怕要数搜索功能。您可以按各种条件搜索证书,其中包括使用者名称、序列号、指纹、颁发者和有效期。如果以编程方式从存储区检索应用程序中的证书,则应当使用唯一的属性,例如接收者密钥标识符。指纹虽然也是唯一的,但要记住,它是证书的一个 SHA-1 哈希值,在诸如续订证书时会发生改变。图 3 中的代码显示了一种搜索证书的常规方法。
Figure 3 搜索证书
在获得 X509 Certificate2 的一个实例后,可以检查证书的各个属性(如使用者名称、到期日期、颁发者和友好名称)。HasPrivateKey 属性会告知您是否存在关联私钥。PrivateKey 和 PublicKey 属性将对应密钥作为一个 RSACryptoServiceProvider 实例返回。
要导入证书,请对 X509Store 实例调用 Add 方法。当存储区的构造函数中不存在您所指定的存储区名称时,就会创建一个新容器。以下说明了如何将名为 alice.cer 的文件中的一个证书导入到名为 Test 的新容器中:
static void ImportCert() { X509Certificate2 cert = new X509Certificate2("alice.cer"); X509Store store = new X509Store("Test", StoreLocation.CurrentUser); try { store.Open(OpenFlags.ReadWrite); store.Add(cert); } finally { store.Close(); } }
显示证书详细信息和证书选择器
Windows 提供了两个标准对话框对证书进行操作:其中一个对话框用于显示证书的详细信息(各个属性和证书路径),另一个则供用户从列表中选择证书。您可以使用 X509Certificate2UI 类的两个静态方法访问这两个对话框:SelectFromCollection 和 DisplayCertificate。
要显示证书列表,必须填充 X509Certificate2Collection 并将其传递给 SelectFromCollection。让用户从存储区内的个人证书之一进行选择是很常见的。为此,只需传入一个已打开的 X509Store 的 Certificates 属性即可。您还可以控制对话框标题、消息以及是否允许多个选择。DisplayCertificate 方法显示的对话框与在 Windows 资源管理器中双击 .cer 文件时看到的对话框相同。图 4 显示了选择证书所用的对话框,图 5 提供相应的代码。
Figure 5 用于选择证书的代码
0) this.src=small; if (current.indexOf(small) > 0)this.src=large;" alt="" src="http://msdn2.microsoft.com/zh-cn/magazine/cc163454.certificatesfig04.gif">
图 4 用于选择证书的对话框 (单击该图像获得较大视图)
验证证书
验证证书时需要考虑几个条件,尤其是颁发方(通常仅信任可信 CA 列表中的 CA 所颁发的证书)及其当前有效性(证书可能会失效,例如当过期或者被颁发证书的 CA 吊销时)。X509Chain 类可以用来检查这些不同属性。使用该类,您可以为有效性检查指定策略,例如,可以要求一个受信任根 CA 或指定是否进行联机检查或检查本地吊销列表。如果需要对用于数据签名的证书进行检查,则在计算签名时检查证书是否有效就变得很重要;为此,X509Chain 允许更改验证时间。
构造策略后,可以调用 Build 方法来获取有关 ChainStatus 属性验证结果的信息。如果出现多个验证错误,您可以循环访问 ChainElement 集合以获取更多详细信息。图 6 显示了如何根据脱机和联机吊销列表对证书及其颁发者执行严格的验证。
Figure 6 严格的证书验证
SSL 支持
SSL 身份验证协议依赖于证书。.NET Framework 中对 SSL 的支持包含两个部分。HTTP 上的 SSL 这种特殊情况(但使用最为广泛)由 HttpWebRequest 类(它最终还可用于 Web 服务客户端代理)实现。要启用 SSL,除了要指定一个使用 Https: 协议的 URL 外,不必执行任何特殊操作。
当连接到一个受 SSL 保护的终结点时,会在客户端上对服务器证书进行验证。如果验证失败,连接会根据默认设置立即关闭。您可以回调一个名为 ServicePointManager 的类来重写该行为。每当 HTTP 客户端的堆栈进行证书验证时,都会首先检查是否可以回调;如果可以,则执行您的代码。要挂接该回调,您必须提供类型 RemoteCertificateValidationCallback 的一个委托:
// override default certificate policy // (for example, for testing purposes) ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(VerifyServerCertificate);
在回调中,您会获得服务器证书、一个错误代码和一个传入的链对象,然后可以执行自己的检查并返回 true 或 false。如果出现诸如证书在开发或测试期间就已过期的情况,那么关闭其中某项检查是有好处的。另一方面,这样做还可以执行比默认更为严格的验证策略。图 7 提供了一个验证回调的示例。
Figure 7 验证回调
SSL 还支持使用证书对客户端进行身份验证。如果您要访问的网站或服务要求提供客户端证书,那么 Web 服务客户端代理和 HttpWebRequest 都可提供 X509Certicate 类型的 ClientCertificates 属性:
proxy.Url = "https://server/app/service.asmx"; proxy.ClientCertificates.Add( PickCertificate(...));
此外,.NET Framework 2.0 还引入了一个名为 SslStream 的新类。该类允许您在任何流的顶部设置 SSL 层,而不仅限于 HTTP,从而可以对基于套接字的自定义协议启用 SSL。SslStream 将标准 .NET 证书支持应用于各个方面,例如,使用我所讨论的验证回调机制:
public SslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback ValidationCallback) {...}
要使用 SslStream 启动 SSL 身份验证,请将 X509Certificate 传递给其 AuthenticateAsServer 方法:
ssl.AuthenticateAsServer(PickCertificate(...));
Web 服务安全性
WS 安全性标准使用证书来指定客户端和服务器的身份验证并确保通信安全。诸如 .NET Framework 的 Web 服务增强 (WSE) 的各种工具包以及诸如 Windows Communication Foundation 的各项技术都对此提供了完全的支持。同样,最终还是要通过代码或配置来提供证书。以下代码段说明了如何使用 WSE3 向 Web 服务代理添加一个客户端证书:
X509SecurityToken token = new X509SecurityToken(PickCertificate(...)); proxy.RequestSoapContext.Security.Tokens.Add(token);
对于 Windows Communication Foundation,通常要引用配置文件中的证书存储区(参见图 8)。如您所见,所有配置属性都直接映射到在前面代码中使用的枚举。
Figure 8 在 WCF 中提供证书引用
安全策略和代码签名
证书还应用于 Authenticode® 代码签名。通过对二进制文件进行签名,您可以添加有关发布者的信息,并确保签名完成后该签名文件能得到可靠验证。.NET Framework SDK 中的 signtool.exe 工具可用于对 .exe 文件和 .dll 文件进行签名。签名后,使用 Windows 资源管理器中的属性对话框验证签名并查看证书。注意,如果要同时使用 Authenticode 签名和强名称签名,需要首先应用强名称签名。此外,在加载时,Authenticode 签名的程序集可能会出现延迟。如果被签名位置为可执行文件的入口点,则延迟会导致应用程序的启动时间延长。
已签名文件还可用于安全策略。使用软件限制策略,您可以根据签名或缺少签名来限制非托管可执行文件的执行(请参阅 microsoft.com/technet/prodtechnol/winxppro/maintain/rstrplcy.mspx)。而且 .NET Framework 代码访问安全 (CAS) 策略可以根据发行者证书来支持代码组。
要创建一个 CAS 策略,请根据发行者成员条件使用 mscorcfg.msc 创建一个新的代码组。然后将一个权限集分配给由该发行者签名的所有应用程序(参见图 9)。
0) this.src=small; if (current.indexOf(small) > 0)this.src=large;" alt="" src="http://msdn2.microsoft.com/zh-cn/magazine/cc163454.certificatesfig09_L.gif">
图 9 将权限分配给发行者 (单击该图像获得较大视图)
ClickOnce 清单
另一种将证书用于发行者信息的技术是 ClickOnce。在发布一个 ClickOnce 应用程序时,您必须对部署和应用程序清单进行签名。这会再次将发行者信息添加到应用程序,并确保清单中的敏感信息只有在签名失效的情况下才能被修改(如安全策略和应用程序依赖项)。在安装过程中,ClickOnce 使客户端能够使用发行者信息,以便客户可以对应用程序的可信性做出明智的决策。根据证书及其验证结果的不同,ClickOnce 安装程序还使用不同的可视化提示。图 10 显示了 Visual Studio® 清单签名对话框。
0) this.src=small; if (current.indexOf(small) > 0)this.src=large;" alt="" src="http://msdn2.microsoft.com/zh-cn/magazine/cc163454.certificatesfig10_L.gif">
图 10 Visutal Studio 清单签名对话框 (单击该图像获得较大视图)
对数据进行签名和加密
到目前为止,我主要介绍了与证书有关的基础 API 以及其他技术如何使用这些 API。现在我想与大家讨论一下加密操作(如使用证书对数据加密和签名)和如何实现 .NET Framework 2.0 中新发现的 PKCS #7。
数据的保护始终分为两个步骤。首先,对数据进行签名以防止其被篡改。然后,对数据进行加密以防止其被泄漏。但是,在使用 PKCS #7 类执行任何加密操作之前,都必须先将数据包装在一个 ContentInfo 对象中,表示为一个 CMS 数据结构。数据将从这里转换为已签名或加密的数据,分别由 SignedCms 和 EnvelopedCms 类来表示。
从技术上讲,数字签名是对随后使用私钥加密的数据的哈希。这意味着您需要一个含有关联私钥或 .pfx 文件的证书。您可以根据这种证书创建一个代表数据签名者的 CmsSigner 对象。接着 SignedCms 类会对签名进行计算并输出一个符合 PKCS #7 和 CMS 的字节数组。图 11 显示了对应的代码。已编码的字节数组包含了您的数据、签名和用于对数据签名的证书。
Figure 11 输出符合 CMS 的字节数组
如果您要对大量数据签名,这样做可能没有什么;但如果要签名的数据量很小,则会增加一些开销。例如,使用 2KB 公钥对一个 10 字节数组进行签名会产生一个大约 2,400 字节的数组。如果您打算将已签名数据存储在(比如说)数据库中,那么请牢记这一点。另一种方法是使用所谓的分离签名。这种方法使您能够从签名中删除数据并单独进行存储。例如,您可以先将多个小数据段加以组合,然后统一对其进行签名。要创建一个分离签名,必须向 SignedCms 构造函数再传递一个 true 值,如图 12 所示。
Figure 12 创建分离签名
对数据进行签名后,即可对其加密。您需要接收者的公钥才能对数据进行解密。通常这些公钥可以从 Other People 存储区获得,但如果不希望使用证书存储区,也可以从 .cer 文件获取公钥。这样一来,所有繁重工作就交给 EnvelopedCms 类来执行。您需要在 CmsRecipientCollection 中指定一个用于加密的公钥并将其传递到 Encrypt 方法中。与 SignedCms 一样,Encode 方法会在此处创建符合 PKCS #7 和 CMS 的字节数组(参见图 13)。
Figure 13 Encode 方法
EnvelopedCms 在内部生成一个随机会话密钥,数据借助该密钥被对称加密。随后再使用每个接收者的公钥对该会话密钥进行加密。这样一来,您就无需为每个收件人单独保留一份数据的加密版本。另外数据中还嵌入了一些额外的信息,使接收者能够从自己的证书存储区中找到匹配的私钥来解密数据。
解密数据和验证签名
接收端的整个解密过程是与加密过程是截然相反的。也就是说,首先要将数据解密,然后再验证签名和签名证书。在代码中,您必须首先调用 SignedCms 和 EnvelopedCms 类的 Decode 方法,将 CMS 字节数组反序列化为对象表示形式。然后再分别调用 Decrypt 和 CheckSignature。
该过程会查看已加密的数据包,以判断是否可以通过在证书存储区中搜索对应的私钥来解密会话密钥。随后再使用已解密的会话密钥对实际数据进行解密。您也可以提供一个解密期间要加以考虑的额外证书的列表,以防私钥并未存储在证书存储区:
static byte[] Decrypt(byte[] data) { // create EnvelopedCms EnvelopedCms encryptedMessage = new EnvelopedCms(); // deserialize PKCS#7 byte array encryptedMessage.Decode(data); // decryt data encryptedMessage.Decrypt(); // return plain text data return encryptedMessage.ContentInfo.Content; }
验证数据的过程分为两步。首先,确保签名是有效的,也就是说数据是未被篡改的。然后检查签名证书。使用 SignedCms 类的 CheckSignature 方法可同时执行这两步操作在这种情况下,证书会比照默认的系统策略被验证。如果您希望对该过程进行更大程度的控制,可以使用 X509Chain 对象和代码(如图 6 所示)亲自进行检查。图 14 显示了用于检查和删除签名的代码,而图 15 提供了用于验证分离签名的代码。
Figure 15 分离签名验证
Figure 14 验证和删除签名
综述
在使用安全策略或通信协议时,您会发现各种情况下都需要用到证书。本文介绍了用于检索和搜索证书的基础 API 以及如何利用这些 API 进行加密和数字签名。此外,我还为大家提供了一些更高级的应用程序服务的示例,这些服务要求您对 Windows 证书存储区以及公钥和私钥之间的关系有所了解。
本文的源代码可以从《MSDN® 杂志》网站下载,其中包括一个很小的支持对文件签名和加密的 Windows 窗体应用程序。该程序使用了本文所讨论的多种方法,如从不同的存储区选择证书以及使用加密和签名来保护/验证数据。
Dominick Baier是德国的一位独立安全咨询师。他帮助各家公司处理安全设计和体系结构、内容开发、渗透测试和代码审核等方面的问题。他还是 DevelopMentor 的安全课程负责人和开发人员安全 MVP,同时也是《Developing More-Secure Microsoft ASP.NET 2.0 Applications》(Microsoft Press,2006)一书的作者。他在 www.leastprivilege.com 开设了博客
http://msdn2.microsoft.com/zh-cn/magazine/cc163454.aspx#S10