Defer processing <key> descriptors until necessary

- Make CreateNewKey more robust against bad key repositories
- Don't hide key deserialization errors
This commit is contained in:
Levi B 2015-03-16 11:38:11 -07:00
parent 4365b531d8
commit 612a81d9ce
18 changed files with 526 additions and 148 deletions

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
private readonly CancellationToken _expirationToken; private readonly CancellationToken _expirationToken;
internal CacheableKeyRing(CancellationToken expirationToken, DateTimeOffset expirationTime, IKey defaultKey, IEnumerable<IKey> allKeys) internal CacheableKeyRing(CancellationToken expirationToken, DateTimeOffset expirationTime, IKey defaultKey, IEnumerable<IKey> allKeys)
: this(expirationToken, expirationTime, keyRing: new KeyRing(defaultKey.KeyId, allKeys)) : this(expirationToken, expirationTime, keyRing: new KeyRing(defaultKey, allKeys))
{ {
} }

View File

@ -10,6 +10,10 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
/// <summary> /// <summary>
/// The default key, may be null if no key is a good default candidate. /// The default key, may be null if no key is a good default candidate.
/// </summary> /// </summary>
/// <remarks>
/// If this property is non-null, its <see cref="IKey.CreateEncryptorInstance"/> method will succeed
/// so is appropriate for use with deferred keys.
/// </remarks>
public IKey DefaultKey; public IKey DefaultKey;
/// <summary> /// <summary>
@ -17,6 +21,10 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
/// honor the <see cref="ShouldGenerateNewKey"/> property. This property may /// honor the <see cref="ShouldGenerateNewKey"/> property. This property may
/// be null if there is no viable fallback key. /// be null if there is no viable fallback key.
/// </summary> /// </summary>
/// <remarks>
/// If this property is non-null, its <see cref="IKey.CreateEncryptorInstance"/> method will succeed
/// so is appropriate for use with deferred keys.
/// </remarks>
public IKey FallbackKey; public IKey FallbackKey;
/// <summary> /// <summary>

View File

@ -4,6 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Cryptography;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
using Microsoft.Framework.Logging; using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.DataProtection.KeyManagement namespace Microsoft.AspNet.DataProtection.KeyManagement
@ -43,11 +45,21 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
_logger = services.GetLogger<DefaultKeyResolver>(); _logger = services.GetLogger<DefaultKeyResolver>();
} }
public DefaultKeyResolution ResolveDefaultKeyPolicy(DateTimeOffset now, IEnumerable<IKey> allKeys) private bool CanCreateAuthenticatedEncryptor(IKey key)
{ {
DefaultKeyResolution retVal = default(DefaultKeyResolution); try
retVal.DefaultKey = FindDefaultKey(now, allKeys, out retVal.FallbackKey, out retVal.ShouldGenerateNewKey); {
return retVal; var encryptorInstance = key.CreateEncryptorInstance() ?? CryptoUtil.Fail<IAuthenticatedEncryptor>("CreateEncryptorInstance returned null.");
return true;
}
catch (Exception ex)
{
if (_logger.IsWarningLevelEnabled())
{
_logger.LogWarningF(ex, $"Key {key.KeyId:B} is ineligible to be the default key because its {nameof(IKey.CreateEncryptorInstance)} method failed.");
}
return false;
}
} }
private IKey FindDefaultKey(DateTimeOffset now, IEnumerable<IKey> allKeys, out IKey fallbackKey, out bool callerShouldGenerateNewKey) private IKey FindDefaultKey(DateTimeOffset now, IEnumerable<IKey> allKeys, out IKey fallbackKey, out bool callerShouldGenerateNewKey)
@ -66,11 +78,11 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
} }
// if the key has been revoked or is expired, it is no longer a candidate // if the key has been revoked or is expired, it is no longer a candidate
if (preferredDefaultKey.IsExpired(now) || preferredDefaultKey.IsRevoked) if (preferredDefaultKey.IsRevoked || preferredDefaultKey.IsExpired(now) || !CanCreateAuthenticatedEncryptor(preferredDefaultKey))
{ {
if (_logger.IsVerboseLevelEnabled()) if (_logger.IsVerboseLevelEnabled())
{ {
_logger.LogVerboseF($"Key {preferredDefaultKey.KeyId:B} is no longer under consideration as default key because it is expired or revoked."); _logger.LogVerboseF($"Key {preferredDefaultKey.KeyId:B} is no longer under consideration as default key because it is expired, revoked, or cannot be deciphered.");
} }
preferredDefaultKey = null; preferredDefaultKey = null;
} }
@ -112,7 +124,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
select key).Concat(from key in allKeys select key).Concat(from key in allKeys
orderby key.CreationDate ascending orderby key.CreationDate ascending
select key) select key)
where !key.IsRevoked where !key.IsRevoked && CanCreateAuthenticatedEncryptor(key)
select key).FirstOrDefault(); select key).FirstOrDefault();
if (_logger.IsVerboseLevelEnabled()) if (_logger.IsVerboseLevelEnabled())
@ -123,5 +135,12 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
callerShouldGenerateNewKey = true; callerShouldGenerateNewKey = true;
return null; return null;
} }
public DefaultKeyResolution ResolveDefaultKeyPolicy(DateTimeOffset now, IEnumerable<IKey> allKeys)
{
DefaultKeyResolution retVal = default(DefaultKeyResolution);
retVal.DefaultKey = FindDefaultKey(now, allKeys, out retVal.FallbackKey, out retVal.ShouldGenerateNewKey);
return retVal;
}
} }
} }

View File

@ -0,0 +1,41 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.Xml.Linq;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNet.DataProtection.XmlEncryption;
namespace Microsoft.AspNet.DataProtection.KeyManagement
{
/// <summary>
/// The basic implementation of <see cref="IKey"/>, where the incoming XML element
/// hasn't yet been fully processed.
/// </summary>
internal sealed class DeferredKey : KeyBase
{
public DeferredKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate, IInternalXmlKeyManager keyManager, XElement keyElement)
: base(keyId, creationDate, activationDate, expirationDate, new Lazy<IAuthenticatedEncryptor>(GetLazyEncryptorDelegate(keyManager, keyElement)))
{
}
private static Func<IAuthenticatedEncryptor> GetLazyEncryptorDelegate(IInternalXmlKeyManager keyManager, XElement keyElement)
{
// The <key> element will be held around in memory for a potentially lengthy period
// of time. Since it might contain sensitive information, we should protect it.
var encryptedKeyElement = keyElement.ToSecret();
try
{
return () => keyManager.DeserializeDescriptorFromKeyElement(encryptedKeyElement.ToXElement()).CreateEncryptorInstance();
}
finally
{
// It's important that the lambda above doesn't capture 'descriptorElement'. Clearing the reference here
// helps us detect if we've done this by causing a null ref at runtime.
keyElement = null;
}
}
}
}

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Xml.Linq;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
namespace Microsoft.AspNet.DataProtection.KeyManagement namespace Microsoft.AspNet.DataProtection.KeyManagement
{ {
@ -9,6 +11,9 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
internal interface IInternalXmlKeyManager internal interface IInternalXmlKeyManager
{ {
IKey CreateNewKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate); IKey CreateNewKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate);
IAuthenticatedEncryptorDescriptor DeserializeDescriptorFromKeyElement(XElement keyElement);
void RevokeSingleKey(Guid keyId, DateTimeOffset revocationDate, string reason); void RevokeSingleKey(Guid keyId, DateTimeOffset revocationDate, string reason);
} }
} }

View File

@ -8,40 +8,14 @@ using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
namespace Microsoft.AspNet.DataProtection.KeyManagement namespace Microsoft.AspNet.DataProtection.KeyManagement
{ {
/// <summary> /// <summary>
/// The basic implementation of <see cref="IKey"/>. /// The basic implementation of <see cref="IKey"/>, where the <see cref="IAuthenticatedEncryptorDescriptor"/>
/// has already been created.
/// </summary> /// </summary>
internal sealed class Key : IKey internal sealed class Key : KeyBase
{ {
private readonly IAuthenticatedEncryptorDescriptor _descriptor;
public Key(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate, IAuthenticatedEncryptorDescriptor descriptor) public Key(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate, IAuthenticatedEncryptorDescriptor descriptor)
: base(keyId, creationDate, activationDate, expirationDate, new Lazy<IAuthenticatedEncryptor>(descriptor.CreateEncryptorInstance))
{ {
KeyId = keyId;
CreationDate = creationDate;
ActivationDate = activationDate;
ExpirationDate = expirationDate;
_descriptor = descriptor;
}
public DateTimeOffset ActivationDate { get; }
public DateTimeOffset CreationDate { get; }
public DateTimeOffset ExpirationDate { get; }
public bool IsRevoked { get; private set; }
public Guid KeyId { get; }
public IAuthenticatedEncryptor CreateEncryptorInstance()
{
return _descriptor.CreateEncryptorInstance();
}
internal void SetRevoked()
{
IsRevoked = true;
} }
} }
} }

View File

@ -0,0 +1,45 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
namespace Microsoft.AspNet.DataProtection.KeyManagement
{
/// <summary>
/// The basic implementation of <see cref="IKey"/>.
/// </summary>
internal abstract class KeyBase : IKey
{
private readonly Lazy<IAuthenticatedEncryptor> _lazyEncryptor;
public KeyBase(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate, Lazy<IAuthenticatedEncryptor> lazyEncryptor)
{
KeyId = keyId;
CreationDate = creationDate;
ActivationDate = activationDate;
ExpirationDate = expirationDate;
_lazyEncryptor = lazyEncryptor;
}
public DateTimeOffset ActivationDate { get; }
public DateTimeOffset CreationDate { get; }
public DateTimeOffset ExpirationDate { get; }
public bool IsRevoked { get; private set; }
public Guid KeyId { get; }
public IAuthenticatedEncryptor CreateEncryptorInstance()
{
return _lazyEncryptor.Value;
}
internal void SetRevoked()
{
IsRevoked = true;
}
}
}

View File

@ -16,16 +16,24 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
private readonly KeyHolder _defaultKeyHolder; private readonly KeyHolder _defaultKeyHolder;
private readonly Dictionary<Guid, KeyHolder> _keyIdToKeyHolderMap; private readonly Dictionary<Guid, KeyHolder> _keyIdToKeyHolderMap;
public KeyRing(Guid defaultKeyId, IEnumerable<IKey> keys) public KeyRing(IKey defaultKey, IEnumerable<IKey> allKeys)
{ {
_keyIdToKeyHolderMap = new Dictionary<Guid, KeyHolder>(); _keyIdToKeyHolderMap = new Dictionary<Guid, KeyHolder>();
foreach (IKey key in keys) foreach (IKey key in allKeys)
{ {
_keyIdToKeyHolderMap.Add(key.KeyId, new KeyHolder(key)); _keyIdToKeyHolderMap.Add(key.KeyId, new KeyHolder(key));
} }
DefaultKeyId = defaultKeyId; // It's possible under some circumstances that the default key won't be part of 'allKeys',
_defaultKeyHolder = _keyIdToKeyHolderMap[defaultKeyId]; // such as if the key manager is forced to use the key it just generated even if such key
// wasn't in the underlying repository. In this case, we just add it now.
if (!_keyIdToKeyHolderMap.ContainsKey(defaultKey.KeyId))
{
_keyIdToKeyHolderMap.Add(defaultKey.KeyId, new KeyHolder(defaultKey));
}
DefaultKeyId = defaultKey.KeyId;
_defaultKeyHolder = _keyIdToKeyHolderMap[DefaultKeyId];
} }
public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor

View File

@ -31,7 +31,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
?? new DefaultKeyResolver(_keyManagementOptions.KeyPropagationWindow, _keyManagementOptions.MaxServerClockSkew, services); ?? new DefaultKeyResolver(_keyManagementOptions.KeyPropagationWindow, _keyManagementOptions.MaxServerClockSkew, services);
} }
private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, bool allowRecursiveCalls = false) private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, IKey keyJustAdded)
{ {
// Refresh the list of all keys // Refresh the list of all keys
var cacheExpirationToken = _keyManager.GetCacheExpirationToken(); var cacheExpirationToken = _keyManager.GetCacheExpirationToken();
@ -50,21 +50,18 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
_logger.LogVerbose("Policy resolution states that a new key should be added to the key ring."); _logger.LogVerbose("Policy resolution states that a new key should be added to the key ring.");
} }
// At this point, we know we need to generate a new key. // We shouldn't call CreateKey more than once, else we risk stack diving. This code path shouldn't
// get hit unless there was an ineligible key with an activation date slightly later than the one we
// This should only occur if a call to CreateNewKey immediately followed by a call to // just added. If this does happen, then we'll just use whatever key we can instead of creating
// GetAllKeys returned 'you need to add a key to the key ring'. This should never happen // new keys endlessly, eventually falling back to the one we just added if all else fails.
// in practice unless there's corruption in the backing store. Regardless, we can't recurse if (keyJustAdded != null)
// forever, so we have to bail now.
if (!allowRecursiveCalls)
{ {
if (_logger.IsErrorLevelEnabled()) var keyToUse = defaultKeyPolicy.DefaultKey ?? defaultKeyPolicy.FallbackKey ?? keyJustAdded;
{ return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, keyToUse, allKeys);
_logger.LogError("Policy resolution states that a new key should be added to the key ring, even after a call to CreateNewKey.");
}
throw CryptoUtil.Fail("Policy resolution states that a new key should be added to the key ring, even after a call to CreateNewKey.");
} }
// At this point, we know we need to generate a new key.
// We have been asked to generate a new key, but auto-generation of keys has been disabled. // We have been asked to generate a new key, but auto-generation of keys has been disabled.
// We need to use the fallback key or fail. // We need to use the fallback key or fail.
if (!_keyManagementOptions.AutoGenerateKeys) if (!_keyManagementOptions.AutoGenerateKeys)
@ -92,16 +89,16 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
{ {
// The case where there's no default key is the easiest scenario, since it // The case where there's no default key is the easiest scenario, since it
// means that we need to create a new key with immediate activation. // means that we need to create a new key with immediate activation.
_keyManager.CreateNewKey(activationDate: now, expirationDate: now + _keyManagementOptions.NewKeyLifetime); var newKey = _keyManager.CreateNewKey(activationDate: now, expirationDate: now + _keyManagementOptions.NewKeyLifetime);
return CreateCacheableKeyRingCore(now); // recursively call return CreateCacheableKeyRingCore(now, keyJustAdded: newKey); // recursively call
} }
else else
{ {
// If there is a default key, then the new key we generate should become active upon // If there is a default key, then the new key we generate should become active upon
// expiration of the default key. The new key lifetime is measured from the creation // expiration of the default key. The new key lifetime is measured from the creation
// date (now), not the activation date. // date (now), not the activation date.
_keyManager.CreateNewKey(activationDate: defaultKeyPolicy.DefaultKey.ExpirationDate, expirationDate: now + _keyManagementOptions.NewKeyLifetime); var newKey = _keyManager.CreateNewKey(activationDate: defaultKeyPolicy.DefaultKey.ExpirationDate, expirationDate: now + _keyManagementOptions.NewKeyLifetime);
return CreateCacheableKeyRingCore(now); // recursively call return CreateCacheableKeyRingCore(now, keyJustAdded: newKey); // recursively call
} }
} }
@ -109,6 +106,9 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
{ {
Debug.Assert(defaultKey != null); Debug.Assert(defaultKey != null);
// Invariant: our caller ensures that CreateEncryptorInstance succeeded at least once
Debug.Assert(defaultKey.CreateEncryptorInstance() != null);
if (_logger.IsVerboseLevelEnabled()) if (_logger.IsVerboseLevelEnabled())
{ {
_logger.LogVerboseF($"Using key {defaultKey.KeyId:B} as the default key."); _logger.LogVerboseF($"Using key {defaultKey.KeyId:B} as the default key.");
@ -186,7 +186,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
CacheableKeyRing ICacheableKeyRingProvider.GetCacheableKeyRing(DateTimeOffset now) CacheableKeyRing ICacheableKeyRingProvider.GetCacheableKeyRing(DateTimeOffset now)
{ {
// the entry point allows one recursive call // the entry point allows one recursive call
return CreateCacheableKeyRingCore(now, allowRecursiveCalls: true); return CreateCacheableKeyRingCore(now, keyJustAdded: null);
} }
} }
} }

View File

@ -122,7 +122,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
var allElements = KeyRepository.GetAllElements(); var allElements = KeyRepository.GetAllElements();
// We aggregate all the information we read into three buckets // We aggregate all the information we read into three buckets
Dictionary<Guid, Key> keyIdToKeyMap = new Dictionary<Guid, Key>(); Dictionary<Guid, KeyBase> keyIdToKeyMap = new Dictionary<Guid, KeyBase>();
HashSet<Guid> revokedKeyIds = null; HashSet<Guid> revokedKeyIds = null;
DateTimeOffset? mostRecentMassRevocationDate = null; DateTimeOffset? mostRecentMassRevocationDate = null;
@ -132,7 +132,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
{ {
// ProcessKeyElement can return null in the case of failure, and if this happens we'll move on. // ProcessKeyElement can return null in the case of failure, and if this happens we'll move on.
// Still need to throw if we see duplicate keys with the same id. // Still need to throw if we see duplicate keys with the same id.
Key key = ProcessKeyElement(element); KeyBase key = ProcessKeyElement(element);
if (key != null) if (key != null)
{ {
if (keyIdToKeyMap.ContainsKey(key.KeyId)) if (keyIdToKeyMap.ContainsKey(key.KeyId))
@ -179,7 +179,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
{ {
foreach (Guid revokedKeyId in revokedKeyIds) foreach (Guid revokedKeyId in revokedKeyIds)
{ {
Key key; KeyBase key;
keyIdToKeyMap.TryGetValue(revokedKeyId, out key); keyIdToKeyMap.TryGetValue(revokedKeyId, out key);
if (key != null) if (key != null)
{ {
@ -224,60 +224,36 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
return Interlocked.CompareExchange(ref _cacheExpirationTokenSource, null, null).Token; return Interlocked.CompareExchange(ref _cacheExpirationTokenSource, null, null).Token;
} }
private Key ProcessKeyElement(XElement keyElement) private KeyBase ProcessKeyElement(XElement keyElement)
{ {
Debug.Assert(keyElement.Name == KeyElementName); Debug.Assert(keyElement.Name == KeyElementName);
try try
{ {
// Read metadata // Read metadata and prepare the key for deferred instantiation
Guid keyId = (Guid)keyElement.Attribute(IdAttributeName); Guid keyId = (Guid)keyElement.Attribute(IdAttributeName);
DateTimeOffset creationDate = (DateTimeOffset)keyElement.Element(CreationDateElementName); DateTimeOffset creationDate = (DateTimeOffset)keyElement.Element(CreationDateElementName);
DateTimeOffset activationDate = (DateTimeOffset)keyElement.Element(ActivationDateElementName); DateTimeOffset activationDate = (DateTimeOffset)keyElement.Element(ActivationDateElementName);
DateTimeOffset expirationDate = (DateTimeOffset)keyElement.Element(ExpirationDateElementName); DateTimeOffset expirationDate = (DateTimeOffset)keyElement.Element(ExpirationDateElementName);
// Figure out who will be deserializing this
XElement descriptorElement = keyElement.Element(DescriptorElementName);
string descriptorDeserializerTypeName = (string)descriptorElement.Attribute(DeserializerTypeAttributeName);
// Decrypt the descriptor element and pass it to the descriptor for consumption
XElement unencryptedInputToDeserializer = descriptorElement.Elements().Single().DecryptElement(_activator);
var deserializerInstance = _activator.CreateInstance<IAuthenticatedEncryptorDescriptorDeserializer>(descriptorDeserializerTypeName);
var descriptorInstance = deserializerInstance.ImportFromXml(unencryptedInputToDeserializer);
// Finally, create the Key instance
if (_logger.IsVerboseLevelEnabled()) if (_logger.IsVerboseLevelEnabled())
{ {
_logger.LogVerboseF($"Found key {keyId:B}."); _logger.LogVerboseF($"Found key {keyId:B}.");
} }
return new Key(
return new DeferredKey(
keyId: keyId, keyId: keyId,
creationDate: creationDate, creationDate: creationDate,
activationDate: activationDate, activationDate: activationDate,
expirationDate: expirationDate, expirationDate: expirationDate,
descriptor: descriptorInstance); keyManager: _internalKeyManager,
keyElement: keyElement);
} }
catch (Exception ex) catch (Exception ex)
{ {
// We only write the exception out to the 'debug' log since it could contain sensitive WriteKeyDeserializationErrorToLog(ex, keyElement);
// information and we don't want to leak it.
if (_logger.IsDebugLevelEnabled())
{
if (_logger.IsWarningLevelEnabled())
{
_logger.LogWarningF($"An exception of type '{ex.GetType().FullName}' occurred while processing the key element '{keyElement.WithoutChildNodes()}', so the key will not be included in the keyring. Full details of the exception will be written to the 'Debug' log.");
}
_logger.LogDebugF(ex, $"An exception occurred while processing the key element '{keyElement}'.");
}
else
{
if (_logger.IsWarningLevelEnabled())
{
_logger.LogWarningF($"An exception of type '{ex.GetType().FullName}' occurred while processing the key element '{keyElement.WithoutChildNodes()}', so the key will not be included in the keyring. To prevent accidental disclosure of sensitive information the full exception details are not being logged. To enable logging full exception details, enable 'Debug' level logging for this provider.");
}
}
// If an error occurs, we just skip this key. // Don't include this key in the key ring
return null; return null;
} }
} }
@ -369,6 +345,26 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
Interlocked.Exchange(ref _cacheExpirationTokenSource, new CancellationTokenSource())?.Cancel(); Interlocked.Exchange(ref _cacheExpirationTokenSource, new CancellationTokenSource())?.Cancel();
} }
private void WriteKeyDeserializationErrorToLog(Exception error, XElement keyElement)
{
// Ideally we'd suppress the error since it might contain sensitive information, but it would be too difficult for
// an administrator to diagnose the issue if we hide this information. Instead we'll log the error to the error
// log and the raw <key> element to the debug log. This works for our out-of-box XML decryptors since they don't
// include sensitive information in the exception message.
if (_logger.IsErrorLevelEnabled())
{
// write sanitized <key> element
_logger.LogErrorF(error, $"An exception occurred while processing the key element '{keyElement.WithoutChildNodes()}'.");
}
if (_logger.IsDebugLevelEnabled())
{
// write full <key> element
_logger.LogDebugF(error, $"An exception occurred while processing the key element '{keyElement}'.");
}
}
IKey IInternalXmlKeyManager.CreateNewKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate) IKey IInternalXmlKeyManager.CreateNewKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate)
{ {
// <key id="{guid}" version="1"> // <key id="{guid}" version="1">
@ -440,6 +436,28 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
descriptor: newDescriptor); descriptor: newDescriptor);
} }
IAuthenticatedEncryptorDescriptor IInternalXmlKeyManager.DeserializeDescriptorFromKeyElement(XElement keyElement)
{
try
{
// Figure out who will be deserializing this
XElement descriptorElement = keyElement.Element(DescriptorElementName);
string descriptorDeserializerTypeName = (string)descriptorElement.Attribute(DeserializerTypeAttributeName);
// Decrypt the descriptor element and pass it to the descriptor for consumption
XElement unencryptedInputToDeserializer = descriptorElement.Elements().Single().DecryptElement(_activator);
var deserializerInstance = _activator.CreateInstance<IAuthenticatedEncryptorDescriptorDeserializer>(descriptorDeserializerTypeName);
var descriptorInstance = deserializerInstance.ImportFromXml(unencryptedInputToDeserializer);
return descriptorInstance ?? CryptoUtil.Fail<IAuthenticatedEncryptorDescriptor>("ImportFromXml returned null.");
}
catch (Exception ex)
{
WriteKeyDeserializationErrorToLog(ex, keyElement);
throw;
}
}
void IInternalXmlKeyManager.RevokeSingleKey(Guid keyId, DateTimeOffset revocationDate, string reason) void IInternalXmlKeyManager.RevokeSingleKey(Guid keyId, DateTimeOffset revocationDate, string reason)
{ {
// <revocation version="1"> // <revocation version="1">

View File

@ -123,8 +123,8 @@ namespace Microsoft.AspNet.DataProtection.XmlEncryption
} }
/// <summary> /// <summary>
/// Converts an <see cref="XElement"/> to a <see cref="Secret"/> so that it can be run through /// Converts an <see cref="XElement"/> to a <see cref="Secret"/> so that it can be kept in memory
/// the DPAPI routines. /// securely or run through the DPAPI routines.
/// </summary> /// </summary>
public static Secret ToSecret(this XElement element) public static Secret ToSecret(this XElement element)
{ {
@ -163,7 +163,7 @@ namespace Microsoft.AspNet.DataProtection.XmlEncryption
} }
/// <summary> /// <summary>
/// Converts a <see cref="Secret"/> provided by the DPAPI routines back into an <see cref="XElement"/>. /// Converts a <see cref="Secret"/> back into an <see cref="XElement"/>.
/// </summary> /// </summary>
public static XElement ToXElement(this Secret secret) public static XElement ToXElement(this Secret secret)
{ {

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
using Moq; using Moq;
using Xunit; using Xunit;
@ -105,7 +106,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
} }
[Fact] [Fact]
public void ResolveDefaultKeyPolicy_MostRecentKeyIsInvalid_ReturnsNull() public void ResolveDefaultKeyPolicy_MostRecentKeyIsInvalid_BecauseOfRevocation_ReturnsNull()
{ {
// Arrange // Arrange
var resolver = CreateDefaultKeyResolver(); var resolver = CreateDefaultKeyResolver();
@ -120,6 +121,22 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
Assert.True(resolution.ShouldGenerateNewKey); Assert.True(resolution.ShouldGenerateNewKey);
} }
[Fact]
public void ResolveDefaultKeyPolicy_MostRecentKeyIsInvalid_BecauseOfFailureToDecipher_ReturnsNull()
{
// Arrange
var resolver = CreateDefaultKeyResolver();
var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
var key2 = CreateKey("2015-03-02 00:00:00Z", "2016-03-01 00:00:00Z", createEncryptorInstanceThrows: true);
// Act
var resolution = resolver.ResolveDefaultKeyPolicy("2015-04-01 00:00:00Z", key1, key2);
// Assert
Assert.Null(resolution.DefaultKey);
Assert.True(resolution.ShouldGenerateNewKey);
}
[Fact] [Fact]
public void ResolveDefaultKeyPolicy_FutureKeyIsValidAndWithinClockSkew_ReturnsFutureKey() public void ResolveDefaultKeyPolicy_FutureKeyIsValidAndWithinClockSkew_ReturnsFutureKey()
{ {
@ -168,7 +185,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
} }
[Fact] [Fact]
public void ResolveDefaultKeyPolicy_FallbackKey_SelectsLatestBeforePriorPropagationWindow() public void ResolveDefaultKeyPolicy_FallbackKey_SelectsLatestBeforePriorPropagationWindow_IgnoresRevokedKeys()
{ {
// Arrange // Arrange
var resolver = CreateDefaultKeyResolver(); var resolver = CreateDefaultKeyResolver();
@ -185,6 +202,24 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
Assert.True(resolution.ShouldGenerateNewKey); Assert.True(resolution.ShouldGenerateNewKey);
} }
[Fact]
public void ResolveDefaultKeyPolicy_FallbackKey_SelectsLatestBeforePriorPropagationWindow_IgnoresFailures()
{
// Arrange
var resolver = CreateDefaultKeyResolver();
var key1 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-01 00:00:00Z");
var key2 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-02 00:00:00Z");
var key3 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-03 00:00:00Z", createEncryptorInstanceThrows: true);
var key4 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-04 00:00:00Z");
// Act
var resolution = resolver.ResolveDefaultKeyPolicy("2000-01-05 00:00:00Z", key1, key2, key3, key4);
// Assert
Assert.Same(key2, resolution.FallbackKey);
Assert.True(resolution.ShouldGenerateNewKey);
}
[Fact] [Fact]
public void ResolveDefaultKeyPolicy_FallbackKey_NoNonRevokedKeysBeforePriorPropagationWindow_SelectsEarliestNonRevokedKey() public void ResolveDefaultKeyPolicy_FallbackKey_NoNonRevokedKeysBeforePriorPropagationWindow_SelectsEarliestNonRevokedKey()
{ {
@ -210,7 +245,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
services: null); services: null);
} }
private static IKey CreateKey(string activationDate, string expirationDate, string creationDate = null, bool isRevoked = false) private static IKey CreateKey(string activationDate, string expirationDate, string creationDate = null, bool isRevoked = false, bool createEncryptorInstanceThrows = false)
{ {
var mockKey = new Mock<IKey>(); var mockKey = new Mock<IKey>();
mockKey.Setup(o => o.KeyId).Returns(Guid.NewGuid()); mockKey.Setup(o => o.KeyId).Returns(Guid.NewGuid());
@ -218,6 +253,14 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
mockKey.Setup(o => o.ActivationDate).Returns(DateTimeOffset.ParseExact(activationDate, "u", CultureInfo.InvariantCulture)); mockKey.Setup(o => o.ActivationDate).Returns(DateTimeOffset.ParseExact(activationDate, "u", CultureInfo.InvariantCulture));
mockKey.Setup(o => o.ExpirationDate).Returns(DateTimeOffset.ParseExact(expirationDate, "u", CultureInfo.InvariantCulture)); mockKey.Setup(o => o.ExpirationDate).Returns(DateTimeOffset.ParseExact(expirationDate, "u", CultureInfo.InvariantCulture));
mockKey.Setup(o => o.IsRevoked).Returns(isRevoked); mockKey.Setup(o => o.IsRevoked).Returns(isRevoked);
if (createEncryptorInstanceThrows)
{
mockKey.Setup(o => o.CreateEncryptorInstance()).Throws(new Exception("This method fails."));
}
else
{
mockKey.Setup(o => o.CreateEncryptorInstance()).Returns(new Mock<IAuthenticatedEncryptor>().Object);
}
return mockKey.Object; return mockKey.Object;
} }
} }

View File

@ -0,0 +1,95 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.Xml.Linq;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNet.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.DataProtection.KeyManagement
{
public class DeferredKeyTests
{
[Fact]
public void Ctor_Properties()
{
// Arrange
var keyId = Guid.NewGuid();
var creationDate = DateTimeOffset.Now;
var activationDate = creationDate.AddDays(2);
var expirationDate = creationDate.AddDays(90);
// Act
var key = new DeferredKey(keyId, creationDate, activationDate, expirationDate, new Mock<IInternalXmlKeyManager>().Object, XElement.Parse(@"<node />"));
// Assert
Assert.Equal(keyId, key.KeyId);
Assert.Equal(creationDate, key.CreationDate);
Assert.Equal(activationDate, key.ActivationDate);
Assert.Equal(expirationDate, key.ExpirationDate);
}
[Fact]
public void SetRevoked_Respected()
{
// Arrange
var now = DateTimeOffset.UtcNow;
var key = new DeferredKey(Guid.Empty, now, now, now, new Mock<IInternalXmlKeyManager>().Object, XElement.Parse(@"<node />"));
// Act & assert
Assert.False(key.IsRevoked);
key.SetRevoked();
Assert.True(key.IsRevoked);
}
[Fact]
public void CreateEncryptorInstance_Success()
{
// Arrange
var expectedEncryptor = new Mock<IAuthenticatedEncryptor>().Object;
var mockDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>();
mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(expectedEncryptor);
var mockKeyManager = new Mock<IInternalXmlKeyManager>();
mockKeyManager.Setup(o => o.DeserializeDescriptorFromKeyElement(It.IsAny<XElement>()))
.Returns<XElement>(element =>
{
XmlAssert.Equal(@"<node />", element);
return mockDescriptor.Object;
});
var now = DateTimeOffset.UtcNow;
var key = new DeferredKey(Guid.Empty, now, now, now, mockKeyManager.Object, XElement.Parse(@"<node />"));
// Act
var actual = key.CreateEncryptorInstance();
// Assert
Assert.Same(expectedEncryptor, actual);
}
[Fact]
public void CreateEncryptorInstance_CachesFailures()
{
// Arrange
int numTimesCalled = 0;
var mockKeyManager = new Mock<IInternalXmlKeyManager>();
mockKeyManager.Setup(o => o.DeserializeDescriptorFromKeyElement(It.IsAny<XElement>()))
.Returns<XElement>(element =>
{
numTimesCalled++;
throw new Exception("How exceptional.");
});
var now = DateTimeOffset.UtcNow;
var key = new DeferredKey(Guid.Empty, now, now, now, mockKeyManager.Object, XElement.Parse(@"<node />"));
// Act & assert
ExceptionAssert.Throws<Exception>(() => key.CreateEncryptorInstance(), "How exceptional.");
ExceptionAssert.Throws<Exception>(() => key.CreateEncryptorInstance(), "How exceptional.");
Assert.Equal(1, numTimesCalled);
}
}
}

View File

@ -203,7 +203,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
// the keyring has only one key // the keyring has only one key
Key key = new Key(Guid.Empty, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object); Key key = new Key(Guid.Empty, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object);
var keyRing = new KeyRing(Guid.Empty, new[] { key }); var keyRing = new KeyRing(key, new[] { key });
var mockKeyRingProvider = new Mock<IKeyRingProvider>(); var mockKeyRingProvider = new Mock<IKeyRingProvider>();
mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
@ -233,7 +233,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
// the keyring has only one key // the keyring has only one key
Key key = new Key(keyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object); Key key = new Key(keyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object);
key.SetRevoked(); key.SetRevoked();
var keyRing = new KeyRing(keyId, new[] { key }); var keyRing = new KeyRing(key, new[] { key });
var mockKeyRingProvider = new Mock<IKeyRingProvider>(); var mockKeyRingProvider = new Mock<IKeyRingProvider>();
mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
@ -272,7 +272,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object); Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object);
defaultKey.SetRevoked(); defaultKey.SetRevoked();
var keyRing = new KeyRing(defaultKeyId, new[] { defaultKey }); var keyRing = new KeyRing(defaultKey, new[] { defaultKey });
var mockKeyRingProvider = new Mock<IKeyRingProvider>(); var mockKeyRingProvider = new Mock<IKeyRingProvider>();
mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
@ -318,7 +318,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(mockEncryptor.Object); mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(mockEncryptor.Object);
Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object); Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object);
var keyRing = new KeyRing(defaultKeyId, new[] { defaultKey }); var keyRing = new KeyRing(defaultKey, new[] { defaultKey });
var mockKeyRingProvider = new Mock<IKeyRingProvider>(); var mockKeyRingProvider = new Mock<IKeyRingProvider>();
mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
@ -368,7 +368,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new Mock<IAuthenticatedEncryptorDescriptor>().Object); Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new Mock<IAuthenticatedEncryptorDescriptor>().Object);
Key embeddedKey = new Key(embeddedKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object); Key embeddedKey = new Key(embeddedKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object);
var keyRing = new KeyRing(defaultKeyId, new[] { defaultKey, embeddedKey }); var keyRing = new KeyRing(defaultKey, new[] { defaultKey, embeddedKey });
var mockKeyRingProvider = new Mock<IKeyRingProvider>(); var mockKeyRingProvider = new Mock<IKeyRingProvider>();
mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
@ -399,7 +399,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
// Arrange // Arrange
byte[] plaintext = new byte[] { 0x10, 0x20, 0x30, 0x40, 0x50 }; byte[] plaintext = new byte[] { 0x10, 0x20, 0x30, 0x40, 0x50 };
Key key = new Key(Guid.NewGuid(), DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new AuthenticatedEncryptorConfiguration(new AuthenticatedEncryptionOptions()).CreateNewDescriptor()); Key key = new Key(Guid.NewGuid(), DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new AuthenticatedEncryptorConfiguration(new AuthenticatedEncryptionOptions()).CreateNewDescriptor());
var keyRing = new KeyRing(key.KeyId, new[] { key }); var keyRing = new KeyRing(key, new[] { key });
var mockKeyRingProvider = new Mock<IKeyRingProvider>(); var mockKeyRingProvider = new Mock<IKeyRingProvider>();
mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);

View File

@ -6,10 +6,13 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
using Moq; using Moq;
using Xunit; using Xunit;
using static System.FormattableString;
namespace Microsoft.AspNet.DataProtection.KeyManagement namespace Microsoft.AspNet.DataProtection.KeyManagement
{ {
public class KeyRingProviderTests public class KeyRingProviderTests
@ -110,7 +113,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
getCacheExpirationTokenReturnValues: new[] { expirationCts1.Token, expirationCts2.Token }, getCacheExpirationTokenReturnValues: new[] { expirationCts1.Token, expirationCts2.Token },
getAllKeysReturnValues: new[] { allKeys1, allKeys2 }, getAllKeysReturnValues: new[] { allKeys1, allKeys2 },
createNewKeyCallbacks: new[] { createNewKeyCallbacks: new[] {
Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90)) Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90), CreateKey())
}, },
resolveDefaultKeyPolicyReturnValues: new[] resolveDefaultKeyPolicyReturnValues: new[]
{ {
@ -140,6 +143,54 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy", "CreateNewKey", "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence); Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy", "CreateNewKey", "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence);
} }
[Fact]
public void CreateCacheableKeyRing_GenerationRequired_NoDefaultKey_CreatesNewKeyWithImmediateActivation_StillNoDefaultKey_ReturnsNewlyCreatedKey()
{
// Arrange
var callSequence = new List<string>();
var expirationCts1 = new CancellationTokenSource();
var expirationCts2 = new CancellationTokenSource();
var now = StringToDateTime("2015-03-01 00:00:00Z");
var allKeys = new IKey[0];
var newlyCreatedKey = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
var keyRingProvider = SetupCreateCacheableKeyRingTestAndCreateKeyManager(
callSequence: callSequence,
getCacheExpirationTokenReturnValues: new[] { expirationCts1.Token, expirationCts2.Token },
getAllKeysReturnValues: new[] { allKeys, allKeys },
createNewKeyCallbacks: new[] {
Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90), newlyCreatedKey)
},
resolveDefaultKeyPolicyReturnValues: new[]
{
Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys, new DefaultKeyResolution()
{
DefaultKey = null,
ShouldGenerateNewKey = true
}),
Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys, new DefaultKeyResolution()
{
DefaultKey = null,
ShouldGenerateNewKey = true
})
});
// Act
var cacheableKeyRing = keyRingProvider.GetCacheableKeyRing(now);
// Assert
Assert.Equal(newlyCreatedKey.KeyId, cacheableKeyRing.KeyRing.DefaultKeyId);
AssertWithinJitterRange(cacheableKeyRing.ExpirationTimeUtc, now);
Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now));
expirationCts1.Cancel();
Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now));
expirationCts2.Cancel();
Assert.False(CacheableKeyRing.IsValid(cacheableKeyRing, now));
Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy", "CreateNewKey", "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence);
}
[Fact] [Fact]
public void CreateCacheableKeyRing_GenerationRequired_NoDefaultKey_KeyGenerationDisabled_Fails() public void CreateCacheableKeyRing_GenerationRequired_NoDefaultKey_KeyGenerationDisabled_Fails()
{ {
@ -154,7 +205,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
getCacheExpirationTokenReturnValues: new[] { CancellationToken.None }, getCacheExpirationTokenReturnValues: new[] { CancellationToken.None },
getAllKeysReturnValues: new[] { allKeys }, getAllKeysReturnValues: new[] { allKeys },
createNewKeyCallbacks: new[] { createNewKeyCallbacks: new[] {
Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90)) Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90), CreateKey())
}, },
resolveDefaultKeyPolicyReturnValues: new[] resolveDefaultKeyPolicyReturnValues: new[]
{ {
@ -194,7 +245,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
getCacheExpirationTokenReturnValues: new[] { expirationCts1.Token, expirationCts2.Token }, getCacheExpirationTokenReturnValues: new[] { expirationCts1.Token, expirationCts2.Token },
getAllKeysReturnValues: new[] { allKeys1, allKeys2 }, getAllKeysReturnValues: new[] { allKeys1, allKeys2 },
createNewKeyCallbacks: new[] { createNewKeyCallbacks: new[] {
Tuple.Create(key1.ExpirationDate, (DateTimeOffset)now + TimeSpan.FromDays(90)) Tuple.Create(key1.ExpirationDate, (DateTimeOffset)now + TimeSpan.FromDays(90), CreateKey())
}, },
resolveDefaultKeyPolicyReturnValues: new[] resolveDefaultKeyPolicyReturnValues: new[]
{ {
@ -304,7 +355,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
IList<string> callSequence, IList<string> callSequence,
IEnumerable<CancellationToken> getCacheExpirationTokenReturnValues, IEnumerable<CancellationToken> getCacheExpirationTokenReturnValues,
IEnumerable<IReadOnlyCollection<IKey>> getAllKeysReturnValues, IEnumerable<IReadOnlyCollection<IKey>> getAllKeysReturnValues,
IEnumerable<Tuple<DateTimeOffset,DateTimeOffset>> createNewKeyCallbacks, IEnumerable<Tuple<DateTimeOffset, DateTimeOffset, IKey>> createNewKeyCallbacks,
IEnumerable<Tuple<DateTimeOffset, IEnumerable<IKey>, DefaultKeyResolution>> resolveDefaultKeyPolicyReturnValues, IEnumerable<Tuple<DateTimeOffset, IEnumerable<IKey>, DefaultKeyResolution>> resolveDefaultKeyPolicyReturnValues,
KeyManagementOptions keyManagementOptions = null) KeyManagementOptions keyManagementOptions = null)
{ {
@ -337,21 +388,21 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
createNewKeyCallbacksEnumerator.MoveNext(); createNewKeyCallbacksEnumerator.MoveNext();
Assert.Equal(createNewKeyCallbacksEnumerator.Current.Item1, activationDate); Assert.Equal(createNewKeyCallbacksEnumerator.Current.Item1, activationDate);
Assert.Equal(createNewKeyCallbacksEnumerator.Current.Item2, expirationDate); Assert.Equal(createNewKeyCallbacksEnumerator.Current.Item2, expirationDate);
return null; // nobody uses this return value return createNewKeyCallbacksEnumerator.Current.Item3;
}); });
} }
var resolveDefaultKeyPolicyReturnValuesEnumerator = resolveDefaultKeyPolicyReturnValues.GetEnumerator(); var resolveDefaultKeyPolicyReturnValuesEnumerator = resolveDefaultKeyPolicyReturnValues.GetEnumerator();
var mockDefaultKeyResolver = new Mock<IDefaultKeyResolver>(MockBehavior.Strict); var mockDefaultKeyResolver = new Mock<IDefaultKeyResolver>(MockBehavior.Strict);
mockDefaultKeyResolver.Setup(o => o.ResolveDefaultKeyPolicy(It.IsAny<DateTimeOffset>(), It.IsAny<IEnumerable<IKey>>())) mockDefaultKeyResolver.Setup(o => o.ResolveDefaultKeyPolicy(It.IsAny<DateTimeOffset>(), It.IsAny<IEnumerable<IKey>>()))
.Returns<DateTimeOffset,IEnumerable<IKey>>((now, allKeys) => .Returns<DateTimeOffset, IEnumerable<IKey>>((now, allKeys) =>
{ {
callSequence.Add("ResolveDefaultKeyPolicy"); callSequence.Add("ResolveDefaultKeyPolicy");
resolveDefaultKeyPolicyReturnValuesEnumerator.MoveNext(); resolveDefaultKeyPolicyReturnValuesEnumerator.MoveNext();
Assert.Equal(resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item1, now); Assert.Equal(resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item1, now);
Assert.Equal(resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item2, allKeys); Assert.Equal(resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item2, allKeys);
return resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item3; return resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item3;
}); });
return CreateKeyRingProvider(mockKeyManager.Object, mockDefaultKeyResolver.Object, keyManagementOptions); return CreateKeyRingProvider(mockKeyManager.Object, mockDefaultKeyResolver.Object, keyManagementOptions);
} }
@ -495,6 +546,12 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
return DateTimeOffset.ParseExact(input, "u", CultureInfo.InvariantCulture).UtcDateTime; return DateTimeOffset.ParseExact(input, "u", CultureInfo.InvariantCulture).UtcDateTime;
} }
private static IKey CreateKey()
{
var now = DateTimeOffset.Now;
return CreateKey(Invariant($"{now:u}"), Invariant($"{now.AddDays(90):u}"));
}
private static IKey CreateKey(string activationDate, string expirationDate, bool isRevoked = false) private static IKey CreateKey(string activationDate, string expirationDate, bool isRevoked = false)
{ {
var mockKey = new Mock<IKey>(); var mockKey = new Mock<IKey>();
@ -502,6 +559,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
mockKey.Setup(o => o.ActivationDate).Returns(DateTimeOffset.ParseExact(activationDate, "u", CultureInfo.InvariantCulture)); mockKey.Setup(o => o.ActivationDate).Returns(DateTimeOffset.ParseExact(activationDate, "u", CultureInfo.InvariantCulture));
mockKey.Setup(o => o.ExpirationDate).Returns(DateTimeOffset.ParseExact(expirationDate, "u", CultureInfo.InvariantCulture)); mockKey.Setup(o => o.ExpirationDate).Returns(DateTimeOffset.ParseExact(expirationDate, "u", CultureInfo.InvariantCulture));
mockKey.Setup(o => o.IsRevoked).Returns(isRevoked); mockKey.Setup(o => o.IsRevoked).Returns(isRevoked);
mockKey.Setup(o => o.CreateEncryptorInstance()).Returns(new Mock<IAuthenticatedEncryptor>().Object);
return mockKey.Object; return mockKey.Object;
} }
} }

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
var key2 = new MyKey(); var key2 = new MyKey();
// Act // Act
var keyRing = new KeyRing(key1.KeyId, new[] { key1, key2 }); var keyRing = new KeyRing(key1, new[] { key1, key2 });
// Assert // Assert
Assert.Equal(0, key1.NumTimesCreateEncryptorInstanceCalled); Assert.Equal(0, key1.NumTimesCreateEncryptorInstanceCalled);
@ -38,12 +38,29 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
var key2 = new MyKey(); var key2 = new MyKey();
// Act // Act
var keyRing = new KeyRing(key2.KeyId, new[] { key1, key2 }); var keyRing = new KeyRing(key2, new[] { key1, key2 });
// Assert // Assert
Assert.Equal(key2.KeyId, keyRing.DefaultKeyId); Assert.Equal(key2.KeyId, keyRing.DefaultKeyId);
} }
[Fact]
public void DefaultKeyIdAndEncryptor_IfDefaultKeyNotPresentInAllKeys()
{
// Arrange
var key1 = new MyKey();
var key2 = new MyKey();
var key3 = new MyKey(expectedEncryptorInstance: new Mock<IAuthenticatedEncryptor>().Object);
// Act
var keyRing = new KeyRing(key3, new[] { key1, key2 });
// Assert
bool unused;
Assert.Equal(key3.KeyId, keyRing.DefaultKeyId);
Assert.Equal(key3.CreateEncryptorInstance(), keyRing.GetAuthenticatedEncryptorByKeyId(key3.KeyId, out unused));
}
[Fact] [Fact]
public void GetAuthenticatedEncryptorByKeyId_DefersInstantiation_AndReturnsRevocationInfo() public void GetAuthenticatedEncryptorByKeyId_DefersInstantiation_AndReturnsRevocationInfo()
{ {
@ -55,7 +72,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
var key2 = new MyKey(expectedEncryptorInstance: expectedEncryptorInstance2); var key2 = new MyKey(expectedEncryptorInstance: expectedEncryptorInstance2);
// Act // Act
var keyRing = new KeyRing(key2.KeyId, new[] { key1, key2 }); var keyRing = new KeyRing(key2, new[] { key1, key2 });
// Assert // Assert
bool isRevoked; bool isRevoked;

View File

@ -0,0 +1,64 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Moq;
using Xunit;
namespace Microsoft.AspNet.DataProtection.KeyManagement
{
public class KeyTests
{
[Fact]
public void Ctor_Properties()
{
// Arrange
var keyId = Guid.NewGuid();
var creationDate = DateTimeOffset.Now;
var activationDate = creationDate.AddDays(2);
var expirationDate = creationDate.AddDays(90);
// Act
var key = new Key(keyId, creationDate, activationDate, expirationDate, new Mock<IAuthenticatedEncryptorDescriptor>().Object);
// Assert
Assert.Equal(keyId, key.KeyId);
Assert.Equal(creationDate, key.CreationDate);
Assert.Equal(activationDate, key.ActivationDate);
Assert.Equal(expirationDate, key.ExpirationDate);
}
[Fact]
public void SetRevoked_Respected()
{
// Arrange
var now = DateTimeOffset.UtcNow;
var key = new Key(Guid.Empty, now, now, now, new Mock<IAuthenticatedEncryptorDescriptor>().Object);
// Act & assert
Assert.False(key.IsRevoked);
key.SetRevoked();
Assert.True(key.IsRevoked);
}
[Fact]
public void CreateEncryptorInstance()
{
// Arrange
var expected = new Mock<IAuthenticatedEncryptor>().Object;
var mockDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>();
mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(expected);
var now = DateTimeOffset.UtcNow;
var key = new Key(Guid.Empty, now, now, now, mockDescriptor.Object);
// Act
var actual = key.CreateEncryptorInstance();
// Assert
Assert.Same(expected, actual);
}
}
}

View File

@ -473,7 +473,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
<key id='78cd498e-9375-4e55-ac0d-d79527ecd09d' version='1'> <key id='78cd498e-9375-4e55-ac0d-d79527ecd09d' version='1'>
<creationDate>2015-01-01T00:00:00Z</creationDate> <creationDate>2015-01-01T00:00:00Z</creationDate>
<activationDate>2015-02-01T00:00:00Z</activationDate> <activationDate>2015-02-01T00:00:00Z</activationDate>
<expirationDate>2015-03-01T00:00:00Z</expirationDate> <expirationDate>NOT A VALID DATE</expirationDate>
<descriptor deserializerType='badDeserializer'> <descriptor deserializerType='badDeserializer'>
<node /> <node />
</descriptor> </descriptor>
@ -492,7 +492,6 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
var expectedEncryptor = new Mock<IAuthenticatedEncryptor>().Object; var expectedEncryptor = new Mock<IAuthenticatedEncryptor>().Object;
var mockActivator = new Mock<IActivator>(); var mockActivator = new Mock<IActivator>();
mockActivator.ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput("goodDeserializer", "<node xmlns='private' />", expectedEncryptor); mockActivator.ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput("goodDeserializer", "<node xmlns='private' />", expectedEncryptor);
mockActivator.Setup(o => o.CreateInstance(It.IsAny<Type>(), "badDeserializer")).Throws(new Exception("How exceptional!"));
// Act // Act
var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray(); var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray();
@ -513,26 +512,18 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
<key id='78cd498e-9375-4e55-ac0d-d79527ecd09d' version='1'> <key id='78cd498e-9375-4e55-ac0d-d79527ecd09d' version='1'>
<creationDate>2015-01-01T00:00:00Z</creationDate> <creationDate>2015-01-01T00:00:00Z</creationDate>
<activationDate>2015-02-01T00:00:00Z</activationDate> <activationDate>2015-02-01T00:00:00Z</activationDate>
<expirationDate>2015-03-01T00:00:00Z</expirationDate> <expirationDate>NOT A VALID DATE</expirationDate>
<descriptor deserializerType='badDeserializer'> <!-- Secret information: 1A2B3C4D -->
<node>
<!-- Secret information: 1A2B3C4D -->
</node>
</descriptor>
</key> </key>
</root>"; </root>";
var mockActivator = new Mock<IActivator>();
mockActivator.Setup(o => o.CreateInstance(It.IsAny<Type>(), "badDeserializer")).Throws(new Exception("Secret information: 9Z8Y7X6W"));
var loggerFactory = new StringLoggerFactory(LogLevel.Verbose); var loggerFactory = new StringLoggerFactory(LogLevel.Verbose);
// Act // Act
RunGetAllKeysCore(xml, mockActivator.Object, loggerFactory).ToArray(); RunGetAllKeysCore(xml, new Mock<IActivator>().Object, loggerFactory).ToArray();
// Assert // Assert
Assert.False(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should not have been logged."); Assert.False(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should not have been logged.");
Assert.False(loggerFactory.ToString().Contains("9Z8Y7X6W"), "The secret '1A2B3C4D' should not have been logged.");
} }
[Fact] [Fact]
@ -545,26 +536,18 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
<key id='78cd498e-9375-4e55-ac0d-d79527ecd09d' version='1'> <key id='78cd498e-9375-4e55-ac0d-d79527ecd09d' version='1'>
<creationDate>2015-01-01T00:00:00Z</creationDate> <creationDate>2015-01-01T00:00:00Z</creationDate>
<activationDate>2015-02-01T00:00:00Z</activationDate> <activationDate>2015-02-01T00:00:00Z</activationDate>
<expirationDate>2015-03-01T00:00:00Z</expirationDate> <expirationDate>NOT A VALID DATE</expirationDate>
<descriptor deserializerType='badDeserializer'> <!-- Secret information: 1A2B3C4D -->
<node>
<!-- Secret information: 1A2B3C4D -->
</node>
</descriptor>
</key> </key>
</root>"; </root>";
var mockActivator = new Mock<IActivator>();
mockActivator.Setup(o => o.CreateInstance(It.IsAny<Type>(), "badDeserializer")).Throws(new Exception("Secret information: 9Z8Y7X6W"));
var loggerFactory = new StringLoggerFactory(LogLevel.Debug); var loggerFactory = new StringLoggerFactory(LogLevel.Debug);
// Act // Act
RunGetAllKeysCore(xml, mockActivator.Object, loggerFactory).ToArray(); RunGetAllKeysCore(xml, new Mock<IActivator>().Object, loggerFactory).ToArray();
// Assert // Assert
Assert.True(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should have been logged."); Assert.True(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should have been logged.");
Assert.True(loggerFactory.ToString().Contains("9Z8Y7X6W"), "The secret '9Z8Y7X6W' should have been logged.");
} }
[Fact] [Fact]