Introduce concept of a "fallback key"
This key is used if there is no preferred default key and the developer has disabled automatic key generation. This will keep the service from falling over if the keys are not rolled and they all expire.
This commit is contained in:
Родитель
58c823bc45
Коммит
4f2288c3da
|
@ -12,6 +12,13 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
|
|||
/// </summary>
|
||||
public IKey DefaultKey;
|
||||
|
||||
/// <summary>
|
||||
/// The fallback key, which should be used only if the caller is configured not to
|
||||
/// honor the <see cref="ShouldGenerateNewKey"/> property. This property may
|
||||
/// be null if there is no viable fallback key.
|
||||
/// </summary>
|
||||
public IKey FallbackKey;
|
||||
|
||||
/// <summary>
|
||||
/// 'true' if a new key should be persisted to the keyring, 'false' otherwise.
|
||||
/// This value may be 'true' even if a valid default key was found.
|
||||
|
|
|
@ -46,11 +46,11 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
|
|||
public DefaultKeyResolution ResolveDefaultKeyPolicy(DateTimeOffset now, IEnumerable<IKey> allKeys)
|
||||
{
|
||||
DefaultKeyResolution retVal = default(DefaultKeyResolution);
|
||||
retVal.DefaultKey = FindDefaultKey(now, allKeys, out retVal.ShouldGenerateNewKey);
|
||||
retVal.DefaultKey = FindDefaultKey(now, allKeys, out retVal.FallbackKey, out retVal.ShouldGenerateNewKey);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private IKey FindDefaultKey(DateTimeOffset now, IEnumerable<IKey> allKeys, out bool callerShouldGenerateNewKey)
|
||||
private IKey FindDefaultKey(DateTimeOffset now, IEnumerable<IKey> allKeys, out IKey fallbackKey, out bool callerShouldGenerateNewKey)
|
||||
{
|
||||
// find the preferred default key (allowing for server-to-server clock skew)
|
||||
var preferredDefaultKey = (from key in allKeys
|
||||
|
@ -97,10 +97,23 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
|
|||
_logger.LogVerbose("Default key expiration imminent and repository contains no viable successor. Caller should generate a successor.");
|
||||
}
|
||||
|
||||
fallbackKey = null;
|
||||
return preferredDefaultKey;
|
||||
}
|
||||
|
||||
// If we got this far, the caller must generate a key now.
|
||||
// We should locate a fallback key, which is a key that can be used to protect payloads if
|
||||
// the caller is configured not to generate a new key. We should try to make sure the fallback
|
||||
// key has propagated to all callers (so its creation date should be before the previous
|
||||
// propagation period), and we cannot use revoked keys. The fallback key may be expired.
|
||||
fallbackKey = (from key in (from key in allKeys
|
||||
where key.CreationDate <= now - _keyPropagationWindow
|
||||
orderby key.CreationDate descending
|
||||
select key).Concat(from key in allKeys
|
||||
orderby key.CreationDate ascending
|
||||
select key)
|
||||
where !key.IsRevoked
|
||||
select key).FirstOrDefault();
|
||||
|
||||
if (_logger.IsVerboseLevelEnabled())
|
||||
{
|
||||
|
|
|
@ -65,10 +65,12 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
|
|||
throw CryptoUtil.Fail("Policy resolution states that a new key should be added to the key ring, even after a call to CreateNewKey.");
|
||||
}
|
||||
|
||||
if (defaultKeyPolicy.DefaultKey == null)
|
||||
// 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.
|
||||
if (!_keyManagementOptions.AutoGenerateKeys)
|
||||
{
|
||||
// We cannot continue if we have no default key and auto-generation of keys is disabled.
|
||||
if (!_keyManagementOptions.AutoGenerateKeys)
|
||||
var keyToUse = defaultKeyPolicy.DefaultKey ?? defaultKeyPolicy.FallbackKey;
|
||||
if (keyToUse == null)
|
||||
{
|
||||
if (_logger.IsErrorLevelEnabled())
|
||||
{
|
||||
|
@ -76,7 +78,18 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
|
|||
}
|
||||
throw new InvalidOperationException(Resources.KeyRingProvider_NoDefaultKey_AutoGenerateDisabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
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. Using fallback key '{0:D}' with expiration {1:u} as default key.", keyToUse.KeyId, keyToUse.ExpirationDate);
|
||||
}
|
||||
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, keyToUse, allKeys);
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultKeyPolicy.DefaultKey == null)
|
||||
{
|
||||
// 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 + _keyManagementOptions.NewKeyLifetime);
|
||||
|
@ -84,16 +97,6 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
|
|||
}
|
||||
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.
|
||||
|
@ -104,19 +107,25 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
|
|||
|
||||
private CacheableKeyRing CreateCacheableKeyRingCoreStep2(DateTimeOffset now, CancellationToken cacheExpirationToken, IKey defaultKey, IEnumerable<IKey> allKeys)
|
||||
{
|
||||
Debug.Assert(defaultKey != null);
|
||||
|
||||
if (_logger.IsVerboseLevelEnabled())
|
||||
{
|
||||
_logger.LogVerbose("Using key '{0:D}' as the default key.", defaultKey.KeyId);
|
||||
}
|
||||
|
||||
DateTimeOffset nextAutoRefreshTime = now + GetRefreshPeriodWithJitter(_keyManagementOptions.KeyRingRefreshPeriod);
|
||||
|
||||
// The cached keyring should expire at the earliest of (default key expiration, next auto-refresh time).
|
||||
// Since the refresh period and safety window are not user-settable, we can guarantee that there's at
|
||||
// least one auto-refresh between the start of the safety window and the key's expiration date.
|
||||
// This gives us an opportunity to update the key ring before expiration, and it prevents multiple
|
||||
// servers in a cluster from trying to update the key ring simultaneously.
|
||||
// servers in a cluster from trying to update the key ring simultaneously. Special case: if the default
|
||||
// key's expiration date is in the past, then we know we're using a fallback key and should disregard
|
||||
// its expiration date in favor of the next auto-refresh time.
|
||||
return new CacheableKeyRing(
|
||||
expirationToken: cacheExpirationToken,
|
||||
expirationTime: Min(defaultKey.ExpirationDate, now + GetRefreshPeriodWithJitter(_keyManagementOptions.KeyRingRefreshPeriod)),
|
||||
expirationTime: (defaultKey.ExpirationDate <= now) ? nextAutoRefreshTime : Min(defaultKey.ExpirationDate, nextAutoRefreshTime),
|
||||
defaultKey: defaultKey,
|
||||
allKeys: allKeys);
|
||||
}
|
||||
|
|
|
@ -167,6 +167,41 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
|
|||
Assert.False(resolution.ShouldGenerateNewKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveDefaultKeyPolicy_FallbackKey_SelectsLatestBeforePriorPropagationWindow()
|
||||
{
|
||||
// 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", isRevoked: 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]
|
||||
public void ResolveDefaultKeyPolicy_FallbackKey_NoNonRevokedKeysBeforePriorPropagationWindow_SelectsEarliestNonRevokedKey()
|
||||
{
|
||||
// Arrange
|
||||
var resolver = CreateDefaultKeyResolver();
|
||||
var key1 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-03 00:00:00Z", isRevoked: true);
|
||||
var key2 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-04 00:00:00Z");
|
||||
var key3 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-05 00:00:00Z");
|
||||
|
||||
// Act
|
||||
var resolution = resolver.ResolveDefaultKeyPolicy("2000-01-05 00:00:00Z", key1, key2, key3);
|
||||
|
||||
// Assert
|
||||
Assert.Same(key2, resolution.FallbackKey);
|
||||
Assert.True(resolution.ShouldGenerateNewKey);
|
||||
}
|
||||
|
||||
private static IDefaultKeyResolver CreateDefaultKeyResolver()
|
||||
{
|
||||
return new DefaultKeyResolver(
|
||||
|
@ -175,10 +210,11 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
|
|||
services: null);
|
||||
}
|
||||
|
||||
private static IKey CreateKey(string activationDate, string expirationDate, bool isRevoked = false)
|
||||
private static IKey CreateKey(string activationDate, string expirationDate, string creationDate = null, bool isRevoked = false)
|
||||
{
|
||||
var mockKey = new Mock<IKey>();
|
||||
mockKey.Setup(o => o.KeyId).Returns(Guid.NewGuid());
|
||||
mockKey.Setup(o => o.CreationDate).Returns((creationDate != null) ? DateTimeOffset.ParseExact(creationDate, "u", CultureInfo.InvariantCulture) : DateTimeOffset.MinValue);
|
||||
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.IsRevoked).Returns(isRevoked);
|
||||
|
|
|
@ -262,6 +262,44 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
|
|||
Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCacheableKeyRing_GenerationRequired_WithFallbackKey_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", "2015-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()
|
||||
{
|
||||
FallbackKey = 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<string> callSequence,
|
||||
IEnumerable<CancellationToken> getCacheExpirationTokenReturnValues,
|
||||
|
|
Загрузка…
Ссылка в новой задаче