aspnetcore/src/Microsoft.AspNetCore.DataPr.../KeyManagement/KeyRingProvider.cs

258 lines
13 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.Diagnostics;
using System.Threading;
using Microsoft.AspNetCore.Cryptography;
using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.DataProtection.KeyManagement
{
internal sealed class KeyRingProvider : ICacheableKeyRingProvider, IKeyRingProvider
{
private CacheableKeyRing _cacheableKeyRing;
private readonly object _cacheableKeyRingLockObj = new object();
private readonly IDefaultKeyResolver _defaultKeyResolver;
private readonly KeyManagementOptions _keyManagementOptions;
private readonly IKeyManager _keyManager;
private readonly ILogger _logger;
public KeyRingProvider(
IKeyManager keyManager,
IOptions<KeyManagementOptions> keyManagementOptions,
IDefaultKeyResolver defaultKeyResolver)
: this(
keyManager,
keyManagementOptions,
defaultKeyResolver,
NullLoggerFactory.Instance)
{
}
public KeyRingProvider(
IKeyManager keyManager,
IOptions<KeyManagementOptions> keyManagementOptions,
IDefaultKeyResolver defaultKeyResolver,
ILoggerFactory loggerFactory)
{
_keyManagementOptions = new KeyManagementOptions(keyManagementOptions.Value); // clone so new instance is immutable
_keyManager = keyManager;
CacheableKeyRingProvider = this;
_defaultKeyResolver = defaultKeyResolver;
_logger = loggerFactory.CreateLogger<KeyRingProvider>();
}
// for testing
internal ICacheableKeyRingProvider CacheableKeyRingProvider { get; set; }
private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, IKey keyJustAdded)
{
// Refresh the list of all keys
var cacheExpirationToken = _keyManager.GetCacheExpirationToken();
var allKeys = _keyManager.GetAllKeys();
// Fetch the current default key from the list of all keys
var defaultKeyPolicy = _defaultKeyResolver.ResolveDefaultKeyPolicy(now, allKeys);
if (!defaultKeyPolicy.ShouldGenerateNewKey)
{
CryptoUtil.Assert(defaultKeyPolicy.DefaultKey != null, "Expected to see a default key.");
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, defaultKeyPolicy.DefaultKey, allKeys);
}
_logger.PolicyResolutionStatesThatANewKeyShouldBeAddedToTheKeyRing();
// We shouldn't call CreateKey more than once, else we risk stack diving. This code path shouldn't
// get hit unless there was an ineligible key with an activation date slightly later than the one we
// just added. If this does happen, then we'll just use whatever key we can instead of creating
// new keys endlessly, eventually falling back to the one we just added if all else fails.
if (keyJustAdded != null)
{
var keyToUse = defaultKeyPolicy.DefaultKey ?? defaultKeyPolicy.FallbackKey ?? keyJustAdded;
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, keyToUse, allKeys);
}
// At this point, we know we need to generate a new key.
// We have been asked to generate a new key, but auto-generation of keys has been disabled.
// We need to use the fallback key or fail.
if (!_keyManagementOptions.AutoGenerateKeys)
{
var keyToUse = defaultKeyPolicy.DefaultKey ?? defaultKeyPolicy.FallbackKey;
if (keyToUse == null)
{
_logger.KeyRingDoesNotContainValidDefaultKey();
throw new InvalidOperationException(Resources.KeyRingProvider_NoDefaultKey_AutoGenerateDisabled);
}
else
{
_logger.UsingFallbackKeyWithExpirationAsDefaultKey(keyToUse.KeyId, keyToUse.ExpirationDate);
return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, keyToUse, allKeys);
}
}
if (defaultKeyPolicy.DefaultKey == null)
{
// The case where there's no default key is the easiest scenario, since it
// means that we need to create a new key with immediate activation.
var newKey = _keyManager.CreateNewKey(activationDate: now, expirationDate: now + _keyManagementOptions.NewKeyLifetime);
return CreateCacheableKeyRingCore(now, keyJustAdded: newKey); // recursively call
}
else
{
// If there is a default key, then the new key we generate should become active upon
// expiration of the default key. The new key lifetime is measured from the creation
// date (now), not the activation date.
var newKey = _keyManager.CreateNewKey(activationDate: defaultKeyPolicy.DefaultKey.ExpirationDate, expirationDate: now + _keyManagementOptions.NewKeyLifetime);
return CreateCacheableKeyRingCore(now, keyJustAdded: newKey); // recursively call
}
}
private CacheableKeyRing CreateCacheableKeyRingCoreStep2(DateTimeOffset now, CancellationToken cacheExpirationToken, IKey defaultKey, IEnumerable<IKey> allKeys)
{
Debug.Assert(defaultKey != null);
// Invariant: our caller ensures that CreateEncryptorInstance succeeded at least once
Debug.Assert(defaultKey.CreateEncryptor() != null);
_logger.UsingKeyAsDefaultKey(defaultKey.KeyId);
var nextAutoRefreshTime = now + GetRefreshPeriodWithJitter(_keyManagementOptions.KeyRingRefreshPeriod);
// The cached keyring should expire at the earliest of (default key expiration, next auto-refresh time).
// Since the refresh period and safety window are not user-settable, we can guarantee that there's at
// least one auto-refresh between the start of the safety window and the key's expiration date.
// This gives us an opportunity to update the key ring before expiration, and it prevents multiple
// servers in a cluster from trying to update the key ring simultaneously. Special case: if the default
// key's expiration date is in the past, then we know we're using a fallback key and should disregard
// its expiration date in favor of the next auto-refresh time.
return new CacheableKeyRing(
expirationToken: cacheExpirationToken,
expirationTime: (defaultKey.ExpirationDate <= now) ? nextAutoRefreshTime : Min(defaultKey.ExpirationDate, nextAutoRefreshTime),
defaultKey: defaultKey,
allKeys: allKeys);
}
public IKeyRing GetCurrentKeyRing()
{
return GetCurrentKeyRingCore(DateTime.UtcNow);
}
internal IKeyRing GetCurrentKeyRingCore(DateTime utcNow)
{
Debug.Assert(utcNow.Kind == DateTimeKind.Utc);
// Can we return the cached keyring to the caller?
var existingCacheableKeyRing = Volatile.Read(ref _cacheableKeyRing);
if (CacheableKeyRing.IsValid(existingCacheableKeyRing, utcNow))
{
return existingCacheableKeyRing.KeyRing;
}
// The cached keyring hasn't been created or must be refreshed. We'll allow one thread to
// update the keyring, and all other threads will continue to use the existing cached
// keyring while the first thread performs the update. There is an exception: if there
// is no usable existing cached keyring, all callers must block until the keyring exists.
var acquiredLock = false;
try
{
Monitor.TryEnter(_cacheableKeyRingLockObj, (existingCacheableKeyRing != null) ? 0 : Timeout.Infinite, ref acquiredLock);
if (acquiredLock)
{
// This thread acquired the critical section and is responsible for updating the
// cached keyring. But first, let's make sure that somebody didn't sneak in before
// us and update the keyring on our behalf.
existingCacheableKeyRing = Volatile.Read(ref _cacheableKeyRing);
if (CacheableKeyRing.IsValid(existingCacheableKeyRing, utcNow))
{
return existingCacheableKeyRing.KeyRing;
}
if (existingCacheableKeyRing != null)
{
_logger.ExistingCachedKeyRingIsExpired();
}
// It's up to us to refresh the cached keyring.
// This call is performed *under lock*.
CacheableKeyRing newCacheableKeyRing;
try
{
newCacheableKeyRing = CacheableKeyRingProvider.GetCacheableKeyRing(utcNow);
}
catch (Exception ex)
{
if (existingCacheableKeyRing != null)
{
_logger.ErrorOccurredWhileRefreshingKeyRing(ex);
}
else
{
_logger.ErrorOccurredWhileReadingKeyRing(ex);
}
// Failures that occur while refreshing the keyring are most likely transient, perhaps due to a
// temporary network outage. Since we don't want every subsequent call to result in failure, we'll
// create a new keyring object whose expiration is now + some short period of time (currently 2 min),
// and after this period has elapsed the next caller will try refreshing. If we don't have an
// existing keyring (perhaps because this is the first call), then there's nothing to extend, so
// each subsequent caller will keep going down this code path until one succeeds.
if (existingCacheableKeyRing != null)
{
Volatile.Write(ref _cacheableKeyRing, existingCacheableKeyRing.WithTemporaryExtendedLifetime(utcNow));
}
// The immediate caller should fail so that he can report the error up his chain. This makes it more likely
// that an administrator can see the error and react to it as appropriate. The caller can retry the operation
// and will probably have success as long as he falls within the temporary extension mentioned above.
throw;
}
Volatile.Write(ref _cacheableKeyRing, newCacheableKeyRing);
return newCacheableKeyRing.KeyRing;
}
else
{
// We didn't acquire the critical section. This should only occur if we passed
// zero for the Monitor.TryEnter timeout, which implies that we had an existing
// (but outdated) keyring that we can use as a fallback.
Debug.Assert(existingCacheableKeyRing != null);
return existingCacheableKeyRing.KeyRing;
}
}
finally
{
if (acquiredLock)
{
Monitor.Exit(_cacheableKeyRingLockObj);
}
}
}
private static TimeSpan GetRefreshPeriodWithJitter(TimeSpan refreshPeriod)
{
// We'll fudge the refresh period up to -20% so that multiple applications don't try to
// hit a single repository simultaneously. For instance, if the refresh period is 1 hour,
// we'll return a value in the vicinity of 48 - 60 minutes. We use the Random class since
// we don't need a secure PRNG for this.
return TimeSpan.FromTicks((long)(refreshPeriod.Ticks * (1.0d - (new Random().NextDouble() / 5))));
}
private static DateTimeOffset Min(DateTimeOffset a, DateTimeOffset b)
{
return (a < b) ? a : b;
}
CacheableKeyRing ICacheableKeyRingProvider.GetCacheableKeyRing(DateTimeOffset now)
{
// the entry point allows one recursive call
return CreateCacheableKeyRingCore(now, keyJustAdded: null);
}
}
}