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
のインスタンスを通常の方法で取得し、IDataProtector
の IPersistedDataProtector
へのキャストを試行してみてください。
注意
すべての 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: 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
*/
ASP.NET Core