-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Make the key ring cache expire shortly after an immediately-activated key is generated #54517
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dbf8dc2
26411c1
69ab2a8
5a6af04
a1ef3f9
be322f0
5daf76f
e7bf0ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace Microsoft.AspNetCore.DataProtection.KeyManagement.Internal; | ||
|
||
/// <summary> | ||
/// This API supports infrastructure and is not intended to be used | ||
/// directly from your code. This API may change or be removed in future releases. | ||
/// </summary> | ||
/// <remarks> | ||
/// Replaces <see cref="ICacheableKeyRingProvider"/> as a test hook for validating | ||
/// <see cref="KeyRingProvider"/>. | ||
/// </remarks> | ||
internal interface IInternalCacheableKeyRingProvider | ||
{ | ||
/// <summary> | ||
/// This API supports infrastructure and is not intended to be used | ||
/// directly from your code. This API may change or be removed in future releases. | ||
/// </summary> | ||
CacheableKeyRing GetCacheableKeyRing(DateTimeOffset now, bool allowShortRefreshPeriod = true); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ | |
|
||
namespace Microsoft.AspNetCore.DataProtection.KeyManagement; | ||
|
||
internal sealed class KeyRingProvider : ICacheableKeyRingProvider, IKeyRingProvider | ||
internal sealed class KeyRingProvider : ICacheableKeyRingProvider, IInternalCacheableKeyRingProvider, IKeyRingProvider | ||
{ | ||
private CacheableKeyRing? _cacheableKeyRing; | ||
private readonly object _cacheableKeyRingLockObj = new object(); | ||
|
@@ -55,13 +55,16 @@ public KeyRingProvider( | |
} | ||
|
||
// for testing | ||
internal ICacheableKeyRingProvider CacheableKeyRingProvider { get; set; } | ||
internal CacheableKeyRing? CacheableKeyRing => Volatile.Read(ref _cacheableKeyRing); | ||
|
||
// for testing | ||
internal IInternalCacheableKeyRingProvider CacheableKeyRingProvider { get; set; } | ||
|
||
internal DateTime AutoRefreshWindowEnd { get; set; } | ||
|
||
internal bool InAutoRefreshWindow() => DateTime.UtcNow < AutoRefreshWindowEnd; | ||
|
||
private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, IKey? keyJustAdded) | ||
private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, bool allowShortRefreshPeriod, IKey? keyJustAdded) | ||
{ | ||
// Refresh the list of all keys | ||
var cacheExpirationToken = _keyManager.GetCacheExpirationToken(); | ||
|
@@ -80,7 +83,7 @@ private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, IKey? ke | |
if (keyJustAdded != null) | ||
{ | ||
var keyToUse = defaultKey ?? defaultKeyPolicy.FallbackKey ?? keyJustAdded; | ||
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, keyToUse, allKeys); | ||
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, keyToUse, keyJustAdded, allowShortRefreshPeriod, allKeys); | ||
} | ||
|
||
// Determine whether we need to generate a new key | ||
|
@@ -107,7 +110,7 @@ private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, IKey? ke | |
if (!shouldGenerateNewKey) | ||
{ | ||
CryptoUtil.Assert(defaultKey != null, "Expected to see a default key."); | ||
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, defaultKey, allKeys); | ||
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, defaultKey, keyJustAdded, allowShortRefreshPeriod, allKeys); | ||
} | ||
|
||
_logger.PolicyResolutionStatesThatANewKeyShouldBeAddedToTheKeyRing(); | ||
|
@@ -127,7 +130,7 @@ private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, IKey? ke | |
else | ||
{ | ||
_logger.UsingFallbackKeyWithExpirationAsDefaultKey(keyToUse.KeyId, keyToUse.ExpirationDate); | ||
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, keyToUse, allKeys); | ||
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, keyToUse, keyJustAdded, allowShortRefreshPeriod, allKeys); | ||
} | ||
} | ||
|
||
|
@@ -139,19 +142,19 @@ private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, IKey? ke | |
// 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. | ||
var newKey = _keyManager.CreateNewKey(activationDate: now, expirationDate: now + _newKeyLifetime); | ||
return CreateCacheableKeyRingCore(now, keyJustAdded: newKey); // recursively call | ||
return CreateCacheableKeyRingCore(now, allowShortRefreshPeriod, keyJustAdded: newKey); // recursively call | ||
} | ||
else | ||
{ | ||
// 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. | ||
var newKey = _keyManager.CreateNewKey(activationDate: defaultKey.ExpirationDate, expirationDate: now + _newKeyLifetime); | ||
return CreateCacheableKeyRingCore(now, keyJustAdded: newKey); // recursively call | ||
return CreateCacheableKeyRingCore(now, allowShortRefreshPeriod, keyJustAdded: newKey); // recursively call | ||
} | ||
} | ||
|
||
private CacheableKeyRing CreateCacheableKeyRingCoreStep2(DateTimeOffset now, CancellationToken cacheExpirationToken, IKey defaultKey, IEnumerable<IKey> allKeys) | ||
private CacheableKeyRing CreateCacheableKeyRingCoreStep2(DateTimeOffset now, CancellationToken cacheExpirationToken, IKey defaultKey, IKey? generatedKey, bool allowShortRefreshPeriod, IEnumerable<IKey> allKeys) | ||
{ | ||
Debug.Assert(defaultKey != null); | ||
|
||
|
@@ -167,7 +170,25 @@ private CacheableKeyRing CreateCacheableKeyRingCoreStep2(DateTimeOffset now, Can | |
|
||
_logger.UsingKeyAsDefaultKey(defaultKey.KeyId); | ||
|
||
var nextAutoRefreshTime = now + GetRefreshPeriodWithJitter(KeyManagementOptions.KeyRingRefreshPeriod); | ||
// If we're in a scenario where we think other app instances may be generating keys that will be | ||
// activated before they are propagated, we'll use a shorter refresh period to ensure that we pick | ||
// up those keys so that we can select from the same pool of candidates as the other instances. | ||
// | ||
// These not-yet-propagated keys are usually immediately-activated, but an app restarted after a period | ||
// of downtime may discover that it has a valid, but soon-to-be-expired key. The replacement will not | ||
// be immediately-activated, but may be activated before it has propagated. | ||
var useShortRefreshPeriod = allowShortRefreshPeriod && | ||
(generatedKey is not null | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Move the second clause of this Boolean to a well-defined method (e.g. |
||
// Either we had to generate a key and it will be activated before it has time to propagate | ||
// (in which case, other instances may have done the same) | ||
? (generatedKey.ActivationDate < now + KeyManagementOptions.KeyPropagationWindow) // No clock skew on a key we generated | ||
// Or we selected a key that has yet to propagate (presumably, from another instance) | ||
: (defaultKey.CreationDate > now - KeyManagementOptions.KeyPropagationWindow)); | ||
|
||
var nextAutoRefreshTime = now + GetRefreshPeriodWithJitter( | ||
useShortRefreshPeriod | ||
? KeyManagementOptions.ShortKeyRingRefreshPeriod | ||
: 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 | ||
|
@@ -179,8 +200,8 @@ private CacheableKeyRing CreateCacheableKeyRingCoreStep2(DateTimeOffset now, Can | |
return new CacheableKeyRing( | ||
expirationToken: cacheExpirationToken, | ||
expirationTime: (defaultKey.ExpirationDate <= now) ? nextAutoRefreshTime : Min(defaultKey.ExpirationDate, nextAutoRefreshTime), | ||
defaultKey: defaultKey, | ||
allKeys: allKeys); | ||
hasShortRefreshPeriod: useShortRefreshPeriod, | ||
keyRing: new KeyRing(defaultKey, allKeys)); | ||
} | ||
|
||
public IKeyRing GetCurrentKeyRing() | ||
|
@@ -241,7 +262,7 @@ internal IKeyRing GetCurrentKeyRingCore(DateTime utcNow, bool forceRefresh = fal | |
|
||
try | ||
{ | ||
newCacheableKeyRing = CacheableKeyRingProvider.GetCacheableKeyRing(utcNow); | ||
newCacheableKeyRing = CacheableKeyRingProvider.GetCacheableKeyRing(utcNow, allowShortRefreshPeriod: existingCacheableKeyRing?.HasShortRefreshPeriod != true); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a nullable bool There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AppContext switch here? |
||
} | ||
catch (Exception ex) | ||
{ | ||
|
@@ -311,9 +332,18 @@ private static DateTimeOffset Min(DateTimeOffset a, DateTimeOffset b) | |
return (a < b) ? a : b; | ||
} | ||
|
||
// This is only here to support external consumers of our internal test hook. | ||
// The object returned has no public properties, so we could probably return a dummy | ||
// without doing any work, but maybe they're calling it for side-effects? | ||
CacheableKeyRing ICacheableKeyRingProvider.GetCacheableKeyRing(DateTimeOffset now) | ||
{ | ||
// the entry point allows one recursive call | ||
return CreateCacheableKeyRingCore(now, keyJustAdded: null); | ||
return CreateCacheableKeyRingCore(now, allowShortRefreshPeriod: false, keyJustAdded: null); | ||
} | ||
|
||
CacheableKeyRing IInternalCacheableKeyRingProvider.GetCacheableKeyRing(DateTimeOffset now, bool allowShortRefreshPeriod) | ||
{ | ||
// the entry point allows one recursive call | ||
return CreateCacheableKeyRingCore(now, allowShortRefreshPeriod, keyJustAdded: null); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any guidance here on what the minimum refresh period should be or why 15 seconds is a sane default?