在 ASP.NET Core 中取消保护密钥已撤销的有效负载

ASP.NET Core 数据保护 API 并非主要用于无限期保留机密有效负载。 Windows CNG DPAPIAzure Rights Management 等其他技术更适用于无限期存储的场景,这些技术具有相应强大的密钥管理功能。 也就是说,开发人员也可以使用 ASP.NET Core 数据保护 API 来长期保护机密数据。 密钥绝不会从密钥链中删除,因此,只要密钥可用并且有效,IDataProtector.Unprotect 便始终能够恢复现有的有效负载。

但是,当开发人员尝试取消保护已撤销的密钥所保护的数据时会出现问题,在这种情况下,IDataProtector.Unprotect 将引发异常。 对于短期或暂时的有效负载(例如身份验证令牌)而言可能问题不大,因为系统可以轻松地重新创建这类有效负载,在最坏的情况下,站点访问者可能需要重新登录。 但是,对于持久性有效负载而言,引发 Unprotect 可能会导致不可接受的数据丢失。

IPersistedDataProtector

为了支持即使在面临密钥已撤销的情况下能够取消对有效负载的保护,数据保护系统包含一个 IPersistedDataProtector 类型。 若要获取 IPersistedDataProtector 的实例,只需以正常方式获取 IDataProtector 的实例并尝试将 IDataProtector 强制转换为 IPersistedDataProtector 即可。

注意

并非所有的 IDataProtector 实例都可强制转换为 IPersistedDataProtector。 开发人员应将 C# 用作运算符或类似的功能来避免无效的强制转换所导致的运行时异常,并且应准备好适当地处理故障情况。

IPersistedDataProtector 公开以下 API 表面:

DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors,
     out bool requiresMigration, out bool wasRevoked) : byte[]

此 API 采用受保护的有效负载(用作字节数组)并返回未受保护的有效负载。 没有基于字符串的重载。 两个输出参数如下所示。

  • requiresMigration:如果用于保护此有效负载的密钥不再是活动的默认密钥,例如用于保护此有效负载的密钥是旧密钥并且自此以来已执行过密钥滚动操作,那么此参数将设为 true。 调用方可能需要根据业务需求考虑重新保护有效负载。

  • wasRevoked:如果用于保护此有效负载的密钥已撤销,此参数将设为 true。

警告

ignoreRevocationErrors: true 传递给 DangerousUnprotect 方法时应极为谨慎。 如果在调用此方法后 wasRevoked 值为 true,这意味着用于保护此有效负载的密钥已撤销,应警惕此有效负载的真实性。 在这种情况下,只有在能通过其他一些方面保证未受保护的有效负载的真实性时才可以继续对此有效负载进行操作,例如此有效负载来自安全的数据库,而不是由不受信任的 Web 客户端发送。

using System;
using System.IO;
using System.Text;
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();

        // get a protector and perform a protect operation
        var protector = services.GetDataProtector("Sample.DangerousUnprotect");
        Console.Write("Input: ");
        byte[] input = Encoding.UTF8.GetBytes(Console.ReadLine());
        var protectedData = protector.Protect(input);
        Console.WriteLine($"Protected payload: {Convert.ToBase64String(protectedData)}");

        // demonstrate that the payload round-trips properly
        var roundTripped = protector.Unprotect(protectedData);
        Console.WriteLine($"Round-tripped payload: {Encoding.UTF8.GetString(roundTripped)}");

        // get a reference to the key manager and revoke all keys in the key ring
        var keyManager = services.GetService<IKeyManager>();
        Console.WriteLine("Revoking all keys in the key ring...");
        keyManager.RevokeAllKeys(DateTimeOffset.Now, "Sample revocation.");

        // try calling Protect - this should throw
        Console.WriteLine("Calling Unprotect...");
        try
        {
            var unprotectedPayload = protector.Unprotect(protectedData);
            Console.WriteLine($"Unprotected payload: {Encoding.UTF8.GetString(unprotectedPayload)}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
        }

        // try calling DangerousUnprotect
        Console.WriteLine("Calling DangerousUnprotect...");
        try
        {
            IPersistedDataProtector persistedProtector = protector as IPersistedDataProtector;
            if (persistedProtector == null)
            {
                throw new Exception("Can't call DangerousUnprotect.");
            }

            bool requiresMigration, wasRevoked;
            var unprotectedPayload = persistedProtector.DangerousUnprotect(
                protectedData: protectedData,
                ignoreRevocationErrors: true,
                requiresMigration: out requiresMigration,
                wasRevoked: out wasRevoked);
            Console.WriteLine($"Unprotected payload: {Encoding.UTF8.GetString(unprotectedPayload)}");
            Console.WriteLine($"Requires migration = {requiresMigration}, was revoked = {wasRevoked}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
        }
    }
}

/*
 * SAMPLE OUTPUT
 *
 * Input: Hello!
 * Protected payload: CfDJ8LHIzUCX1ZVBn2BZ...
 * Round-tripped payload: Hello!
 * Revoking all keys in the key ring...
 * Calling Unprotect...
 * CryptographicException: The key {...} has been revoked.
 * Calling DangerousUnprotect...
 * Unprotected payload: Hello!
 * Requires migration = True, was revoked = True
 */