ASP.NET Core에서 해지된 키가 속한 페이로드 보호 해제

ASP.NET Core 데이터 보호 API는 주로 기밀 페이로드의 무한 지속성을 위한 것이 아닙니다. Windows CNG DPAPIAzure Rights Management와 같은 다른 기술은 무한 스토리지의 시나리오에 더 적합하며 강력한 키 관리 기능을 제공합니다. 즉, 기밀 데이터의 장기 보호를 위해 개발자가 ASP.NET Core 데이터 보호 API를 사용할 필요가 없습니다. 키가 키 링에서 제거되지 않으므로 키가 사용 가능하고 유효한 경우 IDataProtector.Unprotect는 항상 기존 페이로드를 복구할 수 있습니다.

그러나 개발자가 해지된 키로 보호되는 데이터를 보호 해제하려고 할 때 문제가 발생합니다. 이 경우 IDataProtector.Unprotect에서 예외를 throw합니다. 이러한 종류의 페이로드를 시스템에서 쉽게 다시 만들 수 있으며, 최악의 경우 사이트 방문자가 다시 로그인해야 할 수 있으므로 수명이 짧거나 임시 페이로드(예: 인증 토큰)에는 문제가 없을 수 있습니다. 그러나 지속형 페이로드의 경우 Unprotect throw로 인해 데이터 손실이 허용되지 않을 수 있습니다.

IPersistedDataProtector

해지된 키가 있는 경우에도 페이로드의 보호를 해제할 수 있도록 하는 시나리오를 지원하기 위해 데이터 보호 시스템에는 IPersistedDataProtector 형식이 포함되어 있습니다. IPersistedDataProtector의 인스턴스를 가져오려면 단순히 IDataProtector의 인스턴스를 일반적인 방식으로 가져와 IDataProtectorIPersistedDataProtector로 캐스팅 해 봅니다.

참고 항목

모든 IDataProtector 인스턴스를 IPersistedDataProtector로 캐스팅할 수 있는 것은 아닙니다. 개발자는 C#을 연산자 또는 이와 유사한 방법으로 사용하여 잘못된 캐스트로 인해 발생하는 런타임 예외를 방지해야 하며, 실패 사례를 적절하게 처리할 수 있도록 준비해야 합니다.

IPersistedDataProtector는 다음 API 화면을 포함합니다.

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

이 API는 보호된 페이로드(바이트 배열)를 사용하고 보호되지 않는 페이로드를 반환합니다. 문자열 기반 오버로드가 없습니다. 두 out 매개 변수는 다음과 같습니다.

  • requiresMigration: 이 페이로드를 보호하는 데 사용된 키가 더 이상 활성 기본 키가 아닌 경우 true로 설정됩니다. 예를 들어 이 페이로드를 보호하는 데 사용되는 키가 오래되어 키 롤링 작업이 수행된 후에 발생합니다. 호출자는 비즈니스 요구에 따라 페이로드를 다시 보호하는 것을 고려할 수 있습니다.

  • wasRevoked: 이 페이로드를 보호하는 데 사용된 키가 해지된 경우 true로 설정됩니다.

Warning

ignoreRevocationErrors: trueDangerousUnprotect 메서드에 전달할 때는 매우 주의해야 합니다. 이 메서드를 호출한 후에는 wasRevoked 값이 true 이면 이 페이로드를 보호하는 데 사용된 키가 해지되고 페이로드의 신뢰성은 주의 대상으로 처리되어야 합니다. 이 경우 신뢰할 수 없는 웹 클라이언트에서 전송하는 것이 아니라 보안 데이터베이스에서 제공되는 것과 같이 신뢰할 수 있는 것으로 확신하는 경우에만 보호되지 않는 페이로드에서 계속 작동합니다.

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
 */