// 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.Linq;
using System.Xml;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNetCore.DataProtection.Internal;
using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.DataProtection.XmlEncryption;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.DataProtection.KeyManagement
{
public class XmlKeyManagerTests
{
private static readonly XElement serializedDescriptor = XElement.Parse(@"
");
[Fact]
public void Ctor_WithoutEncryptorOrRepository_UsesFallback()
{
// Arrange
var expectedEncryptor = new Mock().Object;
var expectedRepository = new Mock().Object;
var mockFallback = new Mock();
mockFallback.Setup(o => o.GetKeyEncryptor()).Returns(expectedEncryptor);
mockFallback.Setup(o => o.GetKeyRepository()).Returns(expectedRepository);
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(mockFallback.Object);
serviceCollection.AddSingleton(new Mock().Object);
var services = serviceCollection.BuildServiceProvider();
// Act
var keyManager = new XmlKeyManager(services);
// Assert
Assert.Same(expectedEncryptor, keyManager.KeyEncryptor);
Assert.Same(expectedRepository, keyManager.KeyRepository);
}
[Fact]
public void Ctor_WithEncryptorButNoRepository_IgnoresFallback_FailsWithServiceNotFound()
{
// Arrange
var mockFallback = new Mock();
mockFallback.Setup(o => o.GetKeyEncryptor()).Returns(new Mock().Object);
mockFallback.Setup(o => o.GetKeyRepository()).Returns(new Mock().Object);
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(mockFallback.Object);
serviceCollection.AddSingleton(new Mock().Object);
serviceCollection.AddSingleton(new Mock().Object);
var services = serviceCollection.BuildServiceProvider();
// Act & assert - we don't care about exception type, only exception message
Exception ex = Assert.ThrowsAny(() => new XmlKeyManager(services));
Assert.Contains("IXmlRepository", ex.Message);
}
[Fact]
public void CreateNewKey_Internal_NoEscrowOrEncryption()
{
// Constants
var creationDate = new DateTimeOffset(2014, 01, 01, 0, 0, 0, TimeSpan.Zero);
var activationDate = new DateTimeOffset(2014, 02, 01, 0, 0, 0, TimeSpan.Zero);
var expirationDate = new DateTimeOffset(2014, 03, 01, 0, 0, 0, TimeSpan.Zero);
var keyId = new Guid("3d6d01fd-c0e7-44ae-82dd-013b996b4093");
// Arrange - mocks
XElement elementStoredInRepository = null;
string friendlyNameStoredInRepository = null;
var expectedAuthenticatedEncryptor = new Mock().Object;
var mockDescriptor = new Mock();
mockDescriptor.Setup(o => o.ExportToXml()).Returns(new XmlSerializedDescriptorInfo(serializedDescriptor, typeof(MyDeserializer)));
mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(expectedAuthenticatedEncryptor);
var mockConfiguration = new Mock();
mockConfiguration.Setup(o => o.CreateNewDescriptor()).Returns(mockDescriptor.Object);
var mockXmlRepository = new Mock();
mockXmlRepository
.Setup(o => o.StoreElement(It.IsAny(), It.IsAny()))
.Callback((el, friendlyName) =>
{
elementStoredInRepository = el;
friendlyNameStoredInRepository = friendlyName;
});
// Arrange - services
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(mockXmlRepository.Object);
serviceCollection.AddSingleton(mockConfiguration.Object);
var services = serviceCollection.BuildServiceProvider();
var keyManager = new XmlKeyManager(services);
// Act & assert
// The cancellation token should not already be fired
var firstCancellationToken = keyManager.GetCacheExpirationToken();
Assert.False(firstCancellationToken.IsCancellationRequested);
// After the call to CreateNewKey, the first CT should be fired,
// and we should've gotten a new CT.
var newKey = ((IInternalXmlKeyManager)keyManager).CreateNewKey(
keyId: keyId,
creationDate: creationDate,
activationDate: activationDate,
expirationDate: expirationDate);
var secondCancellationToken = keyManager.GetCacheExpirationToken();
Assert.True(firstCancellationToken.IsCancellationRequested);
Assert.False(secondCancellationToken.IsCancellationRequested);
// Does the IKey have the properties we requested?
Assert.Equal(keyId, newKey.KeyId);
Assert.Equal(creationDate, newKey.CreationDate);
Assert.Equal(activationDate, newKey.ActivationDate);
Assert.Equal(expirationDate, newKey.ExpirationDate);
Assert.False(newKey.IsRevoked);
Assert.Same(expectedAuthenticatedEncryptor, newKey.CreateEncryptorInstance());
// Finally, was the correct element stored in the repository?
string expectedXml = String.Format(@"
{1}
{2}
{3}
",
typeof(MyDeserializer).AssemblyQualifiedName,
new XElement("creationDate", creationDate),
new XElement("activationDate", activationDate),
new XElement("expirationDate", expirationDate));
XmlAssert.Equal(expectedXml, elementStoredInRepository);
Assert.Equal("key-3d6d01fd-c0e7-44ae-82dd-013b996b4093", friendlyNameStoredInRepository);
}
[Fact]
public void CreateNewKey_Internal_WithEscrowAndEncryption()
{
// Constants
var creationDate = new DateTimeOffset(2014, 01, 01, 0, 0, 0, TimeSpan.Zero);
var activationDate = new DateTimeOffset(2014, 02, 01, 0, 0, 0, TimeSpan.Zero);
var expirationDate = new DateTimeOffset(2014, 03, 01, 0, 0, 0, TimeSpan.Zero);
var keyId = new Guid("3d6d01fd-c0e7-44ae-82dd-013b996b4093");
// Arrange - mocks
XElement elementStoredInEscrow = null;
Guid? keyIdStoredInEscrow = null;
XElement elementStoredInRepository = null;
string friendlyNameStoredInRepository = null;
var expectedAuthenticatedEncryptor = new Mock().Object;
var mockDescriptor = new Mock();
mockDescriptor.Setup(o => o.ExportToXml()).Returns(new XmlSerializedDescriptorInfo(serializedDescriptor, typeof(MyDeserializer)));
mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(expectedAuthenticatedEncryptor);
var mockConfiguration = new Mock();
mockConfiguration.Setup(o => o.CreateNewDescriptor()).Returns(mockDescriptor.Object);
var mockXmlRepository = new Mock();
mockXmlRepository
.Setup(o => o.StoreElement(It.IsAny(), It.IsAny()))
.Callback((el, friendlyName) =>
{
elementStoredInRepository = el;
friendlyNameStoredInRepository = friendlyName;
});
var mockKeyEscrow = new Mock();
mockKeyEscrow
.Setup(o => o.Store(It.IsAny(), It.IsAny()))
.Callback((innerKeyId, el) =>
{
keyIdStoredInEscrow = innerKeyId;
elementStoredInEscrow = el;
});
// Arrange - services
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(mockXmlRepository.Object);
serviceCollection.AddSingleton(mockConfiguration.Object);
serviceCollection.AddSingleton(mockKeyEscrow.Object);
serviceCollection.AddSingleton();
var services = serviceCollection.BuildServiceProvider();
var keyManager = new XmlKeyManager(services);
// Act & assert
// The cancellation token should not already be fired
var firstCancellationToken = keyManager.GetCacheExpirationToken();
Assert.False(firstCancellationToken.IsCancellationRequested);
// After the call to CreateNewKey, the first CT should be fired,
// and we should've gotten a new CT.
var newKey = ((IInternalXmlKeyManager)keyManager).CreateNewKey(
keyId: keyId,
creationDate: creationDate,
activationDate: activationDate,
expirationDate: expirationDate);
var secondCancellationToken = keyManager.GetCacheExpirationToken();
Assert.True(firstCancellationToken.IsCancellationRequested);
Assert.False(secondCancellationToken.IsCancellationRequested);
// Does the IKey have the properties we requested?
Assert.Equal(keyId, newKey.KeyId);
Assert.Equal(creationDate, newKey.CreationDate);
Assert.Equal(activationDate, newKey.ActivationDate);
Assert.Equal(expirationDate, newKey.ExpirationDate);
Assert.False(newKey.IsRevoked);
Assert.Same(expectedAuthenticatedEncryptor, newKey.CreateEncryptorInstance());
// Was the correct element stored in escrow?
// This should not have gone through the encryptor.
string expectedEscrowXml = String.Format(@"
{1}
{2}
{3}
",
typeof(MyDeserializer).AssemblyQualifiedName,
new XElement("creationDate", creationDate),
new XElement("activationDate", activationDate),
new XElement("expirationDate", expirationDate));
XmlAssert.Equal(expectedEscrowXml, elementStoredInEscrow);
Assert.Equal(keyId, keyIdStoredInEscrow.Value);
// Finally, was the correct element stored in the repository?
// This should have gone through the encryptor (which we set to be the null encryptor in this test)
string expectedRepositoryXml = String.Format(@"
{2}
{3}
{4}
",
typeof(MyDeserializer).AssemblyQualifiedName,
typeof(NullXmlDecryptor).AssemblyQualifiedName,
new XElement("creationDate", creationDate),
new XElement("activationDate", activationDate),
new XElement("expirationDate", expirationDate));
XmlAssert.Equal(expectedRepositoryXml, elementStoredInRepository);
Assert.Equal("key-3d6d01fd-c0e7-44ae-82dd-013b996b4093", friendlyNameStoredInRepository);
}
[Fact]
public void CreateNewKey_CallsInternalManager()
{
// Arrange - mocks
DateTimeOffset minCreationDate = DateTimeOffset.UtcNow;
DateTimeOffset? actualCreationDate = null;
DateTimeOffset activationDate = minCreationDate + TimeSpan.FromDays(7);
DateTimeOffset expirationDate = activationDate.AddMonths(1);
var mockInternalKeyManager = new Mock();
mockInternalKeyManager
.Setup(o => o.CreateNewKey(It.IsAny(), It.IsAny(), activationDate, expirationDate))
.Callback((innerKeyId, innerCreationDate, innerActivationDate, innerExpirationDate) =>
{
actualCreationDate = innerCreationDate;
});
// Arrange - services
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(new Mock().Object);
serviceCollection.AddSingleton(new Mock().Object);
serviceCollection.AddSingleton(mockInternalKeyManager.Object);
var services = serviceCollection.BuildServiceProvider();
var keyManager = new XmlKeyManager(services);
// Act
keyManager.CreateNewKey(activationDate, expirationDate);
// Assert
Assert.InRange(actualCreationDate.Value, minCreationDate, DateTimeOffset.UtcNow);
}
[Fact]
public void GetAllKeys_Empty()
{
// Arrange
const string xml = @"";
var activator = new Mock().Object;
// Act
var keys = RunGetAllKeysCore(xml, activator);
// Assert
Assert.Equal(0, keys.Count);
}
[Fact]
public void GetAllKeys_IgnoresUnknownElements()
{
// Arrange
const string xml = @"
2015-01-01T00:00:00Z
2015-02-01T00:00:00Z
2015-03-01T00:00:00Z
2015-04-01T00:00:00Z
2015-05-01T00:00:00Z
2015-06-01T00:00:00Z
";
var encryptorA = new Mock().Object;
var encryptorB = new Mock().Object;
var mockActivator = new Mock();
mockActivator.ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput("deserializer-A", "", encryptorA);
mockActivator.ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput("deserializer-B", "", encryptorB);
// Act
var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray();
// Assert
Assert.Equal(2, keys.Length);
Assert.Equal(new Guid("62a72ad9-42d7-4e97-b3fa-05bad5d53d33"), keys[0].KeyId);
Assert.Equal(XmlConvert.ToDateTimeOffset("2015-01-01T00:00:00Z"), keys[0].CreationDate);
Assert.Equal(XmlConvert.ToDateTimeOffset("2015-02-01T00:00:00Z"), keys[0].ActivationDate);
Assert.Equal(XmlConvert.ToDateTimeOffset("2015-03-01T00:00:00Z"), keys[0].ExpirationDate);
Assert.False(keys[0].IsRevoked);
Assert.Same(encryptorA, keys[0].CreateEncryptorInstance());
Assert.Equal(new Guid("041be4c0-52d7-48b4-8d32-f8c0ff315459"), keys[1].KeyId);
Assert.Equal(XmlConvert.ToDateTimeOffset("2015-04-01T00:00:00Z"), keys[1].CreationDate);
Assert.Equal(XmlConvert.ToDateTimeOffset("2015-05-01T00:00:00Z"), keys[1].ActivationDate);
Assert.Equal(XmlConvert.ToDateTimeOffset("2015-06-01T00:00:00Z"), keys[1].ExpirationDate);
Assert.False(keys[1].IsRevoked);
Assert.Same(encryptorB, keys[1].CreateEncryptorInstance());
}
[Fact]
public void GetAllKeys_UnderstandsRevocations()
{
// Arrange
const string xml = @"
2015-01-01T00:00:00Z
2015-02-01T00:00:00Z
2015-03-01T00:00:00Z
2016-01-01T00:00:00Z
2016-02-01T00:00:00Z
2016-03-01T00:00:00Z
2017-01-01T00:00:00Z
2017-02-01T00:00:00Z
2017-03-01T00:00:00Z
2018-01-01T00:00:00Z
2018-02-01T00:00:00Z
2018-03-01T00:00:00Z
2014-01-01T00:00:00Z
2017-01-01T00:00:00Z
2020-01-01T00:00:00Z
";
var mockActivator = new Mock();
mockActivator.ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput("theDeserializer", "", new Mock().Object);
// Act
var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray();
// Assert
Assert.Equal(4, keys.Length);
Assert.Equal(new Guid("67f9cdea-83ba-41ed-b160-2b1d0ea30251"), keys[0].KeyId);
Assert.True(keys[0].IsRevoked);
Assert.Equal(new Guid("0cf83742-d175-42a8-94b5-1ec049b354c3"), keys[1].KeyId);
Assert.True(keys[1].IsRevoked);
Assert.Equal(new Guid("21580ac4-c83a-493c-bde6-29a1cc97ca0f"), keys[2].KeyId);
Assert.False(keys[2].IsRevoked);
Assert.Equal(new Guid("6bd14f12-0bb8-4822-91d7-04b360de0497"), keys[3].KeyId);
Assert.True(keys[3].IsRevoked);
}
[Fact]
public void GetAllKeys_PerformsDecryption()
{
// Arrange
const string xml = @"
2015-01-01T00:00:00Z
2015-02-01T00:00:00Z
2015-03-01T00:00:00Z
";
var expectedEncryptor = new Mock().Object;
var mockActivator = new Mock();
mockActivator.ReturnDecryptedElementGivenDecryptorTypeNameAndInput("theDecryptor", "", "");
mockActivator.ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput("theDeserializer", "", expectedEncryptor);
// Act
var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray();
// Assert
Assert.Equal(1, keys.Length);
Assert.Equal(new Guid("09712588-ba68-438a-a5ee-fe842b3453b2"), keys[0].KeyId);
Assert.Same(expectedEncryptor, keys[0].CreateEncryptorInstance());
}
[Fact]
public void GetAllKeys_SwallowsKeyDeserializationErrors()
{
// Arrange
const string xml = @"
2015-01-01T00:00:00Z
2015-02-01T00:00:00Z
NOT A VALID DATE
2015-01-01T00:00:00Z
2015-02-01T00:00:00Z
2015-03-01T00:00:00Z
";
var expectedEncryptor = new Mock().Object;
var mockActivator = new Mock();
mockActivator.ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput("goodDeserializer", "", expectedEncryptor);
// Act
var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray();
// Assert
Assert.Equal(1, keys.Length);
Assert.Equal(new Guid("49c0cda9-0232-4d8c-a541-de20cc5a73d6"), keys[0].KeyId);
Assert.Same(expectedEncryptor, keys[0].CreateEncryptorInstance());
}
[Fact]
public void GetAllKeys_WithKeyDeserializationError_LogLevelDebug_DoesNotWriteSensitiveInformation()
{
// Arrange
const string xml = @"
2015-01-01T00:00:00Z
2015-02-01T00:00:00Z
NOT A VALID DATE
";
var loggerFactory = new StringLoggerFactory(LogLevel.Debug);
// Act
RunGetAllKeysCore(xml, new Mock().Object, loggerFactory).ToArray();
// Assert
Assert.False(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should not have been logged.");
}
[Fact]
public void GetAllKeys_WithKeyDeserializationError_LogLevelTrace_WritesSensitiveInformation()
{
// Arrange
const string xml = @"
2015-01-01T00:00:00Z
2015-02-01T00:00:00Z
NOT A VALID DATE
";
var loggerFactory = new StringLoggerFactory(LogLevel.Trace);
// Act
RunGetAllKeysCore(xml, new Mock().Object, loggerFactory).ToArray();
// Assert
Assert.True(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should have been logged.");
}
[Fact]
public void GetAllKeys_SurfacesRevocationDeserializationErrors()
{
// Arrange
const string xml = @"
2015-01-01T00:00:00Z
";
// Act & assert
// Bad GUID will lead to FormatException
Assert.Throws(() => RunGetAllKeysCore(xml, new Mock().Object));
}
private static IReadOnlyCollection RunGetAllKeysCore(string xml, IActivator activator, ILoggerFactory loggerFactory = null)
{
// Arrange - mocks
var mockXmlRepository = new Mock();
mockXmlRepository.Setup(o => o.GetAllElements()).Returns(XElement.Parse(xml).Elements().ToArray());
// Arrange - services
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(mockXmlRepository.Object);
serviceCollection.AddSingleton(activator);
serviceCollection.AddSingleton(new Mock().Object);
if (loggerFactory != null)
{
serviceCollection.AddSingleton(loggerFactory);
}
var services = serviceCollection.BuildServiceProvider();
var keyManager = new XmlKeyManager(services);
// Act
return keyManager.GetAllKeys();
}
[Fact]
public void RevokeAllKeys()
{
// Arrange - mocks
XElement elementStoredInRepository = null;
string friendlyNameStoredInRepository = null;
var mockXmlRepository = new Mock();
mockXmlRepository
.Setup(o => o.StoreElement(It.IsAny(), It.IsAny()))
.Callback((el, friendlyName) =>
{
elementStoredInRepository = el;
friendlyNameStoredInRepository = friendlyName;
});
// Arrange - services
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(mockXmlRepository.Object);
serviceCollection.AddSingleton(new Mock().Object);
var services = serviceCollection.BuildServiceProvider();
var keyManager = new XmlKeyManager(services);
var revocationDate = XmlConvert.ToDateTimeOffset("2015-03-01T19:13:19.7573854-08:00");
// Act & assert
// The cancellation token should not already be fired
var firstCancellationToken = keyManager.GetCacheExpirationToken();
Assert.False(firstCancellationToken.IsCancellationRequested);
// After the call to RevokeAllKeys, the first CT should be fired,
// and we should've gotten a new CT.
keyManager.RevokeAllKeys(revocationDate, "Here's some reason text.");
var secondCancellationToken = keyManager.GetCacheExpirationToken();
Assert.True(firstCancellationToken.IsCancellationRequested);
Assert.False(secondCancellationToken.IsCancellationRequested);
// Was the correct element stored in the repository?
const string expectedRepositoryXml = @"
2015-03-01T19:13:19.7573854-08:00
Here's some reason text.
";
XmlAssert.Equal(expectedRepositoryXml, elementStoredInRepository);
Assert.Equal("revocation-20150302T0313197573854Z", friendlyNameStoredInRepository);
}
[Fact]
public void RevokeSingleKey_Internal()
{
// Arrange - mocks
XElement elementStoredInRepository = null;
string friendlyNameStoredInRepository = null;
var mockXmlRepository = new Mock();
mockXmlRepository
.Setup(o => o.StoreElement(It.IsAny(), It.IsAny()))
.Callback((el, friendlyName) =>
{
elementStoredInRepository = el;
friendlyNameStoredInRepository = friendlyName;
});
// Arrange - services
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(mockXmlRepository.Object);
serviceCollection.AddSingleton(new Mock().Object);
var services = serviceCollection.BuildServiceProvider();
var keyManager = new XmlKeyManager(services);
var revocationDate = new DateTimeOffset(2014, 01, 01, 0, 0, 0, TimeSpan.Zero);
// Act & assert
// The cancellation token should not already be fired
var firstCancellationToken = keyManager.GetCacheExpirationToken();
Assert.False(firstCancellationToken.IsCancellationRequested);
// After the call to RevokeKey, the first CT should be fired,
// and we should've gotten a new CT.
((IInternalXmlKeyManager)keyManager).RevokeSingleKey(
keyId: new Guid("a11f35fc-1fed-4bd4-b727-056a63b70932"),
revocationDate: revocationDate,
reason: "Here's some reason text.");
var secondCancellationToken = keyManager.GetCacheExpirationToken();
Assert.True(firstCancellationToken.IsCancellationRequested);
Assert.False(secondCancellationToken.IsCancellationRequested);
// Was the correct element stored in the repository?
var expectedRepositoryXml = string.Format(@"
{0}
Here's some reason text.
",
new XElement("revocationDate", revocationDate));
XmlAssert.Equal(expectedRepositoryXml, elementStoredInRepository);
Assert.Equal("revocation-a11f35fc-1fed-4bd4-b727-056a63b70932", friendlyNameStoredInRepository);
}
[Fact]
public void RevokeKey_CallsInternalManager()
{
// Arrange - mocks
var keyToRevoke = new Guid("a11f35fc-1fed-4bd4-b727-056a63b70932");
DateTimeOffset minRevocationDate = DateTimeOffset.UtcNow;
DateTimeOffset? actualRevocationDate = null;
var mockInternalKeyManager = new Mock();
mockInternalKeyManager
.Setup(o => o.RevokeSingleKey(keyToRevoke, It.IsAny(), "Here's some reason text."))
.Callback((innerKeyId, innerRevocationDate, innerReason) =>
{
actualRevocationDate = innerRevocationDate;
});
// Arrange - services
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(new Mock().Object);
serviceCollection.AddSingleton(new Mock().Object);
serviceCollection.AddSingleton(mockInternalKeyManager.Object);
var services = serviceCollection.BuildServiceProvider();
var keyManager = new XmlKeyManager(services);
// Act
keyManager.RevokeKey(keyToRevoke, "Here's some reason text.");
// Assert
Assert.InRange(actualRevocationDate.Value, minRevocationDate, DateTimeOffset.UtcNow);
}
private class MyDeserializer : IAuthenticatedEncryptorDescriptorDeserializer
{
public IAuthenticatedEncryptorDescriptor ImportFromXml(XElement element)
{
throw new NotImplementedException();
}
}
}
}