ASP.NET Core でのキーが取り消されたペイロードの保護の解除

ASP.NET Core データ保護 API は、機密ペイロードを無期限に永続化させることを主な目的としていません。 Windows CNG DPAPI および Azure Rights Management などの他のテクノロジの方が、無期限のストレージのシナリオに適しています。それらのテクノロジは、それに対応した強力なキー管理機能を備えています。 とはいえ、開発者が機密データを長期的に保護するために ASP.NET Core データ保護 API を使用できないわけではありません。 キーがキー リングから削除されることはありません。そのため、IDataProtector.Unprotect を使用すれば、キーが使用可能で有効である限り、いつでも既存のペイロードを回復できます。

ただし、開発者が失効したキーで保護されているデータの保護を解除しようとすると、問題が発生します。この場合、IDataProtector.Unprotect によって例外がスローされます。 短期または一時的なペイロード (認証トークンなど) の場合、システムで簡単に再作成できるため、これで問題ありません。最悪でも、サイト訪問者が再ログインを要求される可能性があるという程度です。 しかし、永続化されたペイロードの場合、Unprotect によってスローされると、許容できないデータ損失が発生する可能性があります。

IPersistedDataProtector

キーが失効している場合でもペイロードの保護を解除できるようにするには、データ保護システムに IPersistedDataProtector タイプを含めます。 IPersistedDataProtector のインスタンスを取得するには、IDataProtector のインスタンスを通常の方法で取得し、IDataProtectorIPersistedDataProtectorへのキャストを試行してみてください。

注意

すべての IDataProtector インスタンスを IPersistedDataProtector にキャストできるわけではありません。 開発者は、無効なキャストによって発生するランタイムの例外を回避するために、オペレーターとして C# またはそれに類似したものを使用する必要があります。また、エラー ケースを適切に処理する準備をしておく必要があります。

IPersistedDataProtector は、次の API サーフェスを公開します。

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

この API は、保護されたペイロードを (バイト配列として) 受け取り、保護されていないペイロードを返します。 文字列ベースのオーバーロードはありません。 2 つの out パラメーターは次のとおりです。

  • requiresMigration: このペイロードの保護に使用されるキーがアクティブな既定のキーでなくなった場合 (このペイロードの保護に使用されるキーが古く、キーのローリング操作が行われるなど)、true に設定されます。 呼び出し元は、ビジネス ニーズに応じてペイロードの再保護を検討する必要があるかもしれません。

  • wasRevoked: このペイロードの保護に使用されたキーが取り消された場合、true に設定されます。

警告

ignoreRevocationErrors: trueDangerousUnprotect メソッドに渡す場合は、細心の注意が必要です。 このメソッドを呼び出した後に 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
 */