这是一系列文章中首次集中讨论关于从Windows Azure社区分享“现实世界”技术信息。该文章的作者是Walter Myers III,他是微软咨询服务的资深顾问。
问题
我浏览过各种Windows Azure相关的帖子,在这些帖子中开发人员选择了使用加密和解锁数据的对称密钥方案。一个重要的情形是,当开发者需要在SQL Azure中存储加密数据时,它将在一个Windows Azure应用程序中被解密,从而呈现给用户。另一种是数据同步的情形,非云端数据必须与SQL Azure里的数据保持同步,当部署到Windows Azure时这些数据就会被加密。
开发人员可能会将加密密钥作为一个blob存储在Windows Azure里,只要存储密钥所涉及的Windows Azure存储是安全的,这种方式就是安全的;但是这不是最佳的做法,因为开发人员必须访问对称密钥,这可能在非云端不知不觉地泄露了对称密钥。此外,如果Windows Azure 应用程序被泄露,那么有可能密钥就被泄露了。本文提供了Windows Azure应用程序的基于证书的数据加密和解密的模型和代码。
解决方案
首先,让我提供一些背景。使用一个基于证书(不对称的密钥)的方法,最好的做法是遵循用来保护私钥的“关注分离”协议。这样,它将通过Windows Azure应用程序负责任何带有私钥并被作为服务证书上传到Windows Azure Management Portal,以供使用(Windows Azure应用程序可用的服务证书必须被上传到相应的托管服务)。开发人员被提供一个公钥,这个公钥只能在应用程序部署时用在开发机器上。当在开发fabric里测试的时候,开发人员必须使用一个他们利用IIS7通过自我认证而创建的证书。在部署时,他们将做一个简单的替换,用上传到Windows Azure的服务证书替换他们加密/解密代码中的thumbprint,并且利用他们的应用程序来部署服务证书的公钥。
开发人员必须在他们的应用程序上部署公钥,当Windows Azure调用角色示例,它将使服务定义里的thumbprint与上传的服务证书匹配并将私钥部署到角色示例。私钥是有意设成的不可导出的.pfx格式,所以你不能通过RDC连接到一个角色实例来获取私钥。
解决方案的实现
到目前为止,我们讨论了一些理论,现在让我们来看看这些概念具体是怎样表现的。注意此解决方案使用了Visual Studio中为证书管理提供的功能。
如果你尚未准备好,那就现在开始吧,将公钥证书安装到你的个人证书存储区。使用Local Computer而不是使用Current User存储区,因此你的代码应该与将要部署证书的Windows Azure的代码 一致。注意,为了看见证书,你不能只是启动certmgr.msc,因为它会带你来到Current User存储区。你还要启动mmc.exe 并选择File| Add/Remove Snap-In…菜单选项,添加证书,如下面的截图所示。
因此你的证书控制台应该和下图类似:
在部署应用程序之前,先让我们看一看它在Visual Studio 2010所展现的样子,然后看看在Windows Azure角色实例中证书控制台是什么样的。下面的截图来自于我的web角色里的Properties页,并让Certificates标签处在选择的状态。我在该截图上为Certificates添加了高亮文本并将其重新命名为EncryptDecrypt。注意存储单元是LocalMachine,存储名称是My,这就是我们想要的。
一旦你在这里添加了证书,你马上可以进入到ServiceDefinition.csdef文件,类似于下图。你在ServiceDefinition.csdef里还会发现与thumbprint 一起的一个entry。
部署应用程序之后,你可以为任意实例建立一个Remote Desktop Connection(RTC)(假设你在发布应用程序时已经配置了RDC)。以上面所示的相同的方式,启动mmc.exe 并为Local Computer 和 Current User添加 Certificates snap-ins。你的RDC窗口应类似于下图。
请注意证书已经安装到Local Computer个人证书存储区,但是没有一个已安装到Current User个人存储区。这是将上传服务证书到托管服务与配置角色(它是Windows Azure需要在证书存储区安装证书的原因)证书结合到一起。现在,如果你右击certificate并试图导出,就像上面所讨论的那样,你将会看到私钥是不可导出的,这是期望中的事情,如下图所示。
因此现在我们知道了即将用来加密/解密的证书是如何处理的。下一步,让我们看一看加密/解密的例程。
public static class X509CertificateHelper { public static X509Certificate2 LoadCertificate(StoreName storeName, StoreLocation storeLocation, string thumbprint) { // The following code gets the cert from the keystore X509Store store = new X509Store(storeName, storeLocation); store.Open(OpenFlags.ReadOnly); X509Certificate2Collection certCollection = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); X509Certificate2Enumerator enumerator = certCollection.GetEnumerator(); X509Certificate2 cert = null; while (enumerator.MoveNext()) { cert = enumerator.Current; } return cert; } public static byte[] Encrypt(byte[] plainData, bool fOAEP, X509Certificate2 certificate) { if (plainData == null) { throw new ArgumentNullException("plainData"); } if (certificate == null) { throw new ArgumentNullException("certificate"); } using (RSACryptoServiceProvider provider = new RSACryptoServiceProvider()) { provider.FromXmlString(GetPublicKey(certificate)); // We use the public key to encrypt. return provider.Encrypt(plainData, fOAEP); } } public static byte[] Decrypt(byte[] encryptedData, bool fOAEP, X509Certificate2 certificate) { if (encryptedData == null) { throw new ArgumentNullException("encryptedData"); } if (certificate == null) { throw new ArgumentNullException("certificate"); } using (RSACryptoServiceProvider provider = (RSACryptoServiceProvider) certificate.PrivateKey) { // We use the private key to decrypt. return provider.Decrypt(encryptedData, fOAEP); } } public static string GetPublicKey(X509Certificate2 certificate) { if (certificate == null) { throw new ArgumentNullException("certificate"); } return certificate.PublicKey.Key.ToXmlString(false); } public static string GetXmlKeyPair(X509Certificate2 certificate) { if (certificate == null) { throw new ArgumentNullException("certificate"); } if (!certificate.HasPrivateKey) { throw new ArgumentException("certificate does not have a PK"); } else { return certificate.PrivateKey.ToXmlString(true); } } }
注意在上面的加密和解密例程中,我们需要为加密获取公钥但必须获取解密的私钥。这很重要,因为Public Key Infrastructure (PKI)使任何拥有公钥的人执行加密,但只有拥有私钥的人才有权限来解密加密字符串。一个显著的差异是,当我们获取密钥,我们可以将公钥导出到XML,如加密例程中所示的那样,但是我们在解密例程中不能将私钥导出到XML,因为证书是与私钥一起部署的,Windows Azure上私钥设为 non-exportable,这就是之前我们所讲到的。
让我们来看看我写的一段代码,使用上面的X509 encrypt/decrypt helper class来加密和解密一个字符串:
string myText = "Encrypt me."; X509Certificate2 certificate = X509CertificateHelper.LoadCertificate( StoreName.My, StoreLocation.LocalMachine, "D3E6F7F969546ED620A255794CAB31D8C07E9F31"); if (certificate == null) { Response.Write("Certificate is null."); return; } byte[] encoded = System.Text.UTF8Encoding.UTF8.GetBytes(myText) byte[] encrypted; byte[] decrypted; try { encrypted = X509CertificateHelper.Encrypt(encoded, true, certificate); } catch (Exception ee) { Response.Write("Encrypt failed with error: " + ee.Message + "<br>"); return; } try { decrypted = X509CertificateHelper.Decrypt(encrypted, true, certificate); } catch (Exception ed) { Response.Write("Decrypt failed with error: " + ed.Message + "<br>"); return; }
这样,在上面的代码中我装载了我的证书,使用个人存储区放在本地计算机上。X509 encrypt/decrypt class 的LoadCertificate方法的最后一个参数保存了角色的property页上Certificates标签里获取的thumbprint。作为练习,你可以编写一些代码从ServiceConfiguration.cscfg文件中检索此字符串。
参考文献: http://www.josefcobonnin.com/post/2007/02/20/Encrypting-with-Certificates.aspx