Merge branch 'release/2.2'

This commit is contained in:
Nate McMaster 2018-07-05 11:33:09 -07:00
commit 3c558069f4
No known key found for this signature in database
GPG Key ID: A778D9601BD78810
7 changed files with 118 additions and 49 deletions

10
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"configurations": [
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}

View File

@ -2,7 +2,6 @@
// 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;
@ -63,8 +62,7 @@ namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
var elementToDecrypt = (XmlElement)xmlDocument.DocumentElement.FirstChild;
// Perform the decryption and update the document in-place.
var decryptionCerts = _options?.KeyDecryptionCertificates;
var encryptedXml = new EncryptedXmlWithCertificateKeys(decryptionCerts, xmlDocument);
var encryptedXml = new EncryptedXmlWithCertificateKeys(_options, xmlDocument);
_decryptor.PerformPreDecryptionSetup(encryptedXml);
encryptedXml.DecryptDocument();
@ -83,48 +81,40 @@ namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
/// </summary>
private class EncryptedXmlWithCertificateKeys : EncryptedXml
{
private readonly IReadOnlyDictionary<string, X509Certificate2> _certificates;
private readonly XmlKeyDecryptionOptions _options;
public EncryptedXmlWithCertificateKeys(IReadOnlyDictionary<string, X509Certificate2> certificates, XmlDocument document)
public EncryptedXmlWithCertificateKeys(XmlKeyDecryptionOptions options, XmlDocument document)
: base(document)
{
_certificates = certificates;
_options = options;
}
public override byte[] DecryptEncryptedKey(EncryptedKey encryptedKey)
{
byte[] key = base.DecryptEncryptedKey(encryptedKey);
if (key != null)
if (_options != null && _options.KeyDecryptionCertificateCount > 0)
{
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))
var keyInfoEnum = encryptedKey.KeyInfo?.GetEnumerator();
if (keyInfoEnum == null)
{
continue;
return null;
}
key = GetKeyFromCert(encryptedKey, kiX509Data);
if (key != null)
while (keyInfoEnum.MoveNext())
{
return key;
if (!(keyInfoEnum.Current is KeyInfoX509Data kiX509Data))
{
continue;
}
byte[] key = GetKeyFromCert(encryptedKey, kiX509Data);
if (key != null)
{
return key;
}
}
}
return null;
return base.DecryptEncryptedKey(encryptedKey);
}
private byte[] GetKeyFromCert(EncryptedKey encryptedKey, KeyInfoX509Data keyInfo)
@ -142,17 +132,25 @@ namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
continue;
}
if (!_certificates.TryGetValue(certInfo.Thumbprint, out var certificate))
if (!_options.TryGetKeyDecryptionCertificates(certInfo, out var keyDecryptionCerts))
{
continue;
}
using (RSA privateKey = certificate.GetRSAPrivateKey())
foreach (var keyDecryptionCert in keyDecryptionCerts)
{
if (privateKey != null)
if (!keyDecryptionCert.HasPrivateKey)
{
var useOAEP = encryptedKey.EncryptionMethod?.KeyAlgorithm == XmlEncRSAOAEPUrl;
return DecryptKey(encryptedKey.CipherData.CipherValue, privateKey, useOAEP);
continue;
}
using (RSA privateKey = keyDecryptionCert.GetRSAPrivateKey())
{
if (privateKey != null)
{
var useOAEP = encryptedKey.EncryptionMethod?.KeyAlgorithm == XmlEncRSAOAEPUrl;
return DecryptKey(encryptedKey.CipherData.CipherValue, privateKey, useOAEP);
}
}
}
}

View File

@ -12,16 +12,28 @@ namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
/// </summary>
internal class XmlKeyDecryptionOptions
{
private readonly Dictionary<string, X509Certificate2> _certs = new Dictionary<string, X509Certificate2>(StringComparer.Ordinal);
private readonly Dictionary<string, List<X509Certificate2>> _certs = new Dictionary<string, List<X509Certificate2>>(StringComparer.Ordinal);
/// <summary>
/// A mapping of key thumbprint to the X509Certificate2
/// </summary>
public IReadOnlyDictionary<string, X509Certificate2> KeyDecryptionCertificates => _certs;
public int KeyDecryptionCertificateCount => _certs.Count;
public bool TryGetKeyDecryptionCertificates(X509Certificate2 certInfo, out IReadOnlyList<X509Certificate2> keyDecryptionCerts)
{
var key = GetKey(certInfo);
var retVal = _certs.TryGetValue(key, out var keyDecryptionCertsRetVal);
keyDecryptionCerts = keyDecryptionCertsRetVal;
return retVal;
}
public void AddKeyDecryptionCertificate(X509Certificate2 certificate)
{
_certs[certificate.Thumbprint] = certificate;
var key = GetKey(certificate);
if (!_certs.TryGetValue(key, out var certificates))
{
certificates = _certs[key] = new List<X509Certificate2>();
}
certificates.Add(certificate);
}
private string GetKey(X509Certificate2 cert) => cert.Thumbprint;
}
}

View File

@ -3,7 +3,6 @@
using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
@ -13,7 +12,6 @@ using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.DataProtection.Test.Shared;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
@ -120,10 +118,12 @@ namespace Microsoft.AspNetCore.DataProtection
public void System_UsesProvidedDirectoryAndCertificate()
{
var filePath = Path.Combine(GetTestFilesPath(), "TestCert.pfx");
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
store.Add(new X509Certificate2(filePath, "password", X509KeyStorageFlags.Exportable));
store.Close();
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadWrite);
store.Add(new X509Certificate2(filePath, "password", X509KeyStorageFlags.Exportable));
store.Close();
}
WithUniqueTempDirectory(directory =>
{
@ -139,7 +139,12 @@ namespace Microsoft.AspNetCore.DataProtection
// 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")));
var data = protector.Protect("payload");
// add a cert without the private key to ensure the decryption will still fallback to the cert store
var certWithoutKey = new X509Certificate2(Path.Combine(GetTestFilesPath(), "TestCertWithoutPrivateKey.pfx"), "password");
var unprotector = DataProtectionProvider.Create(directory, o => o.UnprotectKeysWithAnyCertificate(certWithoutKey)).CreateProtector("purpose");
Assert.Equal("payload", unprotector.Unprotect(data));
// 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();
@ -157,6 +162,50 @@ namespace Microsoft.AspNetCore.DataProtection
});
}
[ConditionalFact]
[X509StoreIsAvailable(StoreName.My, StoreLocation.CurrentUser)]
public void System_UsesProvidedCertificateNotFromStore()
{
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadWrite);
var certWithoutKey = new X509Certificate2(Path.Combine(GetTestFilesPath(), "TestCert3WithoutPrivateKey.pfx"), "password3", X509KeyStorageFlags.Exportable);
Assert.False(certWithoutKey.HasPrivateKey, "Cert should not have private key");
store.Add(certWithoutKey);
store.Close();
}
WithUniqueTempDirectory(directory =>
{
using (var certificateStore = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
certificateStore.Open(OpenFlags.ReadWrite);
var certInStore = certificateStore.Certificates.Find(X509FindType.FindBySubjectName, "TestCert", false)[0];
Assert.NotNull(certInStore);
Assert.False(certInStore.HasPrivateKey);
try
{
var certWithKey = new X509Certificate2(Path.Combine(GetTestFilesPath(), "TestCert3.pfx"), "password3");
var protector = DataProtectionProvider.Create(directory, certWithKey).CreateProtector("purpose");
var data = protector.Protect("payload");
var keylessUnprotector = DataProtectionProvider.Create(directory).CreateProtector("purpose");
Assert.Throws<CryptographicException>(() => keylessUnprotector.Unprotect(data));
var unprotector = DataProtectionProvider.Create(directory, o => o.UnprotectKeysWithAnyCertificate(certInStore, certWithKey)).CreateProtector("purpose");
Assert.Equal("payload", unprotector.Unprotect(data));
}
finally
{
certificateStore.Remove(certInStore);
certificateStore.Close();
}
}
});
}
[Fact]
public void System_UsesInMemoryCertificate()
{
@ -242,7 +291,7 @@ namespace Microsoft.AspNetCore.DataProtection
/// </summary>
private static void WithUniqueTempDirectory(Action<DirectoryInfo> testCode)
{
string uniqueTempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
string uniqueTempPath = Path.Combine(AppContext.BaseDirectory, Path.GetRandomFileName());
var dirInfo = new DirectoryInfo(uniqueTempPath);
try
{