136 lines
6.6 KiB
C#
136 lines
6.6 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Microsoft.AspNetCore.Cryptography;
|
|
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
|
|
using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Microsoft.AspNetCore.DataProtection.KeyManagement
|
|
{
|
|
/// <summary>
|
|
/// Implements policy for resolving the default key from a candidate keyring.
|
|
/// </summary>
|
|
internal sealed class DefaultKeyResolver : IDefaultKeyResolver
|
|
{
|
|
/// <summary>
|
|
/// The window of time before the key expires when a new key should be created
|
|
/// and persisted to the keyring to ensure uninterrupted service.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the propagation time is 5 days and the current key expires within 5 days,
|
|
/// a new key will be generated.
|
|
/// </remarks>
|
|
private readonly TimeSpan _keyPropagationWindow;
|
|
|
|
private readonly ILogger _logger;
|
|
|
|
/// <summary>
|
|
/// The maximum skew that is allowed between servers.
|
|
/// This is used to allow newly-created keys to be used across servers even though
|
|
/// their activation dates might be a few minutes into the future.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the max skew is 5 minutes and the best matching candidate default key has
|
|
/// an activation date of less than 5 minutes in the future, we'll use it.
|
|
/// </remarks>
|
|
private readonly TimeSpan _maxServerToServerClockSkew;
|
|
|
|
public DefaultKeyResolver(TimeSpan keyPropagationWindow, TimeSpan maxServerToServerClockSkew, IServiceProvider services)
|
|
{
|
|
_keyPropagationWindow = keyPropagationWindow;
|
|
_maxServerToServerClockSkew = maxServerToServerClockSkew;
|
|
_logger = services.GetLogger<DefaultKeyResolver>();
|
|
}
|
|
|
|
private bool CanCreateAuthenticatedEncryptor(IKey key)
|
|
{
|
|
try
|
|
{
|
|
var encryptorInstance = key.CreateEncryptorInstance() ?? CryptoUtil.Fail<IAuthenticatedEncryptor>("CreateEncryptorInstance returned null.");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.KeyIsIneligibleToBeTheDefaultKeyBecauseItsMethodFailed(key.KeyId, nameof(IKey.CreateEncryptorInstance), ex);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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
|
|
where key.ActivationDate <= now + _maxServerToServerClockSkew
|
|
orderby key.ActivationDate descending, key.KeyId ascending
|
|
select key).FirstOrDefault();
|
|
|
|
if (preferredDefaultKey != null)
|
|
{
|
|
_logger?.ConsideringKeyWithExpirationDateAsDefaultKey(preferredDefaultKey.KeyId, preferredDefaultKey.ExpirationDate);
|
|
|
|
// if the key has been revoked or is expired, it is no longer a candidate
|
|
if (preferredDefaultKey.IsRevoked || preferredDefaultKey.IsExpired(now) || !CanCreateAuthenticatedEncryptor(preferredDefaultKey))
|
|
{
|
|
_logger?.KeyIsNoLongerUnderConsiderationAsDefault(preferredDefaultKey.KeyId);
|
|
preferredDefaultKey = null;
|
|
}
|
|
}
|
|
|
|
// Only the key that has been most recently activated is eligible to be the preferred default,
|
|
// and only if it hasn't expired or been revoked. This is intentional: generating a new key is
|
|
// an implicit signal that we should stop using older keys (even if they're not revoked), so
|
|
// activating a new key should permanently mark all older keys as non-preferred.
|
|
|
|
if (preferredDefaultKey != null)
|
|
{
|
|
// Does *any* key in the key ring fulfill the requirement that its activation date is prior
|
|
// to the preferred default key's expiration date (allowing for skew) and that it will
|
|
// remain valid one propagation cycle from now? If so, the caller doesn't need to add a
|
|
// new key.
|
|
callerShouldGenerateNewKey = !allKeys.Any(key =>
|
|
key.ActivationDate <= (preferredDefaultKey.ExpirationDate + _maxServerToServerClockSkew)
|
|
&& !key.IsExpired(now + _keyPropagationWindow)
|
|
&& !key.IsRevoked);
|
|
|
|
if (callerShouldGenerateNewKey)
|
|
{
|
|
_logger?.DefaultKeyExpirationImminentAndRepository();
|
|
}
|
|
|
|
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 && CanCreateAuthenticatedEncryptor(key)
|
|
select key).FirstOrDefault();
|
|
|
|
_logger?.RepositoryContainsNoViableDefaultKey();
|
|
|
|
callerShouldGenerateNewKey = true;
|
|
return null;
|
|
}
|
|
|
|
public DefaultKeyResolution ResolveDefaultKeyPolicy(DateTimeOffset now, IEnumerable<IKey> allKeys)
|
|
{
|
|
var retVal = default(DefaultKeyResolution);
|
|
retVal.DefaultKey = FindDefaultKey(now, allKeys, out retVal.FallbackKey, out retVal.ShouldGenerateNewKey);
|
|
return retVal;
|
|
}
|
|
}
|
|
}
|