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:
Levi B 2015-03-11 18:56:46 -07:00
Родитель 58c823bc45
Коммит 4f2288c3da
5 изменённых файлов: 121 добавлений и 18 удалений

Просмотреть файл

@ -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,