From eea8c1a146a1d32c91412a07e41d1844caa2baa3 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Fri, 16 Feb 2018 11:54:13 -0800 Subject: [PATCH] Support decrypting keys with X509Certificate that is not in the X509Store The default implementation of EncryptedXml doesn't support using the RSA key from X509Certificate to decrypt xml unless that cert is in the X509 CurrentUser\My or Localmachine\My store. This adds support for decrypting with the X509Certificate directly. This is useful for Linux (often Docker) scenarios, where the user already has a .pfx file, but may not have added it to X509Store. --- .../DataProtectionBuilderExtensions.cs | 2 + .../XmlEncryption/EncryptedXmlDecryptor.cs | 93 +++++++++++++++++- .../XmlEncryption/XmlKeyDecryptionOptions.cs | 27 +++++ test/CreateTestCert.ps1 | 14 +++ .../DataProtectionProviderTests.cs | 54 ++++++---- ...Core.DataProtection.Extensions.Test.csproj | 1 + .../TestFiles/TestCert2.pfx | Bin 0 -> 2670 bytes .../KeyManagement/XmlKeyManagerTests.cs | 4 +- ...soft.AspNetCore.DataProtection.Test.csproj | 1 + .../TestFiles/TestCert1.PublicKeyOnly.cer | Bin 0 -> 796 bytes .../TestFiles/TestCert1.pfx | Bin 0 -> 2670 bytes .../TestFiles/TestCert2.pfx | Bin 0 -> 2662 bytes .../CertificateXmlEncryptionTests.cs | 1 - .../EncryptedXmlDecryptorTests.cs | 91 +++++++++++++++++ 14 files changed, 266 insertions(+), 22 deletions(-) create mode 100644 src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlKeyDecryptionOptions.cs create mode 100644 test/CreateTestCert.ps1 create mode 100644 test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert2.pfx create mode 100644 test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.PublicKeyOnly.cer create mode 100644 test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.pfx create mode 100644 test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert2.pfx create mode 100644 test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/EncryptedXmlDecryptorTests.cs diff --git a/src/Microsoft.AspNetCore.DataProtection/DataProtectionBuilderExtensions.cs b/src/Microsoft.AspNetCore.DataProtection/DataProtectionBuilderExtensions.cs index ec1d1136dd..f37dab4331 100644 --- a/src/Microsoft.AspNetCore.DataProtection/DataProtectionBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.DataProtection/DataProtectionBuilderExtensions.cs @@ -274,6 +274,8 @@ namespace Microsoft.AspNetCore.DataProtection }); }); + builder.Services.Configure(o => o.AddKeyDecryptionCertificate(certificate)); + return builder; } diff --git a/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs b/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs index 6bc280900c..e020ac7bb0 100644 --- a/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs +++ b/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs @@ -2,10 +2,14 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.Xml; using System.Xml; using System.Xml.Linq; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.DataProtection.XmlEncryption { @@ -15,6 +19,7 @@ namespace Microsoft.AspNetCore.DataProtection.XmlEncryption public sealed class EncryptedXmlDecryptor : IInternalEncryptedXmlDecryptor, IXmlDecryptor { private readonly IInternalEncryptedXmlDecryptor _decryptor; + private readonly XmlKeyDecryptionOptions _options; /// /// Creates a new instance of an . @@ -31,6 +36,7 @@ namespace Microsoft.AspNetCore.DataProtection.XmlEncryption public EncryptedXmlDecryptor(IServiceProvider services) { _decryptor = services?.GetService() ?? this; + _options = services?.GetService>()?.Value; } /// @@ -57,8 +63,10 @@ namespace Microsoft.AspNetCore.DataProtection.XmlEncryption var elementToDecrypt = (XmlElement)xmlDocument.DocumentElement.FirstChild; // Perform the decryption and update the document in-place. - var encryptedXml = new EncryptedXml(xmlDocument); + var decryptionCerts = _options?.KeyDecryptionCertificates; + var encryptedXml = new EncryptedXmlWithCertificateKeys(decryptionCerts, xmlDocument); _decryptor.PerformPreDecryptionSetup(encryptedXml); + encryptedXml.DecryptDocument(); // Strip the element back off and convert the XmlDocument to an XElement. @@ -69,5 +77,88 @@ namespace Microsoft.AspNetCore.DataProtection.XmlEncryption { // no-op } + + /// + /// Can decrypt the XML key data from an that is not in stored in . + /// + private class EncryptedXmlWithCertificateKeys : EncryptedXml + { + private readonly IReadOnlyDictionary _certificates; + + public EncryptedXmlWithCertificateKeys(IReadOnlyDictionary certificates, XmlDocument document) + : base(document) + { + _certificates = certificates; + } + + public override byte[] DecryptEncryptedKey(EncryptedKey encryptedKey) + { + byte[] key = base.DecryptEncryptedKey(encryptedKey); + if (key != null) + { + return key; + } + + if (_certificates == null || _certificates.Count == 0) + { + return null; + } + + var keyInfoEnum = encryptedKey.KeyInfo?.GetEnumerator(); + if (keyInfoEnum == null) + { + return null; + } + + while (keyInfoEnum.MoveNext()) + { + if (!(keyInfoEnum.Current is KeyInfoX509Data kiX509Data)) + { + continue; + } + + key = GetKeyFromCert(encryptedKey, kiX509Data); + if (key != null) + { + return key; + } + } + + return null; + } + + private byte[] GetKeyFromCert(EncryptedKey encryptedKey, KeyInfoX509Data keyInfo) + { + var certEnum = keyInfo.Certificates?.GetEnumerator(); + if (certEnum == null) + { + return null; + } + + while (certEnum.MoveNext()) + { + if (!(certEnum.Current is X509Certificate2 certInfo)) + { + continue; + } + + if (!_certificates.TryGetValue(certInfo.Thumbprint, out var certificate)) + { + continue; + } + + using (RSA privateKey = certificate.GetRSAPrivateKey()) + { + if (privateKey != null) + { + var useOAEP = encryptedKey.EncryptionMethod?.KeyAlgorithm == XmlEncRSAOAEPUrl; + return DecryptKey(encryptedKey.CipherData.CipherValue, privateKey, useOAEP); + } + } + } + + return null; + } + } } } diff --git a/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlKeyDecryptionOptions.cs b/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlKeyDecryptionOptions.cs new file mode 100644 index 0000000000..01999c224d --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlKeyDecryptionOptions.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.AspNetCore.DataProtection.XmlEncryption +{ + /// + /// Specifies settings for how to decrypt XML keys. + /// + internal class XmlKeyDecryptionOptions + { + private readonly Dictionary _certs = new Dictionary(StringComparer.Ordinal); + + /// + /// A mapping of key thumbprint to the X509Certificate2 + /// + public IReadOnlyDictionary KeyDecryptionCertificates => _certs; + + public void AddKeyDecryptionCertificate(X509Certificate2 certificate) + { + _certs[certificate.Thumbprint] = certificate; + } + } +} diff --git a/test/CreateTestCert.ps1 b/test/CreateTestCert.ps1 new file mode 100644 index 0000000000..a85a040f05 --- /dev/null +++ b/test/CreateTestCert.ps1 @@ -0,0 +1,14 @@ +# +# Generates a new test cert in a .pfx file +# Obviously, don't actually use this to produce production certs +# + +param( + [Parameter(Mandatory = $true)] + $OutFile +) + +$password = ConvertTo-SecureString -Force -AsPlainText -String "password" +$cert = New-SelfSignedCertificate -DnsName "localhost" -CertStoreLocation Cert:\CurrentUser\My\ +Export-PfxCertificate -Cert $cert -Password $password -FilePath $OutFile +Remove-Item "Cert:\CurrentUser\My\$($cert.Thumbprint)" diff --git a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs index fc73e1397d..ad3dbb3a27 100644 --- a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs +++ b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Reflection; using System.Runtime.InteropServices; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.DataProtection.Test.Shared; @@ -155,6 +156,39 @@ namespace Microsoft.AspNetCore.DataProtection }); } + [Fact] + public void System_UsesInMemoryCertificate() + { + var filePath = Path.Combine(GetTestFilesPath(), "TestCert2.pfx"); + var certificate = new X509Certificate2(filePath, "password"); + + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadOnly); + // ensure this cert is not in the x509 store + Assert.Empty(store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, false)); + } + + WithUniqueTempDirectory(directory => + { + // 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"); + 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(); + 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); + }); + } + /// /// Runs a test and cleans up the temp directory afterward. /// @@ -177,24 +211,6 @@ namespace Microsoft.AspNetCore.DataProtection } private static string GetTestFilesPath() - { - var projectName = typeof(DataProtectionProviderTests).GetTypeInfo().Assembly.GetName().Name; - var projectPath = RecursiveFind(projectName, Path.GetFullPath(".")); - - return Path.Combine(projectPath, projectName, "TestFiles"); - } - - private static string RecursiveFind(string path, string start) - { - var test = Path.Combine(start, path); - if (Directory.Exists(test)) - { - return start; - } - else - { - return RecursiveFind(path, new DirectoryInfo(start).Parent.FullName); - } - } + => Path.Combine(AppContext.BaseDirectory, "TestFiles"); } } diff --git a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/Microsoft.AspNetCore.DataProtection.Extensions.Test.csproj b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/Microsoft.AspNetCore.DataProtection.Extensions.Test.csproj index 29cf82928f..16a4f12c98 100644 --- a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/Microsoft.AspNetCore.DataProtection.Extensions.Test.csproj +++ b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/Microsoft.AspNetCore.DataProtection.Extensions.Test.csproj @@ -6,6 +6,7 @@ + diff --git a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert2.pfx b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert2.pfx new file mode 100644 index 0000000000000000000000000000000000000000..4ed9bbe3947198f2158ca74183b849ee4751d631 GIT binary patch literal 2670 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 Rj%Cne6{f;j4cA`=@E;W(z}^4= literal 0 HcmV?d00001 diff --git a/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs b/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs index ba9f21be61..c6a2e068a3 100644 --- a/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs +++ b/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using System.Xml; using System.Xml.Linq; using Microsoft.AspNetCore.Cryptography.Cng; @@ -347,7 +349,7 @@ namespace Microsoft.AspNetCore.DataProtection.KeyManagement 2015-06-01T00:00:00Z - + "; diff --git a/test/Microsoft.AspNetCore.DataProtection.Test/Microsoft.AspNetCore.DataProtection.Test.csproj b/test/Microsoft.AspNetCore.DataProtection.Test/Microsoft.AspNetCore.DataProtection.Test.csproj index 54469e4063..bf45498fbf 100644 --- a/test/Microsoft.AspNetCore.DataProtection.Test/Microsoft.AspNetCore.DataProtection.Test.csproj +++ b/test/Microsoft.AspNetCore.DataProtection.Test/Microsoft.AspNetCore.DataProtection.Test.csproj @@ -7,6 +7,7 @@ + diff --git a/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.PublicKeyOnly.cer b/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.PublicKeyOnly.cer new file mode 100644 index 0000000000000000000000000000000000000000..329c90a83b9e9f8d8d872a622c989a843c34284e GIT binary patch literal 796 zcmXqLVwNyyVq#dp%*4pVB;Y+u=HvT>)|rm{p5^5kE*A`V**LY@JlekVGBR?rG8l*$ z3Ktk<*_Tqawr|*+8qzMM=eaaTHKB7=3f=Rv4pQqHDtJASoMp8IVd_NWd7DEzDK4_ogu2UAVi(#+?VS1jw;oU zZJ&9*g*CQpJv;O7<|SE+TPD0a@T~mG1x|VP>awG}&psTP=<;?)bJz4KQg8OZ+cEcF zY$xaPGL5zQF{iJx`TSWkB}S#5U!GAu=CCUhGb01z;xvO413qAg%knca{%2ufW@24n zAPeHFvWOXouyJU!F|x9*$w0e2HpB!M;Yv52vVL@W`k3^%ShzrC}k z*NodyQ&EI#8gfhkBN`YJj12yV3_mTIeqU>^$UC03r=F~F%XyqZfh!c4cW)JRpkEX z3vX}#nsrKONsO*_MxU2y0KcPo&mHc7Y2FMOhraJ+EtB(Ir8EDPx#YCvx`Dr}_OV=v z<9<9Lf_=tGj?EIDm+Go&E^IQ%~UF7oa9i}&5Wq#6bL7dTZd z-?whQbU0(I@zvi9lNb9wllhvUv29nb{i*4{UdqYsy1w-G^#4)smi&6yac1K|Q`z0U i-q$qN1j)w6PMjh(*XF9h)WWxs%NI9@IjC|iECm2C>^N)y literal 0 HcmV?d00001 diff --git a/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.pfx b/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.pfx new file mode 100644 index 0000000000000000000000000000000000000000..8bf695f1d6df5c48b720bb18859e0519bc6ff03b GIT binary patch literal 2670 zcmZWpXEfZ~9{o=boKG8zM>v5&5G)?SLqT;TN;T?`76b?7;StPG zJmL~rMq=?4!2ejV9Vi~QfyKjC$oNh{{nr)^0tC*(L(j2z=rNX+0`Xs&98L$NOi^ki zo>KY1;Ec;nF zXK)PEJuxLpG@-iY3i+0Vryi(VDOE|}v@>x&pHEsPeCT<8-V}}n|9H5#F)Nc5C?(pX zp{=>P5zWkAKLg9KR0xC_nNQkdJ{ z5w1O8_W1oEJVFW#0ZJn`D^|GJEx)v=C-`4`0d*5xty(nwkX&CBY9#l;SAyisz-@I= zb45$rVuXPmfWq5lH-dda!4(u9akjG0-?<~KH3vqvd*#$Syq2Av`g9t_HQ>i)=* zHxxxqQOfjzi~r@svJ=I<5!Bat#yirqWS}}@-w(d|Fxh*fbNE|#6SkI#c>-fxY1aYB z)EB<ce;%Ez*c%Byhs%_jlre=)gUV+2g8Oa{AlB0~hr@WY;J2&sGo!;>f zl1s~qj%~VON*xmXCTZk7#KsFyKC^%Fnbm7x7K;Xv+d-8y<7sYXVnu z*0ck*PokpbWKwQUq#8mU^P{sc?v_0m0y^R5mQ{hd0 z=5QUW_+xfBr&m6$seCcNFl~g1UA}z)@Ar7^YkayoYu1d43H|v@{2s|$LLhS4!EHx+ zOL%#(=UMuyu6NQ6bHFlLZ^Qa)HK%^c22rLMk-*K}pS+Z3B;0jjxJHY$LAf;P^^>?Y>I7BFW#x0#v*Qn)*B4ALh&jhez=y4%3LRGmgDd zOTZvS9r~SI8&SpyYEPY({q&ViT?5VMWHd+UtwHy0Z>?9n+p^mOjyf>`$(z-$Z!!pt z>@5n!Db7+uxx+7~q>+ZXhSF<{75#9ZomA#p+zPSHT~17oqPErOsYes;!&`EiLp-Ut zue<{#5M!$}_mGTYDIl2M0seL#?|iZ6AlT7YAT0vT|;%bV+woH>JnT*M%LC+^_)p0?SC7LzX3RR{ik0de52Zl(^^=b7~O#kXE z&yM(cYuGyxpHYlW?`Ry41XKO3sg!LF1cn7YP0u{PzJN&BF!s{lbxCzugaQf$0RVut_(x#T= zS;GOhelaWZv?4PpvM&aRl9?!gB}0t7D*{N6VMXSWfbF000u&%92roeo`W3GQIFh5i z0C&Kf414l(?0~^v%%8mGMUG$xG{|SbFUN=Md69L8U%KwEO+RvmE&1+6?sWLy|A$lL z;UPy@JY)|H0?1?I9|!dR#h<-9m%io7c=TuShvFd}`RQC%sZy<~_L(h_T5nl)pWIy| z@q4zE2OOERwn9$XSLHrt0+fCHS zAL8hc3=@3wVAkDz%@?pcoSIv^qATL3XK~}-Dy0?q==8_Ssu_#r%}8748h0--Hv9JV zwX950_(PQX1X@(j0>gqef-G;gp0F)M^B=7(??|KRYK8cPSVq+aY}@^;3&!&QXh+0c z2J_D?zTO^xH^L0>wqPY;EQiC9w7G#r^?XzXhlA^)(U0JbOmo_>WMF45SvhRwNo8Q# z0YkywyyEptl^C%{%OPrWIVbEQary4qz z(JCv9lTBPmwG}K5()OD>Nq#(mWBeM!Fr%eO8EhL z{sz^@LQ_=ZLPQ7gDttz3jnZw0Fv&?51IJw$v&p!!efe2Ao=abRp5yX;!2W)jE@^aT z*0eOkzxR?wYwA4`LR|I7l6p`9R6clf^p#V;JIsX2s+^^NAJGUlIbh+c0Yy%ws<(j^ zs-zOddd_S5UPWECaqcN$T9OAdIWpgOFpD=q?^R1jwF{%)Yq}9Xd)HBM&Gn!84DwS} zXNTCh1mw=Fe#-OKs@88o?DIR4+$cHOAhiN?J-wDg;Yu(5f&H| z3P}GX$dB~L*(n)S^&9@~Rau`pKxzO}rnOO56Tdi|yq)mTT9Q(&K|$H>qNVH0G2M@i zdv)s)naYNZj&qdLIV;BtS=j^yX|)_PpUdk1v!F*_MgpW1mtX_V*{f PMjX121mCd#vjF}BxA3SM literal 0 HcmV?d00001 diff --git a/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert2.pfx b/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert2.pfx new file mode 100644 index 0000000000000000000000000000000000000000..a54c93ba34964934f35d3101e3c0469319437ccf GIT binary patch literal 2662 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 => + decryptor.Decrypt(encryptedXml.EncryptedElement)); + Assert.Equal("Unable to retrieve the decryption key.", ex.Message); + } + + [Fact] + public void ThrowsIfProvidedCertificateDoesNotMatch() + { + var testCert1 = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "TestFiles", "TestCert1.pfx"), "password"); + var testCert2 = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "TestFiles", "TestCert2.pfx"), "password"); + var services = new ServiceCollection() + .Configure(o => o.AddKeyDecryptionCertificate(testCert2)) + .BuildServiceProvider(); + var encryptor = new CertificateXmlEncryptor(testCert1, NullLoggerFactory.Instance); + var data = new XElement("SampleData", "Lorem ipsum"); + var encryptedXml = encryptor.Encrypt(data); + var decryptor = new EncryptedXmlDecryptor(services); + + var ex = Assert.Throws(() => + decryptor.Decrypt(encryptedXml.EncryptedElement)); + Assert.Equal("Unable to retrieve the decryption key.", ex.Message); + } + + [Fact] + public void ThrowsIfProvidedCertificateDoesHavePrivateKey() + { + var fullCert = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "TestFiles", "TestCert1.pfx"), "password"); + var publicKeyOnly = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "TestFiles", "TestCert1.PublicKeyOnly.cer"), ""); + var services = new ServiceCollection() + .Configure(o => o.AddKeyDecryptionCertificate(publicKeyOnly)) + .BuildServiceProvider(); + var encryptor = new CertificateXmlEncryptor(fullCert, NullLoggerFactory.Instance); + var data = new XElement("SampleData", "Lorem ipsum"); + var encryptedXml = encryptor.Encrypt(data); + var decryptor = new EncryptedXmlDecryptor(services); + + var ex = Assert.Throws(() => + decryptor.Decrypt(encryptedXml.EncryptedElement)); + Assert.Equal("Unable to retrieve the decryption key.", ex.Message); + } + + [Fact] + public void XmlCanRoundTrip() + { + var testCert1 = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "TestFiles", "TestCert1.pfx"), "password"); + var testCert2 = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "TestFiles", "TestCert2.pfx"), "password"); + var services = new ServiceCollection() + .Configure(o => + { + o.AddKeyDecryptionCertificate(testCert1); + o.AddKeyDecryptionCertificate(testCert2); + }) + .BuildServiceProvider(); + var encryptor = new CertificateXmlEncryptor(testCert1, NullLoggerFactory.Instance); + var data = new XElement("SampleData", "Lorem ipsum"); + var encryptedXml = encryptor.Encrypt(data); + var decryptor = new EncryptedXmlDecryptor(services); + + var decrypted = decryptor.Decrypt(encryptedXml.EncryptedElement); + + Assert.Equal("SampleData", decrypted.Name); + Assert.Equal("Lorem ipsum", decrypted.Value); + } + } +}