ASP.NET Core 中的密钥管理

数据保护系统会自动管理用于保护和取消保护有效负载的主密钥的生存期。 每个密钥都可以处于以下四个阶段之一:

  • 已创建 - 密钥存在于密钥环中,但尚未激活。 在经过足够长的时间,使密钥有机会传播到使用此密钥环的所有计算机之前,不应将密钥用于新保护操作。

  • 活动 - 密钥存在于密钥环中,应该用于所有新保护操作。

  • 已过期 - 密钥运行了其自然生存期,不应再用于新保护操作。

  • 已撤销 - 密钥已泄露,不得用于新保护操作。

已创建、活动和已过期密钥全都可用于取消保护传入的有效负载。 默认情况下,已撤销密钥不能用于取消保护有效负载,但应用程序开发人员可以在必要时替代此行为

警告

开发人员可能想要从密钥环中删除密钥(例如通过从文件系统中删除对应文件)。 目前,由密钥保护的所有数据都是永久无法解密的,没有如同针对已撤销密钥那样的紧急替代。 删除密钥是真正的破坏性行为,因而数据保护系统未公开任何第一类 API 以用于执行此操作。

默认密钥选择

当数据保护系统从后备存储库中读取密钥环时,它会尝试在密钥环中找到“默认”密钥。 默认密钥用于新保护操作。

常规启发式方法是数据保护系统选择具有最新激活日期的密钥作为默认密钥。 (存在小的经验系数可允许服务器到服务器的时钟偏差)如果密钥已过期或已撤销,并且应用程序未禁用自动密钥生成,则会根据下面的密钥过期和滚动更新策略生成新密钥并立即激活。

数据保护系统立即生成新密钥而不是回退到其他密钥的原因是,新密钥生成应被视为在新密钥之前激活的所有密钥的隐式过期。 总体思路是新密钥可能使用与旧密钥不同的算法或静态加密机制进行配置,并且系统应首选当前配置,而不是回退。

有一个例外情况。 如果应用程序开发人员禁用自动密钥生成,则数据保护系统必须选择某些密钥作为默认密钥。 在此回退方案中,系统会选择具有最新激活日期的非已撤销密钥,并优先考虑有时间传播到群集中其他计算机的密钥。 因此,回退系统可能最终会选择已过期默认密钥。 回退系统从不会选择已撤销密钥作为默认密钥,并且如果密钥环为空或每个密钥都已撤销,则系统会在初始化时生成错误。

密钥过期和滚动更新

创建密钥时,系统会自动为密钥提供激活日期“{ 现在 + 2 天 }”和过期日期“{ 现在 + 90 天 }”。 激活前的 2 天延迟使密钥有时间在系统中传播。 也就是说,它使指向后备存储的其他应用程序可以在下次自动刷新期间观察到密钥,从而最大程度提高当密钥环变为活动状态时,它已传播到可能需要使用它的所有应用程序的可能性。

如果默认密钥将在 2 天后过期,并且密钥环还没有在默认密钥过期时将处于活动状态的密钥,则数据保护系统会自动将新密钥保持到密钥环。 此新密钥的激活日期为“{ 默认密钥的过期日期 }”,过期日期为“{ 现在 + 90 天 }”。 这样,系统便可以定期自动滚动更新密钥,而不会中断服务。

在某些情况下,密钥可能会在创建时立即激活。 一个示例是应用程序有一段时间未运行并且密钥环中的所有密钥都已过期的情况。 发生这种情况时,会为密钥提供激活日期“{ 现在 }”,而不是正常的 2 天激活延迟。

默认密钥生存期为 90 天,不过如以下示例所示,这是可配置的。

services.AddDataProtection()
       // use 14-day lifetime instead of 90-day lifetime
       .SetDefaultKeyLifetime(TimeSpan.FromDays(14));

管理员还可以在系统范围内更改默认设置,不过显式调用 SetDefaultKeyLifetime 会替代任何系统范围策略。 默认密钥生存期不能短于 7 天。

自动密钥环刷新

当数据保护系统初始化时,它会从基础存储库中读取密钥环,并将它缓存在内存中。 此缓存允许继续进行保护和取消保护操作,而无需访问后备存储。 系统大约每 24 小时一次或是在当前默认密钥过期时(以先到者为准)自动检查后备存储中是否存在更改。

警告

开发人员应很少(如果有)需要直接使用密钥管理 API。 数据保护系统会按如上所述执行自动密钥管理。

数据保护系统公开了接口 IKeyManager,该接口可用于检查和更改密钥环。 提供 IDataProtectionProvider 的实例的 DI 系统还可以提供 IKeyManager 的实例供你使用。 或者,你可以直接从 IServiceProvider 中拉取 IKeyManager,如下面的示例所示。

修改密钥环的任何操作(显式创建新密钥或执行撤销)会使内存中缓存失效。 对 ProtectUnprotect 的下次调用会导致数据保护系统重新读取密钥环并重新创建缓存。

下面的示例演示如何使用 IKeyManager 接口检查和操作密钥环,包括撤销现有密钥和手动生成新密钥。

using System;
using System.IO;
using System.Threading;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.DependencyInjection;

public class Program
{
    public static void Main(string[] args)
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddDataProtection()
            // point at a specific folder and use DPAPI to encrypt keys
            .PersistKeysToFileSystem(new DirectoryInfo(@"c:\temp-keys"))
            .ProtectKeysWithDpapi();
        var services = serviceCollection.BuildServiceProvider();

        // perform a protect operation to force the system to put at least
        // one key in the key ring
        services.GetDataProtector("Sample.KeyManager.v1").Protect("payload");
        Console.WriteLine("Performed a protect operation.");
        Thread.Sleep(2000);

        // get a reference to the key manager
        var keyManager = services.GetService<IKeyManager>();

        // list all keys in the key ring
        var allKeys = keyManager.GetAllKeys();
        Console.WriteLine($"The key ring contains {allKeys.Count} key(s).");
        foreach (var key in allKeys)
        {
            Console.WriteLine($"Key {key.KeyId:B}: Created = {key.CreationDate:u}, IsRevoked = {key.IsRevoked}");
        }

        // revoke all keys in the key ring
        keyManager.RevokeAllKeys(DateTimeOffset.Now, reason: "Revocation reason here.");
        Console.WriteLine("Revoked all existing keys.");

        // add a new key to the key ring with immediate activation and a 1-month expiration
        keyManager.CreateNewKey(
            activationDate: DateTimeOffset.Now,
            expirationDate: DateTimeOffset.Now.AddMonths(1));
        Console.WriteLine("Added a new key.");

        // list all keys in the key ring
        allKeys = keyManager.GetAllKeys();
        Console.WriteLine($"The key ring contains {allKeys.Count} key(s).");
        foreach (var key in allKeys)
        {
            Console.WriteLine($"Key {key.KeyId:B}: Created = {key.CreationDate:u}, IsRevoked = {key.IsRevoked}");
        }
    }
}

/*
 * SAMPLE OUTPUT
 *
 * Performed a protect operation.
 * The key ring contains 1 key(s).
 * Key {1b948618-be1f-440b-b204-64ff5a152552}: Created = 2015-03-18 22:20:49Z, IsRevoked = False
 * Revoked all existing keys.
 * Added a new key.
 * The key ring contains 2 key(s).
 * Key {1b948618-be1f-440b-b204-64ff5a152552}: Created = 2015-03-18 22:20:49Z, IsRevoked = True
 * Key {2266fc40-e2fb-48c6-8ce2-5fde6b1493f7}: Created = 2015-03-18 22:20:51Z, IsRevoked = False
 */

若要查看翻译为非英语语言的代码注释,请在 此 GitHub 讨论问题中告诉我们。

密钥存储

数据保护系统具有启发式方法,它通过该方法尝试自动推断合适的密钥存储位置和静态加密机制。 密钥持久性机制也可由应用开发人员进行配置。 以下文档讨论这些机制的内置实现: