Skip to content

Commit e72eca7

Browse files
authored
Prefer keys old enough to have propagated (dotnet#54309)
The code for choosing a fallback key when no default was found sensibly distinguished between keys that were old enough to have propagated to all instances and those that were not. The code for choosing the preferred key should do the same. This should help in the case where two instances both discover that a new key is needed and create an immediately-activated key. One of them will get both keys back from the repository and it should choose the _older_ of the two to match the choice of of the other instance (which only knows about its own key).
1 parent 958c8a7 commit e72eca7

File tree

2 files changed

+89
-11
lines changed

2 files changed

+89
-11
lines changed

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,26 @@ private bool CanCreateAuthenticatedEncryptor(IKey key)
7373

7474
private IKey? FindDefaultKey(DateTimeOffset now, IEnumerable<IKey> allKeys, out IKey? fallbackKey)
7575
{
76-
// find the preferred default key (allowing for server-to-server clock skew)
77-
var preferredDefaultKey = (from key in allKeys
78-
where key.ActivationDate <= now + _maxServerToServerClockSkew
76+
// Keys created before this time should have propagated to all instances.
77+
var propagationCutoff = now - _keyPropagationWindow;
78+
79+
// Prefer the most recently activated key that's old enough to have propagated to all instances.
80+
// If no such key exists, fall back to the *least* recently activated key that's too new to have
81+
// propagated to all instances.
82+
83+
// An unpropagated key can still be preferred insofar as we wouldn't want to generate a replacement
84+
// for it (as the replacement would also be unpropagated).
85+
86+
// Note that the two sort orders are opposite: we want the *newest* key that's old enough
87+
// (to have been propagated) or the *oldest* key that's too new.
88+
var activatedKeys = allKeys.Where(key => key.ActivationDate <= now + _maxServerToServerClockSkew);
89+
var preferredDefaultKey = (from key in activatedKeys
90+
where key.CreationDate <= propagationCutoff
7991
orderby key.ActivationDate descending, key.KeyId ascending
80-
select key).FirstOrDefault();
92+
select key).Concat(from key in activatedKeys
93+
where key.CreationDate > propagationCutoff
94+
orderby key.ActivationDate ascending, key.KeyId ascending
95+
select key).FirstOrDefault();
8196

8297
if (preferredDefaultKey != null)
8398
{
@@ -101,18 +116,17 @@ private bool CanCreateAuthenticatedEncryptor(IKey key)
101116
// key has propagated to all callers (so its creation date should be before the previous
102117
// propagation period), and we cannot use revoked keys. The fallback key may be expired.
103118

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.
119+
// As above, the two sort orders are opposite.
106120

107121
// Unlike for the preferred key, we don't choose a fallback key and then reject it if
108122
// CanCreateAuthenticatedEncryptor is false. We want to end up with *some* key, so we
109123
// keep trying until we find one that works.
110124
var unrevokedKeys = allKeys.Where(key => !key.IsRevoked);
111125
fallbackKey = (from key in (from key in unrevokedKeys
112-
where key.CreationDate <= now - _keyPropagationWindow
126+
where key.CreationDate <= propagationCutoff
113127
orderby key.CreationDate descending
114128
select key).Concat(from key in unrevokedKeys
115-
where key.CreationDate > now - _keyPropagationWindow
129+
where key.CreationDate > propagationCutoff
116130
orderby key.CreationDate ascending
117131
select key)
118132
where CanCreateAuthenticatedEncryptor(key)

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

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,18 +237,77 @@ public void ResolveDefaultKeyPolicy_FallbackKey_NoNonRevokedKeysBeforePriorPropa
237237
Assert.True(resolution.ShouldGenerateNewKey);
238238
}
239239

240+
[Fact]
241+
public void ResolveDefaultKeyPolicy_PropagatedKeyPreferred()
242+
{
243+
// Arrange
244+
var resolver = CreateDefaultKeyResolver();
245+
246+
var now = ParseDateTimeOffset("2010-01-01 00:00:00Z");
247+
248+
var creation1 = now - KeyManagementOptions.KeyPropagationWindow;
249+
var creation2 = now;
250+
var activation1 = now + TimeSpan.FromMinutes(1);
251+
var activation2 = activation1 + TimeSpan.FromMinutes(1); // More recently activated, but not propagated
252+
var expiration1 = creation1 + TimeSpan.FromDays(90);
253+
var expiration2 = creation2 + TimeSpan.FromDays(90);
254+
255+
// Both active (key 2 more recently), key 1 propagated, key 2 not
256+
var key1 = CreateKey(activation1, expiration1, creationDate: creation1);
257+
var key2 = CreateKey(activation2, expiration2, creationDate: creation2);
258+
259+
// Act
260+
var resolution = resolver.ResolveDefaultKeyPolicy(now, [key1, key2]);
261+
262+
// Assert
263+
Assert.Same(key1, resolution.DefaultKey);
264+
Assert.False(resolution.ShouldGenerateNewKey);
265+
}
266+
267+
[Fact]
268+
public void ResolveDefaultKeyPolicy_OlderUnpropagatedKeyPreferred()
269+
{
270+
// Arrange
271+
var resolver = CreateDefaultKeyResolver();
272+
273+
var now = ParseDateTimeOffset("2010-01-01 00:00:00Z");
274+
275+
var creation1 = now - TimeSpan.FromHours(1);
276+
var creation2 = creation1 - TimeSpan.FromHours(1);
277+
var activation1 = creation1;
278+
var activation2 = creation2;
279+
var expiration1 = creation1 + TimeSpan.FromDays(90);
280+
var expiration2 = creation2 + TimeSpan.FromDays(90);
281+
282+
// Both active (key 1 more recently), neither propagated
283+
var key1 = CreateKey(activation1, expiration1, creationDate: creation1);
284+
var key2 = CreateKey(activation2, expiration2, creationDate: creation2);
285+
286+
// Act
287+
var resolution = resolver.ResolveDefaultKeyPolicy(now, [key1, key2]);
288+
289+
// Assert
290+
Assert.Same(key2, resolution.DefaultKey);
291+
Assert.False(resolution.ShouldGenerateNewKey);
292+
}
293+
240294
private static IDefaultKeyResolver CreateDefaultKeyResolver()
241295
{
242296
return new DefaultKeyResolver(NullLoggerFactory.Instance);
243297
}
244298

245299
private static IKey CreateKey(string activationDate, string expirationDate, string creationDate = null, bool isRevoked = false, bool createEncryptorThrows = false)
300+
{
301+
return CreateKey(ParseDateTimeOffset(activationDate), ParseDateTimeOffset(expirationDate), creationDate == null ? (DateTimeOffset?)null : ParseDateTimeOffset(creationDate), isRevoked, createEncryptorThrows);
302+
}
303+
304+
private static IKey CreateKey(DateTimeOffset activationDate, DateTimeOffset expirationDate, DateTimeOffset? creationDate = null, bool isRevoked = false, bool createEncryptorThrows = false)
246305
{
247306
var mockKey = new Mock<IKey>();
248307
mockKey.Setup(o => o.KeyId).Returns(Guid.NewGuid());
249-
mockKey.Setup(o => o.CreationDate).Returns((creationDate != null) ? DateTimeOffset.ParseExact(creationDate, "u", CultureInfo.InvariantCulture) : DateTimeOffset.MinValue);
250-
mockKey.Setup(o => o.ActivationDate).Returns(DateTimeOffset.ParseExact(activationDate, "u", CultureInfo.InvariantCulture));
251-
mockKey.Setup(o => o.ExpirationDate).Returns(DateTimeOffset.ParseExact(expirationDate, "u", CultureInfo.InvariantCulture));
308+
mockKey.Setup(o => o.CreationDate).Returns(creationDate ?? DateTimeOffset.MinValue);
309+
mockKey.Setup(o => o.ActivationDate).Returns(activationDate);
310+
mockKey.Setup(o => o.ExpirationDate).Returns(expirationDate);
252311
mockKey.Setup(o => o.IsRevoked).Returns(isRevoked);
253312
if (createEncryptorThrows)
254313
{
@@ -261,6 +320,11 @@ private static IKey CreateKey(string activationDate, string expirationDate, stri
261320

262321
return mockKey.Object;
263322
}
323+
324+
private static DateTimeOffset ParseDateTimeOffset(string dto)
325+
{
326+
return DateTimeOffset.ParseExact(dto, "u", CultureInfo.InvariantCulture);
327+
}
264328
}
265329

266330
internal static class DefaultKeyResolverExtensions

0 commit comments

Comments
 (0)