DataProtection 1.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. commit 2af13658fcd601f951ed9ff60446b7a5241efbab
  2. Author: Nate McMaster <[email protected]>
  3. Date: Thu Jul 5 11:31:46 2018 -0700
  4. Unprotect key material with the local cache of certificates before checking the cert store
  5. In some cases, private keys for certificates is not completely available. When attempting to decrypt key material,
  6. this can cause 'CryptographicException: Keyset does not exist'. This changes the order in which key material
  7. decryption looks up private keys to first key the certificate options provided explicitly to the API, and then
  8. falling back to the cert store for decryption keys.
  9. diff --git a/.vscode/launch.json b/.vscode/launch.json
  10. new file mode 100644
  11. index 00000000000..f4fc2e3731e
  12. --- /dev/null
  13. +++ b/.vscode/launch.json
  14. @@ -0,0 +1,10 @@
  15. +{
  16. + "configurations": [
  17. + {
  18. + "name": ".NET Core Attach",
  19. + "type": "coreclr",
  20. + "request": "attach",
  21. + "processId": "${command:pickProcess}"
  22. + }
  23. + ]
  24. +}
  25. diff --git a/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs b/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs
  26. index e020ac7bb00..fee981b2d7c 100644
  27. --- a/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs
  28. +++ b/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs
  29. @@ -2,7 +2,6 @@
  30. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  31. using System;
  32. -using System.Collections.Generic;
  33. using System.Security.Cryptography;
  34. using System.Security.Cryptography.X509Certificates;
  35. using System.Security.Cryptography.Xml;
  36. @@ -63,8 +62,7 @@ namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
  37. var elementToDecrypt = (XmlElement)xmlDocument.DocumentElement.FirstChild;
  38. // Perform the decryption and update the document in-place.
  39. - var decryptionCerts = _options?.KeyDecryptionCertificates;
  40. - var encryptedXml = new EncryptedXmlWithCertificateKeys(decryptionCerts, xmlDocument);
  41. + var encryptedXml = new EncryptedXmlWithCertificateKeys(_options, xmlDocument);
  42. _decryptor.PerformPreDecryptionSetup(encryptedXml);
  43. encryptedXml.DecryptDocument();
  44. @@ -83,48 +81,40 @@ namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
  45. /// </summary>
  46. private class EncryptedXmlWithCertificateKeys : EncryptedXml
  47. {
  48. - private readonly IReadOnlyDictionary<string, X509Certificate2> _certificates;
  49. + private readonly XmlKeyDecryptionOptions _options;
  50. - public EncryptedXmlWithCertificateKeys(IReadOnlyDictionary<string, X509Certificate2> certificates, XmlDocument document)
  51. + public EncryptedXmlWithCertificateKeys(XmlKeyDecryptionOptions options, XmlDocument document)
  52. : base(document)
  53. {
  54. - _certificates = certificates;
  55. + _options = options;
  56. }
  57. public override byte[] DecryptEncryptedKey(EncryptedKey encryptedKey)
  58. {
  59. - byte[] key = base.DecryptEncryptedKey(encryptedKey);
  60. - if (key != null)
  61. + if (_options != null && _options.KeyDecryptionCertificateCount > 0)
  62. {
  63. - return key;
  64. - }
  65. -
  66. - if (_certificates == null || _certificates.Count == 0)
  67. - {
  68. - return null;
  69. - }
  70. -
  71. - var keyInfoEnum = encryptedKey.KeyInfo?.GetEnumerator();
  72. - if (keyInfoEnum == null)
  73. - {
  74. - return null;
  75. - }
  76. -
  77. - while (keyInfoEnum.MoveNext())
  78. - {
  79. - if (!(keyInfoEnum.Current is KeyInfoX509Data kiX509Data))
  80. + var keyInfoEnum = encryptedKey.KeyInfo?.GetEnumerator();
  81. + if (keyInfoEnum == null)
  82. {
  83. - continue;
  84. + return null;
  85. }
  86. - key = GetKeyFromCert(encryptedKey, kiX509Data);
  87. - if (key != null)
  88. + while (keyInfoEnum.MoveNext())
  89. {
  90. - return key;
  91. + if (!(keyInfoEnum.Current is KeyInfoX509Data kiX509Data))
  92. + {
  93. + continue;
  94. + }
  95. +
  96. + byte[] key = GetKeyFromCert(encryptedKey, kiX509Data);
  97. + if (key != null)
  98. + {
  99. + return key;
  100. + }
  101. }
  102. }
  103. - return null;
  104. + return base.DecryptEncryptedKey(encryptedKey);
  105. }
  106. private byte[] GetKeyFromCert(EncryptedKey encryptedKey, KeyInfoX509Data keyInfo)
  107. @@ -142,17 +132,25 @@ namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
  108. continue;
  109. }
  110. - if (!_certificates.TryGetValue(certInfo.Thumbprint, out var certificate))
  111. + if (!_options.TryGetKeyDecryptionCertificates(certInfo, out var keyDecryptionCerts))
  112. {
  113. continue;
  114. }
  115. - using (RSA privateKey = certificate.GetRSAPrivateKey())
  116. + foreach (var keyDecryptionCert in keyDecryptionCerts)
  117. {
  118. - if (privateKey != null)
  119. + if (!keyDecryptionCert.HasPrivateKey)
  120. + {
  121. + continue;
  122. + }
  123. +
  124. + using (RSA privateKey = keyDecryptionCert.GetRSAPrivateKey())
  125. {
  126. - var useOAEP = encryptedKey.EncryptionMethod?.KeyAlgorithm == XmlEncRSAOAEPUrl;
  127. - return DecryptKey(encryptedKey.CipherData.CipherValue, privateKey, useOAEP);
  128. + if (privateKey != null)
  129. + {
  130. + var useOAEP = encryptedKey.EncryptionMethod?.KeyAlgorithm == XmlEncRSAOAEPUrl;
  131. + return DecryptKey(encryptedKey.CipherData.CipherValue, privateKey, useOAEP);
  132. + }
  133. }
  134. }
  135. }
  136. diff --git a/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlKeyDecryptionOptions.cs b/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlKeyDecryptionOptions.cs
  137. index 01999c224db..7da598816f7 100644
  138. --- a/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlKeyDecryptionOptions.cs
  139. +++ b/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlKeyDecryptionOptions.cs
  140. @@ -12,16 +12,28 @@ namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
  141. /// </summary>
  142. internal class XmlKeyDecryptionOptions
  143. {
  144. - private readonly Dictionary<string, X509Certificate2> _certs = new Dictionary<string, X509Certificate2>(StringComparer.Ordinal);
  145. + private readonly Dictionary<string, List<X509Certificate2>> _certs = new Dictionary<string, List<X509Certificate2>>(StringComparer.Ordinal);
  146. - /// <summary>
  147. - /// A mapping of key thumbprint to the X509Certificate2
  148. - /// </summary>
  149. - public IReadOnlyDictionary<string, X509Certificate2> KeyDecryptionCertificates => _certs;
  150. + public int KeyDecryptionCertificateCount => _certs.Count;
  151. +
  152. + public bool TryGetKeyDecryptionCertificates(X509Certificate2 certInfo, out IReadOnlyList<X509Certificate2> keyDecryptionCerts)
  153. + {
  154. + var key = GetKey(certInfo);
  155. + var retVal = _certs.TryGetValue(key, out var keyDecryptionCertsRetVal);
  156. + keyDecryptionCerts = keyDecryptionCertsRetVal;
  157. + return retVal;
  158. + }
  159. public void AddKeyDecryptionCertificate(X509Certificate2 certificate)
  160. {
  161. - _certs[certificate.Thumbprint] = certificate;
  162. + var key = GetKey(certificate);
  163. + if (!_certs.TryGetValue(key, out var certificates))
  164. + {
  165. + certificates = _certs[key] = new List<X509Certificate2>();
  166. + }
  167. + certificates.Add(certificate);
  168. }
  169. +
  170. + private string GetKey(X509Certificate2 cert) => cert.Thumbprint;
  171. }
  172. }
  173. diff --git a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs
  174. index d20332c1e29..a66ebec2e81 100644
  175. --- a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs
  176. +++ b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs
  177. @@ -3,7 +3,6 @@
  178. using System;
  179. using System.IO;
  180. -using System.Reflection;
  181. using System.Runtime.InteropServices;
  182. using System.Security.Cryptography;
  183. using System.Security.Cryptography.X509Certificates;
  184. @@ -13,7 +12,6 @@ using Microsoft.AspNetCore.DataProtection.Repositories;
  185. using Microsoft.AspNetCore.DataProtection.Test.Shared;
  186. using Microsoft.AspNetCore.Testing.xunit;
  187. using Microsoft.Extensions.DependencyInjection;
  188. -using Microsoft.Extensions.Logging;
  189. using Microsoft.Extensions.Logging.Abstractions;
  190. using Microsoft.Extensions.Options;
  191. using Moq;
  192. @@ -120,10 +118,12 @@ namespace Microsoft.AspNetCore.DataProtection
  193. public void System_UsesProvidedDirectoryAndCertificate()
  194. {
  195. var filePath = Path.Combine(GetTestFilesPath(), "TestCert.pfx");
  196. - var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
  197. - store.Open(OpenFlags.ReadWrite);
  198. - store.Add(new X509Certificate2(filePath, "password", X509KeyStorageFlags.Exportable));
  199. - store.Close();
  200. + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
  201. + {
  202. + store.Open(OpenFlags.ReadWrite);
  203. + store.Add(new X509Certificate2(filePath, "password", X509KeyStorageFlags.Exportable));
  204. + store.Close();
  205. + }
  206. WithUniqueTempDirectory(directory =>
  207. {
  208. @@ -139,7 +139,12 @@ namespace Microsoft.AspNetCore.DataProtection
  209. // Step 2: instantiate the system and round-trip a payload
  210. var protector = DataProtectionProvider.Create(directory, certificate).CreateProtector("purpose");
  211. - Assert.Equal("payload", protector.Unprotect(protector.Protect("payload")));
  212. + var data = protector.Protect("payload");
  213. +
  214. + // add a cert without the private key to ensure the decryption will still fallback to the cert store
  215. + var certWithoutKey = new X509Certificate2(Path.Combine(GetTestFilesPath(), "TestCertWithoutPrivateKey.pfx"), "password");
  216. + var unprotector = DataProtectionProvider.Create(directory, o => o.UnprotectKeysWithAnyCertificate(certWithoutKey)).CreateProtector("purpose");
  217. + Assert.Equal("payload", unprotector.Unprotect(data));
  218. // Step 3: validate that there's now a single key in the directory and that it's is protected using the certificate
  219. var allFiles = directory.GetFiles();
  220. @@ -157,6 +162,50 @@ namespace Microsoft.AspNetCore.DataProtection
  221. });
  222. }
  223. + [ConditionalFact]
  224. + [X509StoreIsAvailable(StoreName.My, StoreLocation.CurrentUser)]
  225. + public void System_UsesProvidedCertificateNotFromStore()
  226. + {
  227. + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
  228. + {
  229. + store.Open(OpenFlags.ReadWrite);
  230. + var certWithoutKey = new X509Certificate2(Path.Combine(GetTestFilesPath(), "TestCert3WithoutPrivateKey.pfx"), "password3", X509KeyStorageFlags.Exportable);
  231. + Assert.False(certWithoutKey.HasPrivateKey, "Cert should not have private key");
  232. + store.Add(certWithoutKey);
  233. + store.Close();
  234. + }
  235. +
  236. + WithUniqueTempDirectory(directory =>
  237. + {
  238. + using (var certificateStore = new X509Store(StoreName.My, StoreLocation.CurrentUser))
  239. + {
  240. + certificateStore.Open(OpenFlags.ReadWrite);
  241. + var certInStore = certificateStore.Certificates.Find(X509FindType.FindBySubjectName, "TestCert", false)[0];
  242. + Assert.NotNull(certInStore);
  243. + Assert.False(certInStore.HasPrivateKey);
  244. +
  245. + try
  246. + {
  247. + var certWithKey = new X509Certificate2(Path.Combine(GetTestFilesPath(), "TestCert3.pfx"), "password3");
  248. +
  249. + var protector = DataProtectionProvider.Create(directory, certWithKey).CreateProtector("purpose");
  250. + var data = protector.Protect("payload");
  251. +
  252. + var keylessUnprotector = DataProtectionProvider.Create(directory).CreateProtector("purpose");
  253. + Assert.Throws<CryptographicException>(() => keylessUnprotector.Unprotect(data));
  254. +
  255. + var unprotector = DataProtectionProvider.Create(directory, o => o.UnprotectKeysWithAnyCertificate(certInStore, certWithKey)).CreateProtector("purpose");
  256. + Assert.Equal("payload", unprotector.Unprotect(data));
  257. + }
  258. + finally
  259. + {
  260. + certificateStore.Remove(certInStore);
  261. + certificateStore.Close();
  262. + }
  263. + }
  264. + });
  265. + }
  266. +
  267. [Fact]
  268. public void System_UsesInMemoryCertificate()
  269. {
  270. @@ -242,7 +291,7 @@ namespace Microsoft.AspNetCore.DataProtection
  271. /// </summary>
  272. private static void WithUniqueTempDirectory(Action<DirectoryInfo> testCode)
  273. {
  274. - string uniqueTempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
  275. + string uniqueTempPath = Path.Combine(AppContext.BaseDirectory, Path.GetRandomFileName());
  276. var dirInfo = new DirectoryInfo(uniqueTempPath);
  277. try
  278. {
  279. diff --git a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3.pfx b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3.pfx
  280. new file mode 100644
  281. index 00000000000..364251ba09d
  282. Binary files /dev/null and b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3.pfx differ
  283. diff --git a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3WithoutPrivateKey.pfx b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3WithoutPrivateKey.pfx
  284. new file mode 100644
  285. index 00000000000..9776e9006d7
  286. Binary files /dev/null and b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3WithoutPrivateKey.pfx differ
  287. diff --git a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCertWithoutPrivateKey.pfx b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCertWithoutPrivateKey.pfx
  288. new file mode 100644
  289. index 00000000000..812374c50c3
  290. Binary files /dev/null and b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCertWithoutPrivateKey.pfx differ