// 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 { /// /// Implements policy for resolving the default key from a candidate keyring. /// internal sealed class DefaultKeyResolver : IDefaultKeyResolver { /// /// The window of time before the key expires when a new key should be created /// and persisted to the keyring to ensure uninterrupted service. /// /// /// If the propagation time is 5 days and the current key expires within 5 days, /// a new key will be generated. /// private readonly TimeSpan _keyPropagationWindow; private readonly ILogger _logger; /// /// 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. /// /// /// 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. /// private readonly TimeSpan _maxServerToServerClockSkew; public DefaultKeyResolver(TimeSpan keyPropagationWindow, TimeSpan maxServerToServerClockSkew, IServiceProvider services) { _keyPropagationWindow = keyPropagationWindow; _maxServerToServerClockSkew = maxServerToServerClockSkew; _logger = services.GetLogger(); } private bool CanCreateAuthenticatedEncryptor(IKey key) { try { var encryptorInstance = key.CreateEncryptorInstance() ?? CryptoUtil.Fail("CreateEncryptorInstance returned null."); return true; } catch (Exception ex) { _logger?.KeyIsIneligibleToBeTheDefaultKeyBecauseItsMethodFailed(key.KeyId, nameof(IKey.CreateEncryptorInstance), ex); return false; } } private IKey FindDefaultKey(DateTimeOffset now, IEnumerable 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 allKeys) { var retVal = default(DefaultKeyResolution); retVal.DefaultKey = FindDefaultKey(now, allKeys, out retVal.FallbackKey, out retVal.ShouldGenerateNewKey); return retVal; } } }