Add HybridCache for IMDSv2 Attestation Token Caching#5539
Add HybridCache for IMDSv2 Attestation Token Caching#5539Robbie-Microsoft wants to merge 44 commits intomainfrom
Conversation
* Changes discovered from manual testing * removed dead comment * removed another dead comment
* init * added comments
* Add ResetStateForTest API for test * Keep same type * Address PR comments
* attestation * address pr comments * remove console * pr comments * pr comments
…dentity Applications (#5511)
| /// For persistent implementations, cross-process synchronization should be considered. | ||
| /// Hybrid implementations should synchronize between memory and persistent storage when possible. | ||
| /// </remarks> | ||
| internal interface IHybridCache |
There was a problem hiding this comment.
I am not sure about the name, as it conflicts with https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0
| /// <exception cref="OperationCanceledException"> | ||
| /// Thrown when the operation is canceled via the cancellation token. | ||
| /// </exception> | ||
| Task RemoveAsync(long key, CancellationToken ct); |
There was a problem hiding this comment.
nit: I believe it's customary to return Task to indicate if an entry was actually removed.
| /// <exception cref="OperationCanceledException"> | ||
| /// Thrown when the operation is canceled via the cancellation token. | ||
| /// </exception> | ||
| Task SetAsync(long key, string token, DateTimeOffset expiresOnUtc, CancellationToken ct); |
There was a problem hiding this comment.
Why don't you set a AttestationTokenResponse ?
| /// | ||
| /// This entry structure is used for both in-memory and file cache storage. | ||
| /// </remarks> | ||
| private class CacheEntry |
There was a problem hiding this comment.
Why do you need this object when you have AttestationTokenResponse ?
| rsaCng.Key.Handle != null && | ||
| !rsaCng.Key.Handle.IsInvalid) | ||
| { | ||
| return rsaCng.Key.Handle.DangerousGetHandle().ToInt64(); |
There was a problem hiding this comment.
I think you should use a key thumbprint instead. When I asked copilot it said that "The handle does not represent the key’s content, just its memory address in the current process."
A key thumbprint (e.g., SHA-256 hash of the public key) would be a stable, cryptographically unique identifier for the key material, regardless of process or machine. This is a standard approach for caching or identifying cryptographic keys.
rsaCng.Key.Export(CngKeyBlobFormat.GenericPublicBlob))
Hash the blob (e.g., using SHA-256)
Use the hash as the cache key.
Thoughts?
There was a problem hiding this comment.
We can also put the thumbprint in the ManagedIdentityKeyInfo (via the ctor) so as to not compute it everytime.
|
|
||
| // Create named mutex for cross-process synchronization | ||
| // Using a deterministic name based on cache file path to ensure same mutex across processes | ||
| _mutexName = $"Global\\MSAL_AttestationCache_{_cacheFilePath.GetHashCode():X8}"; |
There was a problem hiding this comment.
not sure if this will work, as different processes may compute different mutex names?
|
|
||
| _requestContext.Logger.Verbose(() => $"[ImdsV2] GetAttestationJwtAsync called for cache key: {cacheKey}"); | ||
|
|
||
| var cache = new HybridCache(_requestContext.Logger); |
There was a problem hiding this comment.
HybridCache implements IDisposable but is not disposed? should this be wrapped in using for short‑lived use?
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.Warning($"[HybridCache] Error loading file cache from {_cacheFilePath}: {ex.Message}. Created a new file."); |
There was a problem hiding this comment.
but the new file isn't created here?
|
|
||
| _logger.Verbose(() => $"[HybridCache] Loading cache from file: {_cacheFilePath}"); | ||
|
|
||
| var json = await Task.Run(() => File.ReadAllText(_cacheFilePath)).ConfigureAwait(false); |
There was a problem hiding this comment.
can we not use ReadAllTextAsync?
|
|
||
| // Write to temp file first, then move to avoid corruption during write | ||
| var tempFile = _cacheFilePath + ".tmp"; | ||
| await Task.Run(() => File.WriteAllText(tempFile, json)).ConfigureAwait(false); |
| { | ||
| if (!string.IsNullOrEmpty(memoryEntry.Token) && now + s_expirySkew < memoryEntry.ExpiresOnUtc) | ||
| { | ||
| _logger.Info(() => $"[HybridCache] Cache hit in memory for key: {key}"); |
|
|
||
| // Token expired in memory, remove it | ||
| s_memoryCache.TryRemove(keyString, out _); | ||
| _logger.Info(() => $"[HybridCache] Expired token removed from memory cache for key: {key}"); |
| throw new ArgumentNullException(nameof(token)); | ||
| } | ||
|
|
||
| _logger.Info(() => $"[HybridCache] SetAsync called for key: {key}, expires: {expiresOnUtc}"); |
| throw new ObjectDisposedException(nameof(HybridCache)); | ||
| } | ||
|
|
||
| _logger.Info(() => $"[HybridCache] RemoveAsync called for key: {key}"); |
| return Task.CompletedTask; | ||
| }, ct).ConfigureAwait(false); | ||
|
|
||
| _logger.Info(() => $"[HybridCache] Remove operation completed for key: {key}"); |
| { | ||
| cache.Remove(key); | ||
| } | ||
| _logger.Info(() => $"[HybridCache] Cleaned up {keysToRemove.Count} expired entries from file cache"); |
|
|
||
| _logger.Info(() => $"[HybridCache] Initializing cache with directory: {cacheDirectory}"); | ||
|
|
||
| Directory.CreateDirectory(cacheDirectory); |
| _logger.Verbose(() => $"[HybridCache] Attempting to acquire mutex: {_mutexName}"); | ||
|
|
||
| // Try to acquire mutex with timeout to avoid deadlocks | ||
| mutexAcquired = _namedMutex.WaitOne(_mutexTimeout); |
There was a problem hiding this comment.
This is where all the unit tests in HybridCacheTests fail. It seems the mutext can't be acquired multiple times in the same test.
See the test: GetAsync_ValidCachedToken_ReturnsToken
There was a problem hiding this comment.
CC @gladjohn, can you experiment with this when you get time?
* initial * pr comments * pr comments * pr comments
…brid_cache_for_maa
|
closing as not planned |
Summary
Implements a new hybrid caching strategy for IMDSv2 attestation tokens with both in-memory and file-based persistence.
Changes
IHybridCache.cs- Interface for hybrid cache operationsHybridCache.cs- Hybrid cache implementation with:ImdsV2ManagedIdentitySource.cs- Updated to use HybridCache to cache MAA tokensBenefits
TODO: Testing