// 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(); } } } }