Rename KeyLifetimeOptions -> KeyManagementOptions

Simplify default key resolution logic
Introduce API for disabling automatic key generation
This commit is contained in:
Levi B 2015-03-11 15:40:15 -07:00
parent 20fe4f8d63
commit 58c823bc45
11 changed files with 273 additions and 117 deletions

View File

@ -113,6 +113,36 @@ namespace Microsoft.AspNet.DataProtection
return this; return this;
} }
/// <summary>
/// Configures the data protection system not to generate new keys automatically.
/// </summary>
/// <returns>The 'this' instance.</returns>
/// <remarks>
/// Calling this API corresponds to setting <see cref="KeyManagementOptions.AutoGenerateKeys"/>
/// to 'false'. See that property's documentation for more information.
/// </remarks>
public DataProtectionConfiguration DisableAutomaticKeyGeneration()
{
Services.Configure<KeyManagementOptions>(options =>
{
options.AutoGenerateKeys = false;
});
return this;
}
/// <summary>
/// Configures the data protection system to persist keys in storage as plaintext.
/// </summary>
/// <returns>The 'this' instance.</returns>
/// <remarks>
/// Caution: cryptographic key material will not be protected at rest.
/// </remarks>
public DataProtectionConfiguration DisableProtectionOfKeysAtRest()
{
RemoveAllServicesOfType(typeof(IXmlEncryptor));
return this;
}
/// <summary> /// <summary>
/// Configures the data protection system to persist keys to the specified directory. /// Configures the data protection system to persist keys to the specified directory.
/// This path may be on the local machine or may point to a UNC share. /// This path may be on the local machine or may point to a UNC share.
@ -241,30 +271,17 @@ namespace Microsoft.AspNet.DataProtection
/// Sets the default lifetime of keys created by the data protection system. /// Sets the default lifetime of keys created by the data protection system.
/// </summary> /// </summary>
/// <param name="lifetime">The lifetime (time before expiration) for newly-created keys. /// <param name="lifetime">The lifetime (time before expiration) for newly-created keys.
/// See <see cref="KeyLifetimeOptions.NewKeyLifetime"/> for more information and /// See <see cref="KeyManagementOptions.NewKeyLifetime"/> for more information and
/// usage notes.</param> /// usage notes.</param>
/// <returns>The 'this' instance.</returns> /// <returns>The 'this' instance.</returns>
public DataProtectionConfiguration SetDefaultKeyLifetime(TimeSpan lifetime) public DataProtectionConfiguration SetDefaultKeyLifetime(TimeSpan lifetime)
{ {
Services.Configure<KeyLifetimeOptions>(options => Services.Configure<KeyManagementOptions>(options =>
{ {
options.NewKeyLifetime = lifetime; options.NewKeyLifetime = lifetime;
}); });
return this; return this;
} }
/// <summary>
/// Configures the data protection system to persist keys in storage as plaintext.
/// </summary>
/// <returns>The 'this' instance.</returns>
/// <remarks>
/// Caution: cryptographic key material will not be protected at rest.
/// </remarks>
public DataProtectionConfiguration SuppressProtectionOfKeysAtRest()
{
RemoveAllServicesOfType(typeof(IXmlEncryptor));
return this;
}
/// <summary> /// <summary>
/// Configures the data protection system to use the specified cryptographic algorithms /// Configures the data protection system to use the specified cryptographic algorithms

View File

@ -72,7 +72,7 @@ namespace Microsoft.AspNet.DataProtection
{ {
var keyRingProvider = new KeyRingProvider( var keyRingProvider = new KeyRingProvider(
keyManager: services.GetRequiredService<IKeyManager>(), keyManager: services.GetRequiredService<IKeyManager>(),
keyLifetimeOptions: services.GetService<IOptions<KeyLifetimeOptions>>()?.Options, // might be null keyManagementOptions: services.GetService<IOptions<KeyManagementOptions>>()?.Options, // might be null
services: services); services: services);
dataProtectionProvider = new KeyRingBasedDataProtectionProvider(keyRingProvider, services); dataProtectionProvider = new KeyRingBasedDataProtectionProvider(keyRingProvider, services);
} }

View File

@ -39,14 +39,14 @@ namespace Microsoft.Framework.DependencyInjection
} }
/// <summary> /// <summary>
/// An <see cref="IConfigureOptions{KeyLifetimeOptions}"/> where the key lifetime is specified explicitly. /// An <see cref="IConfigureOptions{KeyManagementOptions}"/> where the key lifetime is specified explicitly.
/// </summary> /// </summary>
public static ServiceDescriptor ConfigureOptions_DefaultKeyLifetime(int numDays) public static ServiceDescriptor ConfigureOptions_DefaultKeyLifetime(int numDays)
{ {
return ServiceDescriptor.Transient<IConfigureOptions<KeyLifetimeOptions>>(services => return ServiceDescriptor.Transient<IConfigureOptions<KeyManagementOptions>>(services =>
{ {
return new ConfigureOptions<KeyLifetimeOptions>(options => return new ConfigureOptions<KeyManagementOptions>(options =>
{ {
options.NewKeyLifetime = TimeSpan.FromDays(numDays); options.NewKeyLifetime = TimeSpan.FromDays(numDays);
}); });

View File

@ -18,10 +18,10 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
/// and persisted to the keyring to ensure uninterrupted service. /// and persisted to the keyring to ensure uninterrupted service.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// If the expiration window is 5 days and the current key expires within 5 days, /// If the propagation time is 5 days and the current key expires within 5 days,
/// a new key will be generated. /// a new key will be generated.
/// </remarks> /// </remarks>
private readonly TimeSpan _keyGenBeforeExpirationWindow; private readonly TimeSpan _keyPropagationWindow;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -36,9 +36,9 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
/// </remarks> /// </remarks>
private readonly TimeSpan _maxServerToServerClockSkew; private readonly TimeSpan _maxServerToServerClockSkew;
public DefaultKeyResolver(TimeSpan keyGenBeforeExpirationWindow, TimeSpan maxServerToServerClockSkew, IServiceProvider services) public DefaultKeyResolver(TimeSpan keyPropagationWindow, TimeSpan maxServerToServerClockSkew, IServiceProvider services)
{ {
_keyGenBeforeExpirationWindow = keyGenBeforeExpirationWindow; _keyPropagationWindow = keyPropagationWindow;
_maxServerToServerClockSkew = maxServerToServerClockSkew; _maxServerToServerClockSkew = maxServerToServerClockSkew;
_logger = services.GetLogger<DefaultKeyResolver>(); _logger = services.GetLogger<DefaultKeyResolver>();
} }
@ -52,82 +52,61 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
private IKey FindDefaultKey(DateTimeOffset now, IEnumerable<IKey> allKeys, out bool callerShouldGenerateNewKey) private IKey FindDefaultKey(DateTimeOffset now, IEnumerable<IKey> allKeys, out bool callerShouldGenerateNewKey)
{ {
// the key with the most recent activation date where the activation date is in the past // find the preferred default key (allowing for server-to-server clock skew)
IKey keyMostRecentlyActivated = (from key in allKeys var preferredDefaultKey = (from key in allKeys
where key.ActivationDate <= now where key.ActivationDate <= now + _maxServerToServerClockSkew
orderby key.ActivationDate descending orderby key.ActivationDate descending, key.KeyId ascending
select key).FirstOrDefault(); select key).FirstOrDefault();
if (keyMostRecentlyActivated != null) if (preferredDefaultKey != null)
{ {
if (_logger.IsVerboseLevelEnabled()) if (_logger.IsVerboseLevelEnabled())
{ {
_logger.LogVerbose("Considering key '{0:D}' with expiration date {1:u} as default key candidate.", keyMostRecentlyActivated.KeyId, keyMostRecentlyActivated.ExpirationDate); _logger.LogVerbose("Considering key '{0:D}' with expiration date {1:u} as default key.", preferredDefaultKey.KeyId, preferredDefaultKey.ExpirationDate);
} }
// 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 (keyMostRecentlyActivated.IsExpired(now) || keyMostRecentlyActivated.IsRevoked) if (preferredDefaultKey.IsExpired(now) || preferredDefaultKey.IsRevoked)
{ {
if (_logger.IsVerboseLevelEnabled()) if (_logger.IsVerboseLevelEnabled())
{ {
_logger.LogVerbose("Key '{0:D}' no longer eligible as default key candidate because it is expired or revoked.", keyMostRecentlyActivated.KeyId); _logger.LogVerbose("Key '{0:D}' is no longer under consideration as default key because it is expired or revoked.", preferredDefaultKey.KeyId);
} }
keyMostRecentlyActivated = null; preferredDefaultKey = null;
} }
} }
// There's an interesting edge case here. If two keys have an activation date in the past and // Only the key that has been most recently activated is eligible to be the preferred default,
// an expiration date in the future, and if the most recently activated of those two keys is // and only if it hasn't expired or been revoked. This is intentional: generating a new key is
// revoked, we won't consider the older key a valid candidate. This is intentional: generating // an implicit signal that we should stop using older keys (even if they're not revoked), so
// a new key is an implicit signal that we should stop using older keys without explicitly // activating a new key should permanently mark all older keys as non-preferred.
// revoking them.
// if the key's expiration is beyond our safety window, we can use this key if (preferredDefaultKey != null)
if (keyMostRecentlyActivated != null && keyMostRecentlyActivated.ExpirationDate - now > _keyGenBeforeExpirationWindow)
{ {
callerShouldGenerateNewKey = false; // Does *any* key in the key ring fulfill the requirement that its activation date is prior
return keyMostRecentlyActivated; // to the preferred default key's expiration date (allowing for skew) and that it will
} // remain valid one propagation cycle from now? If so, the caller doesn't need to add a
// new key.
callerShouldGenerateNewKey = !allKeys.Any(key =>
key.ActivationDate <= (preferredDefaultKey.ExpirationDate + _maxServerToServerClockSkew)
&& !key.IsExpired(now + _keyPropagationWindow)
&& !key.IsRevoked);
// the key with the nearest activation date where the activation date is in the future
// and the key isn't expired or revoked
IKey keyNextPendingActivation = (from key in allKeys
where key.ActivationDate > now && !key.IsExpired(now) && !key.IsRevoked
orderby key.ActivationDate ascending
select key).FirstOrDefault();
// if we have a valid current key, return it, and signal to the caller that he must perform
// the keygen step only if the next key pending activation won't be activated until *after*
// the current key expires (allowing for server-to-server skew)
if (keyMostRecentlyActivated != null)
{
callerShouldGenerateNewKey = (keyNextPendingActivation == null || (keyNextPendingActivation.ActivationDate - keyMostRecentlyActivated.ExpirationDate > _maxServerToServerClockSkew));
if (callerShouldGenerateNewKey && _logger.IsVerboseLevelEnabled()) if (callerShouldGenerateNewKey && _logger.IsVerboseLevelEnabled())
{ {
_logger.LogVerbose("Default key expiration imminent and repository contains no viable successor. Caller should generate a successor."); _logger.LogVerbose("Default key expiration imminent and repository contains no viable successor. Caller should generate a successor.");
} }
return keyMostRecentlyActivated; return preferredDefaultKey;
} }
// if there's no valid current key but there is a key pending activation, we can use // If we got this far, the caller must generate a key now.
// it only if its activation period is within the server-to-server clock skew
if (keyNextPendingActivation != null && keyNextPendingActivation.ActivationDate - now <= _maxServerToServerClockSkew)
{
if (_logger.IsVerboseLevelEnabled())
{
_logger.LogVerbose("Considering key '{0:D}' with expiration date {1:u} as default key candidate.", keyNextPendingActivation.KeyId, keyNextPendingActivation.ExpirationDate);
}
callerShouldGenerateNewKey = false;
return keyNextPendingActivation;
}
// if we got this far, there was no valid default key in the keyring
if (_logger.IsVerboseLevelEnabled()) if (_logger.IsVerboseLevelEnabled())
{ {
_logger.LogVerbose("Repository contains no viable default key. Caller should generate a key with immediate activation."); _logger.LogVerbose("Repository contains no viable default key. Caller should generate a key with immediate activation.");
} }
callerShouldGenerateNewKey = true; callerShouldGenerateNewKey = true;
return null; return null;
} }

View File

@ -5,41 +5,58 @@ using System;
namespace Microsoft.AspNet.DataProtection.KeyManagement namespace Microsoft.AspNet.DataProtection.KeyManagement
{ {
public class KeyLifetimeOptions /// <summary>
/// Options that control how an <see cref="IKeyManager"/> should behave.
/// </summary>
public class KeyManagementOptions
{ {
private readonly TimeSpan _keyExpirationSafetyPeriod = TimeSpan.FromDays(2); private static readonly TimeSpan _keyPropagationWindow = TimeSpan.FromDays(2);
private readonly TimeSpan _keyRingRefreshPeriod = TimeSpan.FromHours(24); private static readonly TimeSpan _keyRingRefreshPeriod = TimeSpan.FromHours(24);
private readonly TimeSpan _maxServerClockSkew = TimeSpan.FromMinutes(5); private static readonly TimeSpan _maxServerClockSkew = TimeSpan.FromMinutes(5);
private TimeSpan _newKeyLifetime = TimeSpan.FromDays(90); private TimeSpan _newKeyLifetime = TimeSpan.FromDays(90);
public KeyLifetimeOptions() public KeyManagementOptions()
{ {
} }
// copy ctor // copy ctor
internal KeyLifetimeOptions(KeyLifetimeOptions other) internal KeyManagementOptions(KeyManagementOptions other)
{ {
if (other != null) if (other != null)
{ {
this.AutoGenerateKeys = other.AutoGenerateKeys;
this._newKeyLifetime = other._newKeyLifetime; this._newKeyLifetime = other._newKeyLifetime;
} }
} }
/// <summary> /// <summary>
/// Specifies the period before key expiration in which a new key should be generated. /// Specifies whether the data protection system should auto-generate keys.
/// For example, if this period is 72 hours, then a new key will be created and /// </summary>
/// persisted to storage approximately 72 hours before expiration. /// <remarks>
/// If this value is 'false', the system will not generate new keys automatically.
/// The key ring must contain at least one active non-revoked key, otherwise calls
/// to <see cref="IDataProtector.Protect(byte[])"/> may fail. The system may end up
/// protecting payloads to expired keys if this property is set to 'false'.
/// The default value is 'true'.
/// </remarks>
public bool AutoGenerateKeys { get; set; } = true;
/// <summary>
/// Specifies the period before key expiration in which a new key should be generated
/// so that it has time to propagate fully throughout the key ring. For example, if this
/// period is 72 hours, then a new key will be created and persisted to storage
/// approximately 72 hours before expiration.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This value is currently fixed at 48 hours. /// This value is currently fixed at 48 hours.
/// </remarks> /// </remarks>
internal TimeSpan KeyExpirationSafetyPeriod internal TimeSpan KeyPropagationWindow
{ {
get get
{ {
// This value is not settable since there's a complex interaction between // This value is not settable since there's a complex interaction between
// it and the key ring refresh period. // it and the key ring refresh period.
return _keyExpirationSafetyPeriod; return _keyPropagationWindow;
} }
} }
@ -97,7 +114,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
{ {
if (value < TimeSpan.FromDays(7)) if (value < TimeSpan.FromDays(7))
{ {
throw new ArgumentOutOfRangeException(nameof(value), Resources.KeyLifetimeOptions_MinNewKeyLifetimeViolated); throw new ArgumentOutOfRangeException(nameof(value), Resources.KeyManagementOptions_MinNewKeyLifetimeViolated);
} }
_newKeyLifetime = value; _newKeyLifetime = value;
} }

View File

@ -17,20 +17,20 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
private readonly object _cacheableKeyRingLockObj = new object(); private readonly object _cacheableKeyRingLockObj = new object();
private readonly ICacheableKeyRingProvider _cacheableKeyRingProvider; private readonly ICacheableKeyRingProvider _cacheableKeyRingProvider;
private readonly IDefaultKeyResolver _defaultKeyResolver; private readonly IDefaultKeyResolver _defaultKeyResolver;
private readonly KeyLifetimeOptions _keyLifetimeOptions; private readonly KeyManagementOptions _keyManagementOptions;
private readonly IKeyManager _keyManager; private readonly IKeyManager _keyManager;
private readonly ILogger _logger; private readonly ILogger _logger;
public KeyRingProvider(IKeyManager keyManager, KeyLifetimeOptions keyLifetimeOptions, IServiceProvider services) public KeyRingProvider(IKeyManager keyManager, KeyManagementOptions keyManagementOptions, IServiceProvider services)
{ {
_keyLifetimeOptions = new KeyLifetimeOptions(keyLifetimeOptions); // clone so new instance is immutable _keyManagementOptions = new KeyManagementOptions(keyManagementOptions); // clone so new instance is immutable
_keyManager = keyManager; _keyManager = keyManager;
_cacheableKeyRingProvider = services?.GetService<ICacheableKeyRingProvider>() ?? this; _cacheableKeyRingProvider = services?.GetService<ICacheableKeyRingProvider>() ?? this;
_logger = services?.GetLogger<KeyRingProvider>(); _logger = services?.GetLogger<KeyRingProvider>();
_defaultKeyResolver = services?.GetService<IDefaultKeyResolver>() _defaultKeyResolver = services?.GetService<IDefaultKeyResolver>()
?? new DefaultKeyResolver(_keyLifetimeOptions.KeyExpirationSafetyPeriod, _keyLifetimeOptions.MaxServerClockSkew, services); ?? new DefaultKeyResolver(_keyManagementOptions.KeyPropagationWindow, _keyManagementOptions.MaxServerClockSkew, services);
} }
private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, bool allowRecursiveCalls = false) private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, bool allowRecursiveCalls = false)
{ {
// Refresh the list of all keys // Refresh the list of all keys
@ -67,17 +67,37 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
if (defaultKeyPolicy.DefaultKey == null) if (defaultKeyPolicy.DefaultKey == null)
{ {
// We cannot continue if we have no default key and auto-generation of keys is disabled.
if (!_keyManagementOptions.AutoGenerateKeys)
{
if (_logger.IsErrorLevelEnabled())
{
_logger.LogError("The key ring does not contain a valid default key, and the key manager is configured with auto-generation of keys disabled.");
}
throw new InvalidOperationException(Resources.KeyRingProvider_NoDefaultKey_AutoGenerateDisabled);
}
// 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 + _keyLifetimeOptions.NewKeyLifetime); _keyManager.CreateNewKey(activationDate: now, expirationDate: now + _keyManagementOptions.NewKeyLifetime);
return CreateCacheableKeyRingCore(now); // recursively call return CreateCacheableKeyRingCore(now); // recursively call
} }
else else
{ {
// If auto-generation of keys is disabled, we cannot call CreateNewKey.
if (!_keyManagementOptions.AutoGenerateKeys)
{
if (_logger.IsWarningLevelEnabled())
{
_logger.LogWarning("Policy resolution states that a new key should be added to the key ring, but automatic generation of keys is disabled.");
}
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, defaultKeyPolicy.DefaultKey, allKeys);
}
// 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 + _keyLifetimeOptions.NewKeyLifetime); _keyManager.CreateNewKey(activationDate: defaultKeyPolicy.DefaultKey.ExpirationDate, expirationDate: now + _keyManagementOptions.NewKeyLifetime);
return CreateCacheableKeyRingCore(now); // recursively call return CreateCacheableKeyRingCore(now); // recursively call
} }
} }
@ -96,7 +116,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
// servers in a cluster from trying to update the key ring simultaneously. // servers in a cluster from trying to update the key ring simultaneously.
return new CacheableKeyRing( return new CacheableKeyRing(
expirationToken: cacheExpirationToken, expirationToken: cacheExpirationToken,
expirationTime: Min(defaultKey.ExpirationDate, now + GetRefreshPeriodWithJitter(_keyLifetimeOptions.KeyRingRefreshPeriod)), expirationTime: Min(defaultKey.ExpirationDate, now + GetRefreshPeriodWithJitter(_keyManagementOptions.KeyRingRefreshPeriod)),
defaultKey: defaultKey, defaultKey: defaultKey,
allKeys: allKeys); allKeys: allKeys);
} }

View File

@ -219,19 +219,19 @@ namespace Microsoft.AspNet.DataProtection
} }
/// <summary> /// <summary>
/// The default new key lifetime must be at least one week. /// The new key lifetime must be at least one week.
/// </summary> /// </summary>
internal static string KeyLifetimeOptions_MinNewKeyLifetimeViolated internal static string KeyManagementOptions_MinNewKeyLifetimeViolated
{ {
get { return GetString("KeyLifetimeOptions_MinNewKeyLifetimeViolated"); } get { return GetString("KeyManagementOptions_MinNewKeyLifetimeViolated"); }
} }
/// <summary> /// <summary>
/// The default new key lifetime must be at least one week. /// The new key lifetime must be at least one week.
/// </summary> /// </summary>
internal static string FormatKeyLifetimeOptions_MinNewKeyLifetimeViolated() internal static string FormatKeyManagementOptions_MinNewKeyLifetimeViolated()
{ {
return GetString("KeyLifetimeOptions_MinNewKeyLifetimeViolated"); return GetString("KeyManagementOptions_MinNewKeyLifetimeViolated");
} }
/// <summary> /// <summary>
@ -378,6 +378,22 @@ namespace Microsoft.AspNet.DataProtection
return string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadKeySize"), p0); return string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadKeySize"), p0);
} }
/// <summary>
/// The key ring does not contain a valid default protection key. The data protection system cannot create a new key because auto-generation of keys is disabled.
/// </summary>
internal static string KeyRingProvider_NoDefaultKey_AutoGenerateDisabled
{
get { return GetString("KeyRingProvider_NoDefaultKey_AutoGenerateDisabled"); }
}
/// <summary>
/// The key ring does not contain a valid default protection key. The data protection system cannot create a new key because auto-generation of keys is disabled.
/// </summary>
internal static string FormatKeyRingProvider_NoDefaultKey_AutoGenerateDisabled()
{
return GetString("KeyRingProvider_NoDefaultKey_AutoGenerateDisabled");
}
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -156,8 +156,8 @@
<data name="TypeExtensions_BadCast" xml:space="preserve"> <data name="TypeExtensions_BadCast" xml:space="preserve">
<value>The type '{1}' is not assignable to '{0}'.</value> <value>The type '{1}' is not assignable to '{0}'.</value>
</data> </data>
<data name="KeyLifetimeOptions_MinNewKeyLifetimeViolated" xml:space="preserve"> <data name="KeyManagementOptions_MinNewKeyLifetimeViolated" xml:space="preserve">
<value>The default new key lifetime must be at least one week.</value> <value>The new key lifetime must be at least one week.</value>
</data> </data>
<data name="XmlKeyManager_DuplicateKey" xml:space="preserve"> <data name="XmlKeyManager_DuplicateKey" xml:space="preserve">
<value>The key '{0:D}' already exists in the keyring.</value> <value>The key '{0:D}' already exists in the keyring.</value>
@ -186,4 +186,7 @@
<data name="AlgorithmAssert_BadKeySize" xml:space="preserve"> <data name="AlgorithmAssert_BadKeySize" xml:space="preserve">
<value>The symmetric algorithm key size of {0} bits is invalid. The key size must be between 128 and 2048 bits, inclusive, and it must be a multiple of 8 bits.</value> <value>The symmetric algorithm key size of {0} bits is invalid. The key size must be between 128 and 2048 bits, inclusive, and it must be a multiple of 8 bits.</value>
</data> </data>
<data name="KeyRingProvider_NoDefaultKey_AutoGenerateDisabled" xml:space="preserve">
<value>The key ring does not contain a valid default protection key. The data protection system cannot create a new key because auto-generation of keys is disabled.</value>
</data>
</root> </root>

View File

@ -31,9 +31,10 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
// Arrange // Arrange
var resolver = CreateDefaultKeyResolver(); var resolver = CreateDefaultKeyResolver();
var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z"); var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
var key2 = CreateKey("2016-03-01 00:00:00Z", "2017-03-01 00:00:00Z");
// Act // Act
var resolution = resolver.ResolveDefaultKeyPolicy("2015-04-01 00:00:00Z", key1); var resolution = resolver.ResolveDefaultKeyPolicy("2016-02-20 23:59:00Z", key1, key2);
// Assert // Assert
Assert.Same(key1, resolution.DefaultKey); Assert.Same(key1, resolution.DefaultKey);
@ -41,15 +42,45 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
} }
[Fact] [Fact]
public void ResolveDefaultKeyPolicy_ValidExistingKey_ApproachingSafetyWindow_ReturnsExistingKey_SignalsGenerateNewKey() public void ResolveDefaultKeyPolicy_ValidExistingKey_AllowsForClockSkew_KeysStraddleSkewLine_ReturnsExistingKey()
{ {
// Arrange // Arrange
var resolver = CreateDefaultKeyResolver(); var resolver = CreateDefaultKeyResolver();
var key1 = CreateKey("2015-03-01 00:00:00Z", "2015-04-01 00:00:00Z"); var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
var key2 = CreateKey("2015-04-01 00:00:00Z", "2015-05-01 00:00:00Z", isRevoked: true); var key2 = CreateKey("2016-03-01 00:00:00Z", "2017-03-01 00:00:00Z");
// Act // Act
var resolution = resolver.ResolveDefaultKeyPolicy("2015-03-30 00:00:00Z", key1, key2); var resolution = resolver.ResolveDefaultKeyPolicy("2016-02-29 23:59:00Z", key1, key2);
// Assert
Assert.Same(key2, resolution.DefaultKey);
Assert.False(resolution.ShouldGenerateNewKey);
}
[Fact]
public void ResolveDefaultKeyPolicy_ValidExistingKey_AllowsForClockSkew_AllKeysInFuture_ReturnsExistingKey()
{
// Arrange
var resolver = CreateDefaultKeyResolver();
var key1 = CreateKey("2016-03-01 00:00:00Z", "2017-03-01 00:00:00Z");
// Act
var resolution = resolver.ResolveDefaultKeyPolicy("2016-02-29 23:59:00Z", key1);
// Assert
Assert.Same(key1, resolution.DefaultKey);
Assert.False(resolution.ShouldGenerateNewKey);
}
[Fact]
public void ResolveDefaultKeyPolicy_ValidExistingKey_NoSuccessor_ReturnsExistingKey_SignalsGenerateNewKey()
{
// Arrange
var resolver = CreateDefaultKeyResolver();
var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
// Act
var resolution = resolver.ResolveDefaultKeyPolicy("2016-02-29 23:59:00Z", key1);
// Assert // Assert
Assert.Same(key1, resolution.DefaultKey); Assert.Same(key1, resolution.DefaultKey);
@ -57,20 +88,20 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
} }
[Fact] [Fact]
public void ResolveDefaultKeyPolicy_ValidExistingKey_ApproachingSafetyWindow_FutureKeyIsValidAndWithinSkew_ReturnsExistingKey_NoSignalToGenerateNewKey() public void ResolveDefaultKeyPolicy_ValidExistingKey_NoLegitimateSuccessor_ReturnsExistingKey_SignalsGenerateNewKey()
{ {
// Arrange // Arrange
var resolver = CreateDefaultKeyResolver(); var resolver = CreateDefaultKeyResolver();
var key1 = CreateKey("2015-03-01 00:00:00Z", "2015-04-01 00:00:00Z"); var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
var key2 = CreateKey("2015-04-01 00:00:00Z", "2015-05-01 00:00:00Z", isRevoked: true); var key2 = CreateKey("2016-03-01 00:00:00Z", "2017-03-01 00:00:00Z", isRevoked: true);
var key3 = CreateKey("2015-04-01 00:01:00Z", "2015-05-01 00:00:00Z"); var key3 = CreateKey("2016-03-01 00:00:00Z", "2016-03-02 00:00:00Z"); // key expires too soon
// Act // Act
var resolution = resolver.ResolveDefaultKeyPolicy("2015-03-31 23:59:00Z", key1, key2, key3); var resolution = resolver.ResolveDefaultKeyPolicy("2016-02-29 23:50:00Z", key1, key2, key3);
// Assert // Assert
Assert.Same(key1, resolution.DefaultKey); Assert.Same(key1, resolution.DefaultKey);
Assert.False(resolution.ShouldGenerateNewKey); Assert.True(resolution.ShouldGenerateNewKey);
} }
[Fact] [Fact]
@ -139,7 +170,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
private static IDefaultKeyResolver CreateDefaultKeyResolver() private static IDefaultKeyResolver CreateDefaultKeyResolver()
{ {
return new DefaultKeyResolver( return new DefaultKeyResolver(
keyGenBeforeExpirationWindow: TimeSpan.FromDays(2), keyPropagationWindow: TimeSpan.FromDays(2),
maxServerToServerClockSkew: TimeSpan.FromMinutes(7), maxServerToServerClockSkew: TimeSpan.FromMinutes(7),
services: null); services: null);
} }

View File

@ -140,6 +140,40 @@ 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_KeyGenerationDisabled_Fails()
{
// Arrange
var callSequence = new List<string>();
var now = StringToDateTime("2015-03-01 00:00:00Z");
var allKeys = new IKey[0];
var keyRingProvider = SetupCreateCacheableKeyRingTestAndCreateKeyManager(
callSequence: callSequence,
getCacheExpirationTokenReturnValues: new[] { CancellationToken.None },
getAllKeysReturnValues: new[] { allKeys },
createNewKeyCallbacks: new[] {
Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90))
},
resolveDefaultKeyPolicyReturnValues: new[]
{
Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys, new DefaultKeyResolution()
{
DefaultKey = null,
ShouldGenerateNewKey = true
})
},
keyManagementOptions: new KeyManagementOptions() { AutoGenerateKeys = false });
// Act
var exception = Assert.Throws<InvalidOperationException>(() => keyRingProvider.GetCacheableKeyRing(now));
// Assert
Assert.Equal(Resources.KeyRingProvider_NoDefaultKey_AutoGenerateDisabled, exception.Message);
Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence);
}
[Fact] [Fact]
public void CreateCacheableKeyRing_GenerationRequired_WithDefaultKey_CreatesNewKeyWithDeferredActivationAndExpirationBasedOnCreationTime() public void CreateCacheableKeyRing_GenerationRequired_WithDefaultKey_CreatesNewKeyWithDeferredActivationAndExpirationBasedOnCreationTime()
{ {
@ -190,12 +224,51 @@ 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_WithDefaultKey_KeyGenerationDisabled_DoesNotCreateDefaultKey()
{
// Arrange
var callSequence = new List<string>();
var expirationCts = new CancellationTokenSource();
var now = StringToDateTime("2016-02-01 00:00:00Z");
var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
var allKeys = new[] { key1 };
var keyRingProvider = SetupCreateCacheableKeyRingTestAndCreateKeyManager(
callSequence: callSequence,
getCacheExpirationTokenReturnValues: new[] { expirationCts.Token },
getAllKeysReturnValues: new[] { allKeys },
createNewKeyCallbacks: null, // empty
resolveDefaultKeyPolicyReturnValues: new[]
{
Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys, new DefaultKeyResolution()
{
DefaultKey = key1,
ShouldGenerateNewKey = true
})
},
keyManagementOptions: new KeyManagementOptions() { AutoGenerateKeys = false });
// Act
var cacheableKeyRing = keyRingProvider.GetCacheableKeyRing(now);
// Assert
Assert.Equal(key1.KeyId, cacheableKeyRing.KeyRing.DefaultKeyId);
AssertWithinJitterRange(cacheableKeyRing.ExpirationTimeUtc, now);
Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now));
expirationCts.Cancel();
Assert.False(CacheableKeyRing.IsValid(cacheableKeyRing, now));
Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence);
}
private static ICacheableKeyRingProvider SetupCreateCacheableKeyRingTestAndCreateKeyManager( private static ICacheableKeyRingProvider SetupCreateCacheableKeyRingTestAndCreateKeyManager(
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>> createNewKeyCallbacks,
IEnumerable<Tuple<DateTimeOffset, IEnumerable<IKey>, DefaultKeyResolution>> resolveDefaultKeyPolicyReturnValues) IEnumerable<Tuple<DateTimeOffset, IEnumerable<IKey>, DefaultKeyResolution>> resolveDefaultKeyPolicyReturnValues,
KeyManagementOptions keyManagementOptions = null)
{ {
var getCacheExpirationTokenReturnValuesEnumerator = getCacheExpirationTokenReturnValues.GetEnumerator(); var getCacheExpirationTokenReturnValuesEnumerator = getCacheExpirationTokenReturnValues.GetEnumerator();
var mockKeyManager = new Mock<IKeyManager>(MockBehavior.Strict); var mockKeyManager = new Mock<IKeyManager>(MockBehavior.Strict);
@ -242,7 +315,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
return resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item3; return resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item3;
}); });
return CreateKeyRingProvider(mockKeyManager.Object, mockDefaultKeyResolver.Object); return CreateKeyRingProvider(mockKeyManager.Object, mockDefaultKeyResolver.Object, keyManagementOptions);
} }
[Fact] [Fact]
@ -359,17 +432,17 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
serviceCollection.AddInstance<ICacheableKeyRingProvider>(cacheableKeyRingProvider); serviceCollection.AddInstance<ICacheableKeyRingProvider>(cacheableKeyRingProvider);
return new KeyRingProvider( return new KeyRingProvider(
keyManager: null, keyManager: null,
keyLifetimeOptions: null, keyManagementOptions: null,
services: serviceCollection.BuildServiceProvider()); services: serviceCollection.BuildServiceProvider());
} }
private static ICacheableKeyRingProvider CreateKeyRingProvider(IKeyManager keyManager, IDefaultKeyResolver defaultKeyResolver) private static ICacheableKeyRingProvider CreateKeyRingProvider(IKeyManager keyManager, IDefaultKeyResolver defaultKeyResolver, KeyManagementOptions keyManagementOptions= null)
{ {
var serviceCollection = new ServiceCollection(); var serviceCollection = new ServiceCollection();
serviceCollection.AddInstance<IDefaultKeyResolver>(defaultKeyResolver); serviceCollection.AddInstance<IDefaultKeyResolver>(defaultKeyResolver);
return new KeyRingProvider( return new KeyRingProvider(
keyManager: keyManager, keyManager: keyManager,
keyLifetimeOptions: null, keyManagementOptions: keyManagementOptions,
services: serviceCollection.BuildServiceProvider()); services: serviceCollection.BuildServiceProvider());
} }

View File

@ -61,8 +61,8 @@ namespace Microsoft.AspNet.DataProtection
}); });
var services = serviceCollection.BuildServiceProvider(); var services = serviceCollection.BuildServiceProvider();
var keyLifetimeOptions = services.GetService<IOptions<KeyLifetimeOptions>>(); var keyManagementOptions = services.GetService<IOptions<KeyManagementOptions>>();
Assert.Equal(TimeSpan.FromDays(1024), keyLifetimeOptions.Options.NewKeyLifetime); Assert.Equal(TimeSpan.FromDays(1024), keyManagementOptions.Options.NewKeyLifetime);
} }
[ConditionalFact] [ConditionalFact]