Skip to content

Commit ad9ddd8

Browse files
authored
Revert "Prefer keys old enough to have propagated (#54309)" (#57186)
* Revert "Prefer keys old enough to have propagated (#54309)" This reverts commit e72eca7. It was a speculative improvement and now we have concrete evidence of (minor) problems. Fixes #57137 * Add a targeted regression test
1 parent a9e17d0 commit ad9ddd8

File tree

2 files changed

+29
-52
lines changed

2 files changed

+29
-52
lines changed

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

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -147,26 +147,20 @@ private bool CanCreateAuthenticatedEncryptor(IKey key, ref int retriesRemaining)
147147

148148
private IKey? FindDefaultKey(DateTimeOffset now, IEnumerable<IKey> allKeys, out IKey? fallbackKey)
149149
{
150-
// Keys created before this time should have propagated to all instances.
151-
var propagationCutoff = now - _keyPropagationWindow;
152-
153-
// Prefer the most recently activated key that's old enough to have propagated to all instances.
154-
// If no such key exists, fall back to the *least* recently activated key that's too new to have
155-
// propagated to all instances.
156-
157-
// An unpropagated key can still be preferred insofar as we wouldn't want to generate a replacement
158-
// for it (as the replacement would also be unpropagated).
159-
160-
// Note that the two sort orders are opposite: we want the *newest* key that's old enough
161-
// (to have been propagated) or the *oldest* key that's too new.
162-
var activatedKeys = allKeys.Where(key => key.ActivationDate <= now + _maxServerToServerClockSkew);
163-
var preferredDefaultKey = (from key in activatedKeys
164-
where key.CreationDate <= propagationCutoff
150+
// find the preferred default key (allowing for server-to-server clock skew)
151+
152+
// Note: another approach would be to prefer the *least-recently* activated key if none
153+
// are old enough to have been propagated. We tried that and reverted it because it
154+
// made us less resilient against bad keys. This way, we'll reject a bad key and generate
155+
// a replacement and then the next instance to run will pick up the replacement, rather
156+
// than the bad key. It did have the conceptual advantage of being more similar to the
157+
// fallback code below and the hypothetical advantage of making it easier for instances
158+
// to choose the same key in the event of a race (though we never managed to show that
159+
// empirically. See also https://github.com/dotnet/aspnetcore/issues/57137.
160+
var preferredDefaultKey = (from key in allKeys
161+
where key.ActivationDate <= now + _maxServerToServerClockSkew
165162
orderby key.ActivationDate descending, key.KeyId ascending
166-
select key).Concat(from key in activatedKeys
167-
where key.CreationDate > propagationCutoff
168-
orderby key.ActivationDate ascending, key.KeyId ascending
169-
select key).FirstOrDefault();
163+
select key).FirstOrDefault();
170164

171165
var decryptRetriesRemaining = _maxDecryptRetries;
172166

@@ -192,18 +186,19 @@ where key.CreationDate > propagationCutoff
192186
// key has propagated to all callers (so its creation date should be before the previous
193187
// propagation period), and we cannot use revoked keys. The fallback key may be expired.
194188

195-
// As above, the two sort orders are opposite.
189+
// Note that the two sort orders are opposite: we want the *newest* key that's old enough
190+
// (to have been propagated) or the *oldest* key that's too new.
196191

197192
// Unlike for the preferred key, we don't choose a fallback key and then reject it if
198193
// CanCreateAuthenticatedEncryptor is false. We want to end up with *some* key, so we
199194
// keep trying until we find one that works.
200195
var unrevokedKeys = allKeys.Where(key => !key.IsRevoked);
201196
fallbackKey = (from key in (from key in unrevokedKeys
202197
where !ReferenceEquals(key, preferredDefaultKey) // Don't reconsider it as a fallback
203-
where key.CreationDate <= propagationCutoff
198+
where key.CreationDate <= now - _keyPropagationWindow
204199
orderby key.CreationDate descending
205200
select key).Concat(from key in unrevokedKeys
206-
where key.CreationDate > propagationCutoff
201+
where key.CreationDate > now - _keyPropagationWindow
207202
orderby key.CreationDate ascending
208203
select key)
209204
where CanCreateAuthenticatedEncryptor(key, ref decryptRetriesRemaining)

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

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -239,35 +239,17 @@ public void ResolveDefaultKeyPolicy_FallbackKey_NoNonRevokedKeysBeforePriorPropa
239239
}
240240

241241
[Fact]
242-
public void ResolveDefaultKeyPolicy_PropagatedKeyPreferred()
242+
public void ResolveDefaultKeyPolicy_OlderBadKeyVersusNewerGoodKey()
243243
{
244-
// Arrange
245-
var resolver = CreateDefaultKeyResolver();
246-
247-
var now = ParseDateTimeOffset("2010-01-01 00:00:00Z");
244+
// In https://github.com/dotnet/aspnetcore/issues/57137, we encountered an issue
245+
// where we selected the oldest unpropagated key, even though it could not be decrypted.
246+
// Choosing the oldest unpropagated key makes sense in principle, but we were late
247+
// enough in the release cycle that it made more sense to revert to the older behavior
248+
// (preferring the most recently activated key, regardless of propagation) than to
249+
// attempt a forward fix (i.e. harden the fancier logic against decryption failures).
250+
// This test is intended to ensure that the bad key is never chosen, regardless of
251+
// how we order the activated keys in the future.
248252

249-
var creation1 = now - KeyManagementOptions.KeyPropagationWindow;
250-
var creation2 = now;
251-
var activation1 = now + TimeSpan.FromMinutes(1);
252-
var activation2 = activation1 + TimeSpan.FromMinutes(1); // More recently activated, but not propagated
253-
var expiration1 = creation1 + TimeSpan.FromDays(90);
254-
var expiration2 = creation2 + TimeSpan.FromDays(90);
255-
256-
// Both active (key 2 more recently), key 1 propagated, key 2 not
257-
var key1 = CreateKey(activation1, expiration1, creationDate: creation1);
258-
var key2 = CreateKey(activation2, expiration2, creationDate: creation2);
259-
260-
// Act
261-
var resolution = resolver.ResolveDefaultKeyPolicy(now, [key1, key2]);
262-
263-
// Assert
264-
Assert.Same(key1, resolution.DefaultKey);
265-
Assert.False(resolution.ShouldGenerateNewKey);
266-
}
267-
268-
[Fact]
269-
public void ResolveDefaultKeyPolicy_OlderUnpropagatedKeyPreferred()
270-
{
271253
// Arrange
272254
var resolver = CreateDefaultKeyResolver();
273255

@@ -280,15 +262,15 @@ public void ResolveDefaultKeyPolicy_OlderUnpropagatedKeyPreferred()
280262
var expiration1 = creation1 + TimeSpan.FromDays(90);
281263
var expiration2 = creation2 + TimeSpan.FromDays(90);
282264

283-
// Both active (key 1 more recently), neither propagated
265+
// Both active (key 1 more recently), neither propagated, key2 can't be decrypted
284266
var key1 = CreateKey(activation1, expiration1, creationDate: creation1);
285-
var key2 = CreateKey(activation2, expiration2, creationDate: creation2);
267+
var key2 = CreateKey(activation2, expiration2, creationDate: creation2, createEncryptorThrows: true);
286268

287269
// Act
288270
var resolution = resolver.ResolveDefaultKeyPolicy(now, [key1, key2]);
289271

290272
// Assert
291-
Assert.Same(key2, resolution.DefaultKey);
273+
Assert.Same(key1, resolution.DefaultKey);
292274
Assert.False(resolution.ShouldGenerateNewKey);
293275
}
294276

0 commit comments

Comments
 (0)