테이터 보호: 키가 취소된 페이로드의 보호 해제하기

등록일시: 2017-04-12 08:00,  수정일시: 2017-04-12 08:00
조회수: 4,118
이 문서는 ASP.NET Core 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 키가 취소된 페이로드를 보호 해제하는 방법과 작업 시 주의사항 등을 살펴봅니다.

ASP.NET Core 데이터 보호 API는 무기한 기밀 페이로드의 유지를 목적으로 설계되지 않았습니다. 무기한 저장 시나리오에는 Windows CNG DPAPIAzure 권한 관리 같은 다른 기술이 더 적합하며, 이런 기술들은 그에 상응하는 강력한 키 관리 기능을 갖추고 있습니다. 그러나 개발자가 ASP.NET Core 데이터 보호 API를 이용해서 기밀 데이터를 장기간 보호하는 행위 자체를 금지하는 것은 아닙니다. 키는 키 링에서 절대로 제거되지 않으므로, 키가 사용 가능하고 유효하기만 하다면 언제나 IDataProtector.Unprotect 메서드로 원본 페이로드를 복구할 수 있습니다.

그러나 개발자가 취소된 키를 이용해서 보호된 데이터를 보호 해제하려고 시도할 경우 문제가 발생하는데, IDataProtector.Unprotect 메서드가 예외를 던지기 때문입니다. 단기 및 임시 페이로드는 (인증 토큰 같은) 시스템에서 손쉽게 재생성 할 수 있으므로 이런 유형의 페이로드는 크게 문제가 되지 않으며, 최악의 경우가 발생하더라도 사용자가 다시 로그인하면 그만입니다. 그러나 영속화된 페이로드의 경우에는 Unprotect 메서드가 예외를 던지는 상황이 발생하면 간과할 수 없는 데이터 유실이 발생하게 됩니다.

IPersistedDataProtector

키가 해제된 경우에도 페이로드를 보호 해제할 수 있도록 허용하는 시나리오를 지원하기 위해서 데이터 보호 시스템에는 IPersistedDataProtector 형식이 포함되어 있습니다. IPersistedDataProtector 인터페이스의 인스턴스를 얻기 위해서는 그냥 일반적인 IDataProtector의 인스턴스를 얻은 다음, 해당 인스턴스를 IPersistedDataProtector 형식으로 형변환하기만 하면 됩니다.

노트

모든 IDataProtector 인스턴스를 IPersistedDataProtector 형식으로 형변환 할 수 있는 것은 아닙니다. 따라서 개발자는 C#의 as 연산자나 비슷한 다른 기능을 이용해서 유효하지 않은 형변환 시도로부터 발생하는 런타임 예외를 방지하고, 형변환에 실패하는 경우에 대한 적절한 처리를 준비해야 합니다.

IPersistedDataProtector 인터페이스는 다음과 같은 API 표면을 노출합니다:

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

이 API는 보호된 페이로드를 전달 받아서 (byte 배열로) 보호 해제된 페이로드를 반환합니다. 이 API에는 문자열 기반의 오버로드가 존재하지 않습니다. 마지막 두 가지 out 매개변수는 다음과 같습니다.

  • requiresMigration: 페이로드를 보호하는데 사용된 키가 더 이상 활성화 된 기본 키가 아닌 경우 true로 설정됩니다 (페이로드를 보호하는데 사용된 키가 오래되어 키 롤링 작업이 발생한 경우 등). 업무 규칙에 따라서는 호출자가 페이로드를 다시 보호해야 하는 경우도 있습니다.

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

주의

DangerousUnprotect 메서드를 호출할 때 ignoreRevocationErrors 매개변수를 true로 전달하는 경우에는 특히 주의하시기 바랍니다. 만약 이 메서드를 호출한 후 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
 */