《数据加解密与哈希》演示了“数据保护”框架如何用来对数据进行加解密,而“数据保护”框架的核心是“密钥管理”。数据保护框架以XML的形式来存储密钥,默认的IKeyManager实现类型为XmlKeyManager。接下来我们通过模拟代码和实例演示的形式来介绍一下XmlKeyManager对象针对密钥的创建、撤销和回收的实现原理。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)
[S1308]基于本地文件系统的密钥管理(密钥创建)[S1308]基于本地文件系统的密钥管理(密钥创建)(源代码)
[S1309]基于本地文件系统的密钥管理(密钥撤销)(源代码)
接下来我们通过如下这个简单的演示实例来看看创建出来的密钥对应的XML具有怎样的结构。如代码片段所示,我们通过依赖注入容器得到IKeyManager对象,并用它创建了三个密钥。在调用AddDataProtection扩展方法后,我们调用返回IDataProtectionBuilder对象的PersistKeysToFileSystem扩展方法,其目的是利用FileSystemXmlRepository对象将代表创建密钥和密钥撤销操作的XML存储在指定的目录(“c:\keys”)下。
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection.KeyManagement; using Microsoft.Extensions.DependencyInjection; var directory = "c:\\keys"; var services = new ServiceCollection(); services .AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(directory)); var keyManager = services .BuildServiceProvider() .GetRequiredService<IKeyManager>(); var key1 = keyManager.CreateNewKey(DateTimeOffset.Now, DateTimeOffset.Now.AddDays(1)); var key2 = keyManager.CreateNewKey(DateTimeOffset.Now ,DateTimeOffset.Now.AddDays(2)); var key3 = keyManager.CreateNewKey(DateTimeOffset.Now, DateTimeOffset.Now.AddDays(3)); Console.WriteLine(key1.KeyId); Console.WriteLine(key2.KeyId); Console.WriteLine(key3.KeyId);
演示程序运行后会将创建的三个密钥的ID以如图1的形式输出到控制台上。与此同时,目录“c:\keys\”下会出现三个对应的XML文件。从图中可以看出,表示密钥文件的名称正是调用IXmlRepository对象的StoreElement方法时指定的名称。
图1 以XML文件存储的密钥
如下所示的是其中一个密钥对应的XML文件的内容。通过IAuthenticatedEncryptorDescriptor对象导出的XML体现在<key>/<descriptor>节点上。可以看出当前环境下默认采用的加密和哈希算法分别是AES_256_CBC和HMACSHA256,AuthenticatedEncryptorDescriptorDeserializer为采用的反序列化器类型。
<?xml version="1.0" encoding="utf-8"?> <key id="3f8769f0-78c8-42f2-9f13-102d83bb298c" version="1"> <creationDate>2022-03-12T09:47:38.4677311Z</creationDate> <activationDate>2022-03-12T17:47:38.4630578+08:00</activationDate> <expirationDate>2022-03-13T17:47:38.4674829+08:00</expirationDate> <descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60"> <descriptor> <encryption algorithm="AES_256_CBC" /> <validation algorithm="HMACSHA256" /> <masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection"> <!-- Warning: the key below is in an unencrypted form. --> <value>oFjUpRP4cKwp0QQctjibDN8QK3m76uQAlkGZ1V4E5sb9l9Yn4zzgjHyxpoJ7qENqBGwdD0A11U90YzGRb53p+g==</value> </masterKey> </descriptor> </descriptor> </key>[S1309]基于本地文件系统的密钥管理(密钥撤销)
我们接着来看看密钥的撤销。如果是针对单个密钥的撤销,该密钥的ID会通过名为“key”的子元素保存下来。如果需要撤销现有的所有密钥,这个key元素的值会设置为“*”。两者采用的文件名称也不相同,格式分别为“revocation-{KeyId}”和“revocation-{RevocationDate}”。我们接下来演示针对密钥的撤销。如下面的代码片段所示,在得到IKeyManager对象之后,我们调用其GetAllKeys方法得到所有密钥。在调用RevokeKey方法撤销第一个得到密钥之后,我们调用RevokeAllKeys方法将现有密钥全部撤销掉。
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection.KeyManagement; using Microsoft.Extensions.DependencyInjection; var directory = "c:\\keys"; var services = new ServiceCollection(); services .AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(directory)); var keyManager = services .BuildServiceProvider() .GetRequiredService<IKeyManager>(); var key = keyManager.GetAllKeys().First(); keyManager.RevokeKey(key.KeyId); keyManager.RevokeAllKeys(DateTimeOffset.Now, "Revocation Test");
演示程序执行后会在密钥存储目录(c:\keys\)下生成两个名称前缀为“revocation-”的XML文件。与上面的代码进行比较,我们会发现XML文件的名称依然是调用IXmlRepository对象的StoreElement方法指定的名称。
图2 描述密钥撤销的XML文件
如下所示的是这两个描述密钥撤销的XML文件的内容,可以看出XML结构与前面提供的RevokeKey和RevokeAllKeys方法的定义是匹配的。
revocation-184d9274-78f1-4352-bbed-1d0d6803ddad.xml
<?xml version="1.0" encoding="utf-8"?> <revocation version="1"> <revocationDate>2022-03-12T09:54:48.1157691Z</revocationDate> <key id="184d9274-78f1-4352-bbed-1d0d6803ddad" /> <reason /> </revocation>
revocation-20220312T0954481194781Z.xml
<?xml version="1.0" encoding="utf-8"?> <revocation version="1"> <revocationDate>2022-03-12T17:54:48.1194781+08:00</revocationDate> <!-- All keys created before the revocation date are revoked. --> <key id="*" /> <reason>Revocation Test</reason> </revocation>