Skip to content

Commit 726835d

Browse files
authored
Move computation of ShouldGenerateNewKey to KeyRingProvider (#54264)
* Move computation of ShouldGenerateNewKey to KeyRingProvider It used to be the case that part of IDefaultKeyResolver.ResolveDefaultKeyPolicy's job was to determine whether the current default key was close enough to expiration that a new one ought to be generated. This didn't make sense as the definition of "too close" depended upon the refresh period and propagation time of the ICacheableKeyRingProvider. That is, the IDefaultKeyResolver had to make assumptions about how often it would be polled for changed. The old logic was also very subtle and, as far as I was able to determine, slightly incorrect. Formerly, the presence of any key activated prior to the current default key's expiration date and not expiring during the next propagation cycle was considered an acceptable replacement. Several things seem strange about this: 1. The logic for finding a successor key is not the same as the logic for finding a preferred key (e.g. CanCreateAuthenticatedEncryptor is not checked) 2. The propagation window is counted forward from the current time, rather than backward from the expiration time 3. It's not immediately clear what happens if the successor key is unexpired at the end of the propagation window but expired before the default key's expiration time (maybe that's impossible or maybe that would be caught next refresh?) 4. As mentioned above, it doesn't seem like the resolver should know about the refresh period or make assumptions about how often it's called Now, the ICacheableKeyRingProvider is responsible for determining whether the returned default key is close enough to expiration that a new key should be generated. It checks whether the current time is within one propagation cycle of the expiration time, padding by an extra refresh period to account for the fact that we don't know where in the refresh cycle expiration will fall (i.e. so that we never generate a new key _less_ than a full propagation cycle ahead of when it's needed). Part of #53654 * Don't repeat the second resolution after key generation * Update comment * Add explanatory comment * Make comment more explicit
1 parent c935c96 commit 726835d

File tree

5 files changed

+271
-61
lines changed

5 files changed

+271
-61
lines changed

src/DataProtection/DataProtection/src/KeyManagement/DefaultKeyResolver.cs

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ private bool CanCreateAuthenticatedEncryptor(IKey key)
7171
}
7272
}
7373

74-
private IKey? FindDefaultKey(DateTimeOffset now, IEnumerable<IKey> allKeys, out IKey? fallbackKey, out bool callerShouldGenerateNewKey)
74+
private IKey? FindDefaultKey(DateTimeOffset now, IEnumerable<IKey> allKeys, out IKey? fallbackKey)
7575
{
7676
// find the preferred default key (allowing for server-to-server clock skew)
7777
var preferredDefaultKey = (from key in allKeys
@@ -87,59 +87,48 @@ private bool CanCreateAuthenticatedEncryptor(IKey key)
8787
if (preferredDefaultKey.IsRevoked || preferredDefaultKey.IsExpired(now) || !CanCreateAuthenticatedEncryptor(preferredDefaultKey))
8888
{
8989
_logger.KeyIsNoLongerUnderConsiderationAsDefault(preferredDefaultKey.KeyId);
90-
preferredDefaultKey = null;
9190
}
92-
}
93-
94-
// Only the key that has been most recently activated is eligible to be the preferred default,
95-
// and only if it hasn't expired or been revoked. This is intentional: generating a new key is
96-
// an implicit signal that we should stop using older keys (even if they're not revoked), so
97-
// activating a new key should permanently mark all older keys as non-preferred.
98-
99-
if (preferredDefaultKey != null)
100-
{
101-
// Does *any* key in the key ring fulfill the requirement that its activation date is prior
102-
// to the preferred default key's expiration date (allowing for skew) and that it will
103-
// remain valid one propagation cycle from now? If so, the caller doesn't need to add a
104-
// new key.
105-
callerShouldGenerateNewKey = !allKeys.Any(key =>
106-
key.ActivationDate <= (preferredDefaultKey.ExpirationDate + _maxServerToServerClockSkew)
107-
&& !key.IsExpired(now + _keyPropagationWindow)
108-
&& !key.IsRevoked);
109-
110-
if (callerShouldGenerateNewKey)
91+
else
11192
{
112-
_logger.DefaultKeyExpirationImminentAndRepository();
93+
fallbackKey = null;
94+
return preferredDefaultKey;
11395
}
114-
115-
fallbackKey = null;
116-
return preferredDefaultKey;
11796
}
11897

11998
// If we got this far, the caller must generate a key now.
12099
// We should locate a fallback key, which is a key that can be used to protect payloads if
121100
// the caller is configured not to generate a new key. We should try to make sure the fallback
122101
// key has propagated to all callers (so its creation date should be before the previous
123102
// propagation period), and we cannot use revoked keys. The fallback key may be expired.
124-
fallbackKey = (from key in (from key in allKeys
103+
104+
// Note that the two sort orders are opposite: we want the *newest* key that's old enough
105+
// (to have been propagated) or the *oldest* key that's too new.
106+
107+
// Unlike for the preferred key, we don't choose a fallback key and then reject it if
108+
// CanCreateAuthenticatedEncryptor is false. We want to end up with *some* key, so we
109+
// keep trying until we find one that works.
110+
var unrevokedKeys = allKeys.Where(key => !key.IsRevoked);
111+
fallbackKey = (from key in (from key in unrevokedKeys
125112
where key.CreationDate <= now - _keyPropagationWindow
126113
orderby key.CreationDate descending
127-
select key).Concat(from key in allKeys
114+
select key).Concat(from key in unrevokedKeys
115+
where key.CreationDate > now - _keyPropagationWindow
128116
orderby key.CreationDate ascending
129117
select key)
130-
where !key.IsRevoked && CanCreateAuthenticatedEncryptor(key)
118+
where CanCreateAuthenticatedEncryptor(key)
131119
select key).FirstOrDefault();
132120

133121
_logger.RepositoryContainsNoViableDefaultKey();
134122

135-
callerShouldGenerateNewKey = true;
136123
return null;
137124
}
138125

139126
public DefaultKeyResolution ResolveDefaultKeyPolicy(DateTimeOffset now, IEnumerable<IKey> allKeys)
140127
{
141128
var retVal = default(DefaultKeyResolution);
142-
retVal.DefaultKey = FindDefaultKey(now, allKeys, out retVal.FallbackKey, out retVal.ShouldGenerateNewKey);
129+
var defaultKey = FindDefaultKey(now, allKeys, out retVal.FallbackKey);
130+
retVal.DefaultKey = defaultKey;
131+
retVal.ShouldGenerateNewKey = defaultKey is null;
143132
return retVal;
144133
}
145134
}

src/DataProtection/DataProtection/src/KeyManagement/Internal/DefaultKeyResolution.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ public struct DefaultKeyResolution
3030
public IKey? FallbackKey;
3131

3232
/// <summary>
33-
/// 'true' if a new key should be persisted to the keyring, 'false' otherwise.
33+
/// True if the caller should generate and persist a new key to the keyring.
34+
/// False if the caller should determine for itself whether to generate a new key.
3435
/// This value may be 'true' even if a valid default key was found.
3536
/// </summary>
37+
/// <remarks>
38+
/// Does not reflect the time to expiration of the default key, if there is one.
39+
/// </remarks>
3640
public bool ShouldGenerateNewKey;
3741
}

src/DataProtection/DataProtection/src/KeyManagement/KeyRingProvider.cs

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,31 +66,56 @@ private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, IKey? ke
6666

6767
// Fetch the current default key from the list of all keys
6868
var defaultKeyPolicy = _defaultKeyResolver.ResolveDefaultKeyPolicy(now, allKeys);
69-
if (!defaultKeyPolicy.ShouldGenerateNewKey)
69+
var defaultKey = defaultKeyPolicy.DefaultKey;
70+
71+
// We shouldn't call CreateKey more than once, else we risk stack diving. Thus, we don't even
72+
// check defaultKeyPolicy.ShouldGenerateNewKey. However, this code path shouldn't get hit
73+
// with ShouldGenerateNewKey true unless there was an ineligible key with an activation date
74+
// slightly later than the one we just added. If this does happen, then we'll just use whatever
75+
// key we can instead of creating new keys endlessly, eventually falling back to the one we just
76+
// added if all else fails.
77+
if (keyJustAdded != null)
7078
{
71-
CryptoUtil.Assert(defaultKeyPolicy.DefaultKey != null, "Expected to see a default key.");
72-
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, defaultKeyPolicy.DefaultKey, allKeys);
79+
var keyToUse = defaultKey ?? defaultKeyPolicy.FallbackKey ?? keyJustAdded;
80+
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, keyToUse, allKeys);
7381
}
7482

75-
_logger.PolicyResolutionStatesThatANewKeyShouldBeAddedToTheKeyRing();
83+
// Determine whether we need to generate a new key
84+
bool shouldGenerateNewKey;
85+
if (defaultKeyPolicy.ShouldGenerateNewKey || defaultKey == null)
86+
{
87+
shouldGenerateNewKey = true;
88+
}
89+
else
90+
{
91+
// If we have a default key, we have to consider its expiration date. We have to generate a replacement
92+
// if it will expire within the propagation window starting now (so that all other consumers pick up the
93+
// replacement before the current default key expires). However, we also have to factor in the refresh
94+
// period, since we need to ensure that key generation occurs during the refresh that *precedes* the
95+
// propagation window ending at the expiration date.
96+
var minExpirationDate = now + KeyManagementOptions.KeyRingRefreshPeriod + KeyManagementOptions.KeyPropagationWindow;
97+
var defaultKeyExpirationDate = defaultKey.ExpirationDate;
98+
shouldGenerateNewKey =
99+
defaultKeyExpirationDate < minExpirationDate &&
100+
(_defaultKeyResolver.ResolveDefaultKeyPolicy(defaultKeyExpirationDate, allKeys).DefaultKey is not { } nextDefaultKey ||
101+
nextDefaultKey.ExpirationDate < minExpirationDate);
102+
}
76103

77-
// We shouldn't call CreateKey more than once, else we risk stack diving. This code path shouldn't
78-
// get hit unless there was an ineligible key with an activation date slightly later than the one we
79-
// just added. If this does happen, then we'll just use whatever key we can instead of creating
80-
// new keys endlessly, eventually falling back to the one we just added if all else fails.
81-
if (keyJustAdded != null)
104+
if (!shouldGenerateNewKey)
82105
{
83-
var keyToUse = defaultKeyPolicy.DefaultKey ?? defaultKeyPolicy.FallbackKey ?? keyJustAdded;
84-
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, keyToUse, allKeys);
106+
CryptoUtil.Assert(defaultKey != null, "Expected to see a default key.");
107+
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, defaultKey, allKeys);
85108
}
86109

110+
_logger.PolicyResolutionStatesThatANewKeyShouldBeAddedToTheKeyRing();
111+
87112
// At this point, we know we need to generate a new key.
88113

89114
// We have been asked to generate a new key, but auto-generation of keys has been disabled.
90115
// We need to use the fallback key or fail.
91116
if (!_keyManagementOptions.AutoGenerateKeys)
92117
{
93-
var keyToUse = defaultKeyPolicy.DefaultKey ?? defaultKeyPolicy.FallbackKey;
118+
var keyToUse = defaultKey ?? defaultKeyPolicy.FallbackKey;
94119
if (keyToUse == null)
95120
{
96121
_logger.KeyRingDoesNotContainValidDefaultKey();
@@ -103,7 +128,10 @@ private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, IKey? ke
103128
}
104129
}
105130

106-
if (defaultKeyPolicy.DefaultKey == null)
131+
// We're going to generate a new key. You'd think we could just take for granted what effect
132+
// this would have on the final result, but the key resolver is an extension point, so we have
133+
// to give it a chance to weigh in - hence the recursive call, triggering re-resolution.
134+
if (defaultKey == null)
107135
{
108136
// The case where there's no default key is the easiest scenario, since it
109137
// means that we need to create a new key with immediate activation.
@@ -115,7 +143,7 @@ private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, IKey? ke
115143
// If there is a default key, then the new key we generate should become active upon
116144
// expiration of the default key. The new key lifetime is measured from the creation
117145
// date (now), not the activation date.
118-
var newKey = _keyManager.CreateNewKey(activationDate: defaultKeyPolicy.DefaultKey.ExpirationDate, expirationDate: now + _keyManagementOptions.NewKeyLifetime);
146+
var newKey = _keyManager.CreateNewKey(activationDate: defaultKey.ExpirationDate, expirationDate: now + _keyManagementOptions.NewKeyLifetime);
119147
return CreateCacheableKeyRingCore(now, keyJustAdded: newKey); // recursively call
120148
}
121149
}

src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/KeyManagement/DefaultKeyResolverTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public void ResolveDefaultKeyPolicy_ValidExistingKey_AllowsForClockSkew_AllKeysI
7474
}
7575

7676
[Fact]
77-
public void ResolveDefaultKeyPolicy_ValidExistingKey_NoSuccessor_ReturnsExistingKey_SignalsGenerateNewKey()
77+
public void ResolveDefaultKeyPolicy_ValidExistingKey_NoSuccessor_ReturnsExistingKey_DoesNotSignalGenerateNewKey()
7878
{
7979
// Arrange
8080
var resolver = CreateDefaultKeyResolver();
@@ -85,11 +85,11 @@ public void ResolveDefaultKeyPolicy_ValidExistingKey_NoSuccessor_ReturnsExisting
8585

8686
// Assert
8787
Assert.Same(key1, resolution.DefaultKey);
88-
Assert.True(resolution.ShouldGenerateNewKey);
88+
Assert.False(resolution.ShouldGenerateNewKey); // Does not reflect pending expiration
8989
}
9090

9191
[Fact]
92-
public void ResolveDefaultKeyPolicy_ValidExistingKey_NoLegitimateSuccessor_ReturnsExistingKey_SignalsGenerateNewKey()
92+
public void ResolveDefaultKeyPolicy_ValidExistingKey_NoLegitimateSuccessor_ReturnsExistingKey_DoesNotSignalGenerateNewKey()
9393
{
9494
// Arrange
9595
var resolver = CreateDefaultKeyResolver();
@@ -102,7 +102,7 @@ public void ResolveDefaultKeyPolicy_ValidExistingKey_NoLegitimateSuccessor_Retur
102102

103103
// Assert
104104
Assert.Same(key1, resolution.DefaultKey);
105-
Assert.True(resolution.ShouldGenerateNewKey);
105+
Assert.False(resolution.ShouldGenerateNewKey); // Does not reflect pending expiration
106106
}
107107

108108
[Fact]

0 commit comments

Comments
 (0)