From 58c823bc45c37b0ebdd75fa64d6fb809a6beb046 Mon Sep 17 00:00:00 2001 From: Levi B Date: Wed, 11 Mar 2015 15:40:15 -0700 Subject: [PATCH] Rename KeyLifetimeOptions -> KeyManagementOptions Simplify default key resolution logic Introduce API for disabling automatic key generation --- .../DataProtectionConfiguration.cs | 47 +++++++---- .../DataProtectionProvider.cs | 2 +- .../DataProtectionServiceDescriptors.cs | 6 +- .../KeyManagement/DefaultKeyResolver.cs | 81 +++++++----------- ...timeOptions.cs => KeyManagementOptions.cs} | 41 ++++++--- .../KeyManagement/KeyRingProvider.cs | 36 ++++++-- .../Properties/Resources.Designer.cs | 28 +++++-- .../Resources.resx | 7 +- .../KeyManagement/DefaultKeyResolverTests.cs | 55 +++++++++--- .../KeyManagement/KeyRingProviderTests.cs | 83 +++++++++++++++++-- .../RegistryPolicyResolverTests.cs | 4 +- 11 files changed, 273 insertions(+), 117 deletions(-) rename src/Microsoft.AspNet.DataProtection/KeyManagement/{KeyLifetimeOptions.cs => KeyManagementOptions.cs} (64%) diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionConfiguration.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionConfiguration.cs index 2fa1164d04..e2350cd642 100644 --- a/src/Microsoft.AspNet.DataProtection/DataProtectionConfiguration.cs +++ b/src/Microsoft.AspNet.DataProtection/DataProtectionConfiguration.cs @@ -113,6 +113,36 @@ namespace Microsoft.AspNet.DataProtection return this; } + /// + /// Configures the data protection system not to generate new keys automatically. + /// + /// The 'this' instance. + /// + /// Calling this API corresponds to setting + /// to 'false'. See that property's documentation for more information. + /// + public DataProtectionConfiguration DisableAutomaticKeyGeneration() + { + Services.Configure(options => + { + options.AutoGenerateKeys = false; + }); + return this; + } + + /// + /// Configures the data protection system to persist keys in storage as plaintext. + /// + /// The 'this' instance. + /// + /// Caution: cryptographic key material will not be protected at rest. + /// + public DataProtectionConfiguration DisableProtectionOfKeysAtRest() + { + RemoveAllServicesOfType(typeof(IXmlEncryptor)); + return this; + } + /// /// 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. @@ -241,30 +271,17 @@ namespace Microsoft.AspNet.DataProtection /// Sets the default lifetime of keys created by the data protection system. /// /// The lifetime (time before expiration) for newly-created keys. - /// See for more information and + /// See for more information and /// usage notes. /// The 'this' instance. public DataProtectionConfiguration SetDefaultKeyLifetime(TimeSpan lifetime) { - Services.Configure(options => + Services.Configure(options => { options.NewKeyLifetime = lifetime; }); return this; } - - /// - /// Configures the data protection system to persist keys in storage as plaintext. - /// - /// The 'this' instance. - /// - /// Caution: cryptographic key material will not be protected at rest. - /// - public DataProtectionConfiguration SuppressProtectionOfKeysAtRest() - { - RemoveAllServicesOfType(typeof(IXmlEncryptor)); - return this; - } /// /// Configures the data protection system to use the specified cryptographic algorithms diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionProvider.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionProvider.cs index 20d42ee09e..de61cdd9f7 100644 --- a/src/Microsoft.AspNet.DataProtection/DataProtectionProvider.cs +++ b/src/Microsoft.AspNet.DataProtection/DataProtectionProvider.cs @@ -72,7 +72,7 @@ namespace Microsoft.AspNet.DataProtection { var keyRingProvider = new KeyRingProvider( keyManager: services.GetRequiredService(), - keyLifetimeOptions: services.GetService>()?.Options, // might be null + keyManagementOptions: services.GetService>()?.Options, // might be null services: services); dataProtectionProvider = new KeyRingBasedDataProtectionProvider(keyRingProvider, services); } diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs index 5a1c08ca29..43b94d65a5 100644 --- a/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs +++ b/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs @@ -39,14 +39,14 @@ namespace Microsoft.Framework.DependencyInjection } /// - /// An where the key lifetime is specified explicitly. + /// An where the key lifetime is specified explicitly. /// public static ServiceDescriptor ConfigureOptions_DefaultKeyLifetime(int numDays) { - return ServiceDescriptor.Transient>(services => + return ServiceDescriptor.Transient>(services => { - return new ConfigureOptions(options => + return new ConfigureOptions(options => { options.NewKeyLifetime = TimeSpan.FromDays(numDays); }); diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolver.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolver.cs index 624b23e53f..66beedb862 100644 --- a/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolver.cs +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolver.cs @@ -18,10 +18,10 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement /// and persisted to the keyring to ensure uninterrupted service. /// /// - /// 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. /// - private readonly TimeSpan _keyGenBeforeExpirationWindow; + private readonly TimeSpan _keyPropagationWindow; private readonly ILogger _logger; @@ -36,9 +36,9 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement /// 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; _logger = services.GetLogger(); } @@ -52,82 +52,61 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement private IKey FindDefaultKey(DateTimeOffset now, IEnumerable allKeys, out bool callerShouldGenerateNewKey) { - // the key with the most recent activation date where the activation date is in the past - IKey keyMostRecentlyActivated = (from key in allKeys - where key.ActivationDate <= now - orderby key.ActivationDate descending - select key).FirstOrDefault(); + // find the preferred default key (allowing for server-to-server clock skew) + var preferredDefaultKey = (from key in allKeys + where key.ActivationDate <= now + _maxServerToServerClockSkew + orderby key.ActivationDate descending, key.KeyId ascending + select key).FirstOrDefault(); - if (keyMostRecentlyActivated != null) + if (preferredDefaultKey != null) { 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 (keyMostRecentlyActivated.IsExpired(now) || keyMostRecentlyActivated.IsRevoked) + if (preferredDefaultKey.IsExpired(now) || preferredDefaultKey.IsRevoked) { 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 - // an expiration date in the future, and if the most recently activated of those two keys is - // revoked, we won't consider the older key a valid candidate. This is intentional: generating - // a new key is an implicit signal that we should stop using older keys without explicitly - // revoking them. + // Only the key that has been most recently activated is eligible to be the preferred default, + // and only if it hasn't expired or been revoked. This is intentional: generating a new key is + // an implicit signal that we should stop using older keys (even if they're not revoked), so + // activating a new key should permanently mark all older keys as non-preferred. - // if the key's expiration is beyond our safety window, we can use this key - if (keyMostRecentlyActivated != null && keyMostRecentlyActivated.ExpirationDate - now > _keyGenBeforeExpirationWindow) + if (preferredDefaultKey != null) { - callerShouldGenerateNewKey = false; - return keyMostRecentlyActivated; - } + // Does *any* key in the key ring fulfill the requirement that its activation date is prior + // 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()) { _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 - // 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); - } + // If we got this far, the caller must generate a key now. - callerShouldGenerateNewKey = false; - return keyNextPendingActivation; - } - - // if we got this far, there was no valid default key in the keyring if (_logger.IsVerboseLevelEnabled()) { _logger.LogVerbose("Repository contains no viable default key. Caller should generate a key with immediate activation."); } + callerShouldGenerateNewKey = true; return null; } diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyLifetimeOptions.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyManagementOptions.cs similarity index 64% rename from src/Microsoft.AspNet.DataProtection/KeyManagement/KeyLifetimeOptions.cs rename to src/Microsoft.AspNet.DataProtection/KeyManagement/KeyManagementOptions.cs index 7316cdb3f7..ae4d7479b5 100644 --- a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyLifetimeOptions.cs +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyManagementOptions.cs @@ -5,41 +5,58 @@ using System; namespace Microsoft.AspNet.DataProtection.KeyManagement { - public class KeyLifetimeOptions + /// + /// Options that control how an should behave. + /// + public class KeyManagementOptions { - private readonly TimeSpan _keyExpirationSafetyPeriod = TimeSpan.FromDays(2); - private readonly TimeSpan _keyRingRefreshPeriod = TimeSpan.FromHours(24); - private readonly TimeSpan _maxServerClockSkew = TimeSpan.FromMinutes(5); + private static readonly TimeSpan _keyPropagationWindow = TimeSpan.FromDays(2); + private static readonly TimeSpan _keyRingRefreshPeriod = TimeSpan.FromHours(24); + private static readonly TimeSpan _maxServerClockSkew = TimeSpan.FromMinutes(5); private TimeSpan _newKeyLifetime = TimeSpan.FromDays(90); - public KeyLifetimeOptions() + public KeyManagementOptions() { } // copy ctor - internal KeyLifetimeOptions(KeyLifetimeOptions other) + internal KeyManagementOptions(KeyManagementOptions other) { if (other != null) { + this.AutoGenerateKeys = other.AutoGenerateKeys; this._newKeyLifetime = other._newKeyLifetime; } } /// - /// Specifies the period before key expiration in which a new key should be generated. - /// For example, if this period is 72 hours, then a new key will be created and - /// persisted to storage approximately 72 hours before expiration. + /// Specifies whether the data protection system should auto-generate keys. + /// + /// + /// 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 may fail. The system may end up + /// protecting payloads to expired keys if this property is set to 'false'. + /// The default value is 'true'. + /// + public bool AutoGenerateKeys { get; set; } = true; + + /// + /// 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. /// /// /// This value is currently fixed at 48 hours. /// - internal TimeSpan KeyExpirationSafetyPeriod + internal TimeSpan KeyPropagationWindow { get { // This value is not settable since there's a complex interaction between // 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)) { - throw new ArgumentOutOfRangeException(nameof(value), Resources.KeyLifetimeOptions_MinNewKeyLifetimeViolated); + throw new ArgumentOutOfRangeException(nameof(value), Resources.KeyManagementOptions_MinNewKeyLifetimeViolated); } _newKeyLifetime = value; } diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingProvider.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingProvider.cs index ec8c878c04..475ffda929 100644 --- a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingProvider.cs +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingProvider.cs @@ -17,20 +17,20 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement private readonly object _cacheableKeyRingLockObj = new object(); private readonly ICacheableKeyRingProvider _cacheableKeyRingProvider; private readonly IDefaultKeyResolver _defaultKeyResolver; - private readonly KeyLifetimeOptions _keyLifetimeOptions; + private readonly KeyManagementOptions _keyManagementOptions; private readonly IKeyManager _keyManager; 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; _cacheableKeyRingProvider = services?.GetService() ?? this; _logger = services?.GetLogger(); _defaultKeyResolver = services?.GetService() - ?? new DefaultKeyResolver(_keyLifetimeOptions.KeyExpirationSafetyPeriod, _keyLifetimeOptions.MaxServerClockSkew, services); + ?? new DefaultKeyResolver(_keyManagementOptions.KeyPropagationWindow, _keyManagementOptions.MaxServerClockSkew, services); } - + private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, bool allowRecursiveCalls = false) { // Refresh the list of all keys @@ -67,17 +67,37 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement 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 // 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 } 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 // expiration of the default key. The new key lifetime is measured from the creation // 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 } } @@ -96,7 +116,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement // servers in a cluster from trying to update the key ring simultaneously. return new CacheableKeyRing( expirationToken: cacheExpirationToken, - expirationTime: Min(defaultKey.ExpirationDate, now + GetRefreshPeriodWithJitter(_keyLifetimeOptions.KeyRingRefreshPeriod)), + expirationTime: Min(defaultKey.ExpirationDate, now + GetRefreshPeriodWithJitter(_keyManagementOptions.KeyRingRefreshPeriod)), defaultKey: defaultKey, allKeys: allKeys); } diff --git a/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs index fad1928f16..9edb8c1b05 100644 --- a/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs @@ -219,19 +219,19 @@ namespace Microsoft.AspNet.DataProtection } /// - /// The default new key lifetime must be at least one week. + /// The new key lifetime must be at least one week. /// - internal static string KeyLifetimeOptions_MinNewKeyLifetimeViolated + internal static string KeyManagementOptions_MinNewKeyLifetimeViolated { - get { return GetString("KeyLifetimeOptions_MinNewKeyLifetimeViolated"); } + get { return GetString("KeyManagementOptions_MinNewKeyLifetimeViolated"); } } /// - /// The default new key lifetime must be at least one week. + /// The new key lifetime must be at least one week. /// - internal static string FormatKeyLifetimeOptions_MinNewKeyLifetimeViolated() + internal static string FormatKeyManagementOptions_MinNewKeyLifetimeViolated() { - return GetString("KeyLifetimeOptions_MinNewKeyLifetimeViolated"); + return GetString("KeyManagementOptions_MinNewKeyLifetimeViolated"); } /// @@ -378,6 +378,22 @@ namespace Microsoft.AspNet.DataProtection return string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadKeySize"), p0); } + /// + /// 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. + /// + internal static string KeyRingProvider_NoDefaultKey_AutoGenerateDisabled + { + get { return GetString("KeyRingProvider_NoDefaultKey_AutoGenerateDisabled"); } + } + + /// + /// 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. + /// + internal static string FormatKeyRingProvider_NoDefaultKey_AutoGenerateDisabled() + { + return GetString("KeyRingProvider_NoDefaultKey_AutoGenerateDisabled"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.DataProtection/Resources.resx b/src/Microsoft.AspNet.DataProtection/Resources.resx index ad1f4512df..3562a6a959 100644 --- a/src/Microsoft.AspNet.DataProtection/Resources.resx +++ b/src/Microsoft.AspNet.DataProtection/Resources.resx @@ -156,8 +156,8 @@ The type '{1}' is not assignable to '{0}'. - - The default new key lifetime must be at least one week. + + The new key lifetime must be at least one week. The key '{0:D}' already exists in the keyring. @@ -186,4 +186,7 @@ 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. + + 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. + \ No newline at end of file diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs index 7c66fdc3e0..3eed6faa19 100644 --- a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs +++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs @@ -31,9 +31,10 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement // Arrange var resolver = CreateDefaultKeyResolver(); 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 - 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.Same(key1, resolution.DefaultKey); @@ -41,15 +42,45 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement } [Fact] - public void ResolveDefaultKeyPolicy_ValidExistingKey_ApproachingSafetyWindow_ReturnsExistingKey_SignalsGenerateNewKey() + public void ResolveDefaultKeyPolicy_ValidExistingKey_AllowsForClockSkew_KeysStraddleSkewLine_ReturnsExistingKey() { // Arrange var resolver = CreateDefaultKeyResolver(); - var key1 = CreateKey("2015-03-01 00:00:00Z", "2015-04-01 00:00:00Z"); - var key2 = CreateKey("2015-04-01 00:00:00Z", "2015-05-01 00:00:00Z", isRevoked: true); + 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 - 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.Same(key1, resolution.DefaultKey); @@ -57,20 +88,20 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement } [Fact] - public void ResolveDefaultKeyPolicy_ValidExistingKey_ApproachingSafetyWindow_FutureKeyIsValidAndWithinSkew_ReturnsExistingKey_NoSignalToGenerateNewKey() + public void ResolveDefaultKeyPolicy_ValidExistingKey_NoLegitimateSuccessor_ReturnsExistingKey_SignalsGenerateNewKey() { // Arrange var resolver = CreateDefaultKeyResolver(); - var key1 = CreateKey("2015-03-01 00:00:00Z", "2015-04-01 00:00:00Z"); - var key2 = CreateKey("2015-04-01 00:00:00Z", "2015-05-01 00:00:00Z", isRevoked: true); - var key3 = CreateKey("2015-04-01 00:01:00Z", "2015-05-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", isRevoked: true); + var key3 = CreateKey("2016-03-01 00:00:00Z", "2016-03-02 00:00:00Z"); // key expires too soon // 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.Same(key1, resolution.DefaultKey); - Assert.False(resolution.ShouldGenerateNewKey); + Assert.True(resolution.ShouldGenerateNewKey); } [Fact] @@ -139,7 +170,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement private static IDefaultKeyResolver CreateDefaultKeyResolver() { return new DefaultKeyResolver( - keyGenBeforeExpirationWindow: TimeSpan.FromDays(2), + keyPropagationWindow: TimeSpan.FromDays(2), maxServerToServerClockSkew: TimeSpan.FromMinutes(7), services: null); } diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs index b117c9e215..6bb7ab2c0c 100644 --- a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs +++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs @@ -140,6 +140,40 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy", "CreateNewKey", "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence); } + [Fact] + public void CreateCacheableKeyRing_GenerationRequired_NoDefaultKey_KeyGenerationDisabled_Fails() + { + // Arrange + var callSequence = new List(); + + 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)allKeys, new DefaultKeyResolution() + { + DefaultKey = null, + ShouldGenerateNewKey = true + }) + }, + keyManagementOptions: new KeyManagementOptions() { AutoGenerateKeys = false }); + + // Act + var exception = Assert.Throws(() => keyRingProvider.GetCacheableKeyRing(now)); + + // Assert + Assert.Equal(Resources.KeyRingProvider_NoDefaultKey_AutoGenerateDisabled, exception.Message); + Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence); + } + [Fact] 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); } + [Fact] + public void CreateCacheableKeyRing_GenerationRequired_WithDefaultKey_KeyGenerationDisabled_DoesNotCreateDefaultKey() + { + // Arrange + var callSequence = new List(); + 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)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( IList callSequence, IEnumerable getCacheExpirationTokenReturnValues, IEnumerable> getAllKeysReturnValues, IEnumerable> createNewKeyCallbacks, - IEnumerable, DefaultKeyResolution>> resolveDefaultKeyPolicyReturnValues) + IEnumerable, DefaultKeyResolution>> resolveDefaultKeyPolicyReturnValues, + KeyManagementOptions keyManagementOptions = null) { var getCacheExpirationTokenReturnValuesEnumerator = getCacheExpirationTokenReturnValues.GetEnumerator(); var mockKeyManager = new Mock(MockBehavior.Strict); @@ -242,7 +315,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement return resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item3; }); - return CreateKeyRingProvider(mockKeyManager.Object, mockDefaultKeyResolver.Object); + return CreateKeyRingProvider(mockKeyManager.Object, mockDefaultKeyResolver.Object, keyManagementOptions); } [Fact] @@ -359,17 +432,17 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement serviceCollection.AddInstance(cacheableKeyRingProvider); return new KeyRingProvider( keyManager: null, - keyLifetimeOptions: null, + keyManagementOptions: null, 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(); serviceCollection.AddInstance(defaultKeyResolver); return new KeyRingProvider( keyManager: keyManager, - keyLifetimeOptions: null, + keyManagementOptions: keyManagementOptions, services: serviceCollection.BuildServiceProvider()); } diff --git a/test/Microsoft.AspNet.DataProtection.Test/RegistryPolicyResolverTests.cs b/test/Microsoft.AspNet.DataProtection.Test/RegistryPolicyResolverTests.cs index 12f2818957..8dcb0424eb 100644 --- a/test/Microsoft.AspNet.DataProtection.Test/RegistryPolicyResolverTests.cs +++ b/test/Microsoft.AspNet.DataProtection.Test/RegistryPolicyResolverTests.cs @@ -61,8 +61,8 @@ namespace Microsoft.AspNet.DataProtection }); var services = serviceCollection.BuildServiceProvider(); - var keyLifetimeOptions = services.GetService>(); - Assert.Equal(TimeSpan.FromDays(1024), keyLifetimeOptions.Options.NewKeyLifetime); + var keyManagementOptions = services.GetService>(); + Assert.Equal(TimeSpan.FromDays(1024), keyManagementOptions.Options.NewKeyLifetime); } [ConditionalFact]