diff --git a/src/Microsoft.AspNetCore.DataProtection/DataProtectionBuilderExtensions.cs b/src/Microsoft.AspNetCore.DataProtection/DataProtectionBuilderExtensions.cs index f37dab4331..7789ca074f 100644 --- a/src/Microsoft.AspNetCore.DataProtection/DataProtectionBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.DataProtection/DataProtectionBuilderExtensions.cs @@ -320,6 +320,33 @@ namespace Microsoft.AspNetCore.DataProtection return builder; } + /// + /// Configures certificates which can be used to decrypt keys loaded from storage. + /// + /// The . + /// Certificates that can be used to decrypt key data. + /// A reference to the after this operation has completed. + public static IDataProtectionBuilder UnprotectKeysWithAnyCertificate(this IDataProtectionBuilder builder, params X509Certificate2[] certificates) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Services.Configure(o => + { + if (certificates != null) + { + foreach (var certificate in certificates) + { + o.AddKeyDecryptionCertificate(certificate); + } + } + }); + + return builder; + } + /// /// Configures keys to be encrypted with Windows DPAPI before being persisted to /// storage. The encrypted key will only be decryptable by the current Windows user account. diff --git a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs index ad3dbb3a27..63931f2f8f 100644 --- a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs +++ b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs @@ -171,16 +171,16 @@ namespace Microsoft.AspNetCore.DataProtection WithUniqueTempDirectory(directory => { - // Step 1: directory should be completely empty - directory.Create(); + // Step 1: directory should be completely empty + directory.Create(); Assert.Empty(directory.GetFiles()); - // Step 2: instantiate the system and round-trip a payload - var protector = DataProtectionProvider.Create(directory, certificate).CreateProtector("purpose"); + // Step 2: instantiate the system and round-trip a payload + var protector = DataProtectionProvider.Create(directory, certificate).CreateProtector("purpose"); Assert.Equal("payload", protector.Unprotect(protector.Protect("payload"))); - // Step 3: validate that there's now a single key in the directory and that it's is protected using the certificate - var allFiles = directory.GetFiles(); + // Step 3: validate that there's now a single key in the directory and that it's is protected using the certificate + var allFiles = directory.GetFiles(); Assert.Single(allFiles); Assert.StartsWith("key-", allFiles[0].Name, StringComparison.OrdinalIgnoreCase); string fileText = File.ReadAllText(allFiles[0].FullName); @@ -189,6 +189,40 @@ namespace Microsoft.AspNetCore.DataProtection }); } + [Fact] + public void System_CanUnprotectWithCert() + { + var filePath = Path.Combine(GetTestFilesPath(), "TestCert2.pfx"); + var certificate = new X509Certificate2(filePath, "password"); + + WithUniqueTempDirectory(directory => + { + // Step 1: directory should be completely empty + directory.Create(); + Assert.Empty(directory.GetFiles()); + + // Step 2: instantiate the system and create some data + var protector = DataProtectionProvider + .Create(directory, certificate) + .CreateProtector("purpose"); + + var data = protector.Protect("payload"); + + // Step 3: validate that there's now a single key in the directory and that it's is protected using the certificate + var allFiles = directory.GetFiles(); + Assert.Single(allFiles); + Assert.StartsWith("key-", allFiles[0].Name, StringComparison.OrdinalIgnoreCase); + string fileText = File.ReadAllText(allFiles[0].FullName); + Assert.DoesNotContain("Warning: the key below is in an unencrypted form.", fileText, StringComparison.Ordinal); + Assert.Contains("X509Certificate", fileText, StringComparison.Ordinal); + + // Step 4: setup a second system and validate it can decrypt keys and unprotect data + var unprotector = DataProtectionProvider.Create(directory, + b => b.UnprotectKeysWithAnyCertificate(certificate)); + Assert.Equal("payload", unprotector.CreateProtector("purpose").Unprotect(data)); + }); + } + /// /// Runs a test and cleans up the temp directory afterward. ///