diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1fa1850602..5de1e62bc8 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -25,6 +25,7 @@
+
@@ -74,7 +75,6 @@
-
diff --git a/build/template-run-mi-e2e-azurearc.yaml b/build/template-run-mi-e2e-azurearc.yaml
index 62c11d9222..82a8dcce0c 100644
--- a/build/template-run-mi-e2e-azurearc.yaml
+++ b/build/template-run-mi-e2e-azurearc.yaml
@@ -37,4 +37,4 @@ steps:
codeCoverageEnabled: false
failOnMinTestsNotRun: true
minimumExpectedTests: '1'
- testFiltercriteria: 'TestCategory=MI_E2E_AzureArc'
+ testFiltercriteria: '(TestCategory=MI_E2E_AzureArc|TestCategory=MI_E2E_KeyAcquisition_KeyGuard)'
diff --git a/build/template-run-mi-e2e-imds.yaml b/build/template-run-mi-e2e-imds.yaml
index 3beb42d030..c98eda0ed3 100644
--- a/build/template-run-mi-e2e-imds.yaml
+++ b/build/template-run-mi-e2e-imds.yaml
@@ -38,4 +38,4 @@ steps:
runInParallel: false
failOnMinTestsNotRun: true
minimumExpectedTests: '1'
- testFiltercriteria: 'TestCategory=MI_E2E_Imds'
+ testFiltercriteria: '(TestCategory=MI_E2E_Imds|TestCategory=MI_E2E_KeyAcquisition_Hardware)'
diff --git a/src/client/Microsoft.Identity.Client/Internal/Constants.cs b/src/client/Microsoft.Identity.Client/Internal/Constants.cs
index a7f2a9719a..4dbb0a7502 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Constants.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Constants.cs
@@ -76,5 +76,7 @@ public static string FormatAdfsWebFingerUrl(string host, string resource)
{
return $"https://{host}/.well-known/webfinger?rel={DefaultRealm}&resource={resource}";
}
+
+ public const int RsaKeySize = 2048;
}
}
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs
index 6c35139243..8e838a42eb 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT License.
using System.Collections.Generic;
-using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.ApiConfig.Parameters;
@@ -21,6 +20,7 @@ internal class ManagedIdentityAuthRequest : RequestBase
private readonly ManagedIdentityClient _managedIdentityClient;
private static readonly SemaphoreSlim s_semaphoreSlim = new SemaphoreSlim(1, 1);
private readonly ICryptographyManager _cryptoManager;
+ private readonly IManagedIdentityKeyProvider _managedIdentityKeyProvider;
public ManagedIdentityAuthRequest(
IServiceBundle serviceBundle,
@@ -32,6 +32,7 @@ public ManagedIdentityAuthRequest(
_managedIdentityParameters = managedIdentityParameters;
_managedIdentityClient = managedIdentityClient;
_cryptoManager = serviceBundle.PlatformProxy.CryptographyManager;
+ _managedIdentityKeyProvider = serviceBundle.PlatformProxy.ManagedIdentityKeyProvider;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/IManagedIdentityKeyProvider.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/IManagedIdentityKeyProvider.cs
new file mode 100644
index 0000000000..ed7f64fdb8
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/IManagedIdentityKeyProvider.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client.Core;
+
+namespace Microsoft.Identity.Client.ManagedIdentity
+{
+ ///
+ /// Provides managed identity keys for authentication scenarios.
+ /// Implementations of this interface are responsible for obtaining or creating
+ /// the best available key type (KeyGuard, Hardware, or InMemory) for managed identity authentication.
+ ///
+ internal interface IManagedIdentityKeyProvider
+ {
+ ///
+ /// Gets an existing managed identity key or creates a new one if none exists.
+ /// The method returns the best available key type based on the provider's capabilities
+ /// and the current environment.
+ ///
+ /// Logger adapter for recording operations and diagnostics.
+ /// Cancellation token to observe while waiting for the task to complete.
+ ///
+ /// A task that represents the asynchronous operation. The task result contains
+ /// a object with the key, its type, and provider message.
+ ///
+ ///
+ /// Thrown when the operation is canceled via the cancellation token.
+ ///
+ Task GetOrCreateKeyAsync(ILoggerAdapter logger, CancellationToken ct);
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/KeyProviders/InMemoryManagedIdentityKeyProvider.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/KeyProviders/InMemoryManagedIdentityKeyProvider.cs
new file mode 100644
index 0000000000..dcc47a7518
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/KeyProviders/InMemoryManagedIdentityKeyProvider.cs
@@ -0,0 +1,159 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Security.Cryptography;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client.Core;
+using Microsoft.Identity.Client.Internal;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.Identity.Client.ManagedIdentity.KeyProviders
+{
+ ///
+ /// In-memory RSA key provider for managed identity authentication.
+ ///
+ internal sealed class InMemoryManagedIdentityKeyProvider : IManagedIdentityKeyProvider
+ {
+ private static readonly SemaphoreSlim s_once = new (1, 1);
+ private volatile ManagedIdentityKeyInfo _cachedKey;
+
+ ///
+ /// Asynchronously retrieves or creates an RSA key pair for managed identity authentication.
+ /// Uses thread-safe caching to ensure only one key is created per provider instance.
+ ///
+ /// Logger adapter for recording key creation operations and diagnostics.
+ /// Cancellation token to support cooperative cancellation of the key creation process.
+ ///
+ /// A task that represents the asynchronous operation. The task result contains a
+ /// with the RSA key, key type, and provider message.
+ ///
+ public async Task GetOrCreateKeyAsync(
+ ILoggerAdapter logger,
+ CancellationToken ct)
+ {
+ // Return cached if available
+ if (_cachedKey is not null)
+ {
+ logger?.Info("[MI][InMemoryKeyProvider] Returning cached key.");
+ return _cachedKey;
+ }
+
+ // Ensure only one creation at a time
+ logger?.Info(() => "[MI][InMemoryKeyProvider] Waiting on creation semaphore.");
+ await s_once.WaitAsync(ct).ConfigureAwait(false);
+
+ try
+ {
+ if (_cachedKey is not null)
+ {
+ logger?.Info(() => "[MI][InMemoryKeyProvider] Cached key created while waiting; returning it.");
+ return _cachedKey;
+ }
+
+ if (ct.IsCancellationRequested)
+ {
+ logger?.Info(() => "[MI][InMemoryKeyProvider] Cancellation requested after entering critical section.");
+ ct.ThrowIfCancellationRequested();
+ }
+
+ logger?.Info(() => "[MI][InMemoryKeyProvider] Starting RSA key creation.");
+ RSA rsa = null;
+ string message;
+
+ try
+ {
+ rsa = CreateRsaKeyPair();
+ message = "In-memory RSA key created for Managed Identity authentication.";
+ logger?.Info("[MI][InMemoryKeyProvider] RSA key created (2048).");
+ }
+ catch (Exception ex)
+ {
+ message = $"Failed to create in-memory RSA key: {ex.GetType().Name} - {ex.Message}";
+ logger?.WarningPii(
+ $"[MI][InMemoryKeyProvider] Exception during RSA creation: {ex}",
+ $"[MI][InMemoryKeyProvider] Exception during RSA creation: {ex.GetType().Name}");
+ }
+
+ _cachedKey = new ManagedIdentityKeyInfo(rsa, ManagedIdentityKeyType.InMemory, message);
+
+ logger?.Info(() =>
+ $"[MI][InMemoryKeyProvider] Caching key. Success={(rsa != null)}. HasMessage={!string.IsNullOrEmpty(message)}.");
+
+ return _cachedKey;
+ }
+ finally
+ {
+ s_once.Release();
+ }
+ }
+
+ ///
+ /// Creates a new RSA key pair with 2048-bit key size for cryptographic operations.
+ /// Uses platform-specific RSA implementations: RSACng on .NET Framework and RSA.Create() on other platforms.
+ ///
+ ///
+ /// An instance configured with a 2048-bit key size.
+ /// On .NET Framework, returns ; on other platforms, returns the default RSA implementation.
+ ///
+ ///
+ /// This method is public instead of private because it is used in unit tests
+ ///
+ public static RSA CreateRsaKeyPair()
+ {
+#if NET462 || NET472 || NET8_0
+ // Windows-only TFMs (Framework or -windows TFMs): compile CNG path
+ return CreateWindowsPersistedRsa();
+
+#else
+ // netstandard2.0 can run anywhere; pick at runtime
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return CreateWindowsPersistedRsa(); // requires CNG package in csproj
+ }
+ return CreatePortableRsa();
+
+#endif
+ }
+
+ private static RSA CreatePortableRsa()
+ {
+ var rsa = RSA.Create();
+ if (rsa.KeySize < Constants.RsaKeySize)
+ rsa.KeySize = Constants.RsaKeySize;
+ return rsa;
+ }
+
+ private static RSA CreateWindowsPersistedRsa()
+ {
+ // Persisted CNG key (non-ephemeral) so Schannel can use it for TLS client auth
+ var creation = new CngKeyCreationParameters
+ {
+ ExportPolicy = CngExportPolicies.AllowExport,
+ KeyCreationOptions = CngKeyCreationOptions.MachineKey, // try machine store first
+ Provider = CngProvider.MicrosoftSoftwareKeyStorageProvider
+ };
+
+ // Persist key length with the key
+ creation.Parameters.Add(
+ new CngProperty("Length", BitConverter.GetBytes(Constants.RsaKeySize), CngPropertyOptions.Persist));
+
+ // Non-null name => persisted; null would be ephemeral (bad for Schannel)
+ string keyName = "MSAL-MTLS-" + Guid.NewGuid().ToString("N");
+
+ try
+ {
+ var key = CngKey.Create(CngAlgorithm.Rsa, keyName, creation);
+ return new RSACng(key);
+ }
+ catch (CryptographicException)
+ {
+ // Some environments disallow MachineKey. Fall back to user profile.
+ creation.KeyCreationOptions = CngKeyCreationOptions.None;
+ var key = CngKey.Create(CngAlgorithm.Rsa, keyName, creation);
+ return new RSACng(key);
+ }
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/KeyProviders/WindowsCngKeyOperations.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/KeyProviders/WindowsCngKeyOperations.cs
new file mode 100644
index 0000000000..3d2af6c0f8
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/KeyProviders/WindowsCngKeyOperations.cs
@@ -0,0 +1,294 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Security.Cryptography;
+using Microsoft.Identity.Client.Core;
+using Microsoft.Identity.Client.Internal;
+
+namespace Microsoft.Identity.Client.ManagedIdentity.KeyProviders
+{
+ ///
+ /// Provides CNG-backed cryptographic key operations for Windows platforms, supporting both
+ /// KeyGuard-protected keys (with VBS/TPM integration) and hardware-backed TPM/KSP keys
+ /// for managed identity authentication scenarios.
+ ///
+ ///
+ /// This class handles two primary key protection mechanisms:
+ ///
+ /// - KeyGuard: Requires Virtualization Based Security (VBS) and provides enhanced key protection
+ /// - Hardware TPM/KSP: Uses Platform Crypto Provider (PCP) for TPM-backed keys
+ ///
+ /// All operations are performed in user scope with silent key access patterns.
+ ///
+ internal static class WindowsCngKeyOperations
+ {
+ private const string SoftwareKspName = "Microsoft Software Key Storage Provider";
+ private const string KeyGuardKeyName = "KeyGuardRSAKey";
+ private const string HardwareKeyName = "HardwareRSAKey";
+ private const string KeyGuardVirtualIsoProperty = "Virtual Iso";
+ private const string VbsNotAvailable = "VBS key isolation is not available";
+
+ // KeyGuard + per-boot flags
+ private const CngKeyCreationOptions NCryptUseVirtualIsolationFlag = (CngKeyCreationOptions)0x00020000;
+ private const CngKeyCreationOptions NCryptUsePerBootKeyFlag = (CngKeyCreationOptions)0x00040000;
+
+ ///
+ /// Attempts to get or create a KeyGuard-protected RSA key for managed identity operations.
+ /// This method first tries to open an existing key, and if not found, creates a fresh KeyGuard-protected key.
+ /// KeyGuard requires VBS (Virtualization Based Security) to be enabled and supported.
+ ///
+ /// Logger adapter for diagnostic messages and error reporting
+ /// When this method returns , contains the RSA instance with the KeyGuard-protected key;
+ /// when this method returns , this parameter is set to
+ /// if a KeyGuard-protected RSA key was successfully obtained or created;
+ /// if KeyGuard is unavailable, VBS is not supported, or the operation failed
+ ///
+ /// This method performs the following operations in sequence:
+ ///
+ /// - Attempts to open an existing KeyGuard key using the software KSP in user scope
+ /// - If the key doesn't exist, creates a new KeyGuard-protected key
+ /// - Validates that the key is actually KeyGuard-protected
+ /// - If validation fails, recreates the key and re-validates
+ /// - Ensures the RSA key size is at least 2048 bits when possible
+ ///
+ /// The method gracefully handles scenarios where VBS is disabled or not supported by returning .
+ ///
+ /// Thrown when VBS/Core Isolation is not available on the platform
+ /// Thrown when cryptographic operations fail during key creation or access
+ public static bool TryGetOrCreateKeyGuard(ILoggerAdapter logger, out RSA rsa)
+ {
+ rsa = default(RSA);
+
+ try
+ {
+ // Try open by the known name first (Software KSP, user scope, silent)
+ CngKey key;
+ try
+ {
+ key = CngKey.Open(
+ KeyGuardKeyName,
+ new CngProvider(SoftwareKspName),
+ CngKeyOpenOptions.UserKey | CngKeyOpenOptions.Silent);
+ }
+ catch (CryptographicException)
+ {
+ // Not found -> create fresh (helper may return null if VBS unavailable)
+ logger?.Info(() => "[MI][WinKeyProvider] KeyGuard key not found; creating fresh.");
+ key = CreateFresh(logger);
+ }
+
+ // If VBS is unavailable, CreateFresh() returns null. Bail out cleanly.
+ if (key == null)
+ {
+ logger?.Info(() => "[MI][WinKeyProvider] KeyGuard unavailable (VBS off or not supported).");
+ return false;
+ }
+
+ // Ensure actually KeyGuard-protected; recreate if not
+ if (!IsKeyGuardProtected(key))
+ {
+ logger?.Info(() => "[MI][WinKeyProvider] KeyGuard key found but not protected; recreating.");
+ key.Dispose();
+ key = CreateFresh(logger);
+
+ // Check again after recreate; still null or not protected -> give up KeyGuard path
+ if (key == null || !IsKeyGuardProtected(key))
+ {
+ key?.Dispose();
+ logger?.Info(() => "[MI][WinKeyProvider] Unable to obtain a KeyGuard-protected key.");
+ return false;
+ }
+ }
+
+ rsa = new RSACng(key);
+ if (rsa.KeySize < Constants.RsaKeySize)
+ {
+ try
+ { rsa.KeySize = Constants.RsaKeySize; }
+ catch { logger?.Info(() => $"[MI][WinKeyProvider] Unable to extend the size of the KeyGuard key to {Constants.RsaKeySize} bits."); }
+ }
+ return true;
+ }
+ catch (PlatformNotSupportedException)
+ {
+ // VBS/Core Isolation not available => KeyGuard unavailable
+ logger?.Info(() => "[MI][WinKeyProvider] Exception creating KeyGuard key.");
+ return false;
+ }
+ catch (CryptographicException ex)
+ {
+ logger?.Info(() => $"[MI][WinKeyProvider] KeyGuard creation failed due to platform limitation. {ex.GetType().Name}: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Attempts to get or create a hardware-backed RSA key using the Platform Crypto Provider (PCP)
+ /// for TPM-based key storage and operations.
+ ///
+ /// Logger adapter for diagnostic messages and error reporting
+ /// When this method returns , contains the RSA instance backed by hardware (TPM);
+ /// when this method returns , this parameter is set to
+ /// if a hardware-backed RSA key was successfully obtained or created;
+ /// if hardware key operations are not available or the operation failed
+ ///
+ /// This method performs the following operations:
+ ///
+ /// - Checks if a hardware key with the predefined name already exists in user scope
+ /// - Opens the existing key if found, or creates a new hardware-backed key if not found
+ /// - Configures the key with non-exportable policy (standard for TPM keys)
+ /// - Ensures the RSA key size is at least 2048 bits when supported by the provider
+ ///
+ /// The created keys are stored in user scope and are non-exportable for security reasons.
+ /// TPM providers typically ignore post-creation key size changes.
+ ///
+ /// Thrown when hardware key creation, opening, or configuration fails.
+ /// The exception's HResult property provides additional diagnostic information
+ public static bool TryGetOrCreateHardwareRsa(ILoggerAdapter logger, out RSA rsa)
+ {
+ rsa = default(RSA);
+
+ try
+ {
+ // PCP (TPM) in USER scope
+ CngProvider provider = new CngProvider(SoftwareKspName);
+ CngKeyOpenOptions openOpts = CngKeyOpenOptions.UserKey | CngKeyOpenOptions.Silent;
+
+ CngKey key = CngKey.Exists(HardwareKeyName, provider, openOpts)
+ ? CngKey.Open(HardwareKeyName, provider, openOpts)
+ : CreateUserPcpRsa(provider, HardwareKeyName);
+
+ rsa = new RSACng(key);
+
+ if (rsa.KeySize < Constants.RsaKeySize)
+ {
+ try
+ { rsa.KeySize = Constants.RsaKeySize; }
+ catch { logger?.Info(() => $"[MI][WinKeyProvider] Unable to extend the size of the Hardware key to {Constants.RsaKeySize} bits."); }
+ }
+
+ logger?.Info("[MI][WinKeyProvider] Using Hardware key (RSA, PCP user).");
+ return true;
+ }
+ catch (CryptographicException e)
+ {
+ // Add HResult to make CI diagnostics actionable
+ logger?.Info(() => "[MI][WinKeyProvider] Hardware key creation/open failed. " +
+ $"HR=0x{e.HResult:X8}. {e.GetType().Name}: {e.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Creates a new RSA key using the Platform Crypto Provider (PCP) in user scope
+ /// with non-exportable policy suitable for TPM-backed operations.
+ ///
+ /// The CNG provider to use for key creation (typically PCP for TPM)
+ /// The name to assign to the created key for future reference
+ /// A new instance configured for signing operations with 2048-bit key size
+ ///
+ /// The created key has the following characteristics:
+ ///
+ /// - Algorithm: RSA
+ /// - Key size: 2048 bits
+ /// - Usage: Signing operations
+ /// - Export policy: None (non-exportable)
+ /// - Scope: User scope
+ ///
+ ///
+ private static CngKey CreateUserPcpRsa(CngProvider provider, string name)
+ {
+ var ckcParams = new CngKeyCreationParameters
+ {
+ Provider = provider,
+ KeyUsage = CngKeyUsages.Signing,
+ ExportPolicy = CngExportPolicies.None, // non-exportable (expected for TPM)
+ KeyCreationOptions = CngKeyCreationOptions.None // USER scope
+ };
+
+ ckcParams.Parameters.Add(new CngProperty("Length", BitConverter.GetBytes(Constants.RsaKeySize), CngPropertyOptions.None));
+
+ return CngKey.Create(CngAlgorithm.Rsa, name, ckcParams);
+ }
+
+ ///
+ /// Creates a new RSA-2048 Key Guard key.
+ ///
+ /// Logger adapter for recording diagnostic information and warnings.
+ ///
+ /// A instance protected by Key Guard if VBS is available;
+ /// otherwise, null if VBS is not supported on the system.
+ ///
+ ///
+ /// This method attempts to create a cryptographic key with hardware-backed security using
+ /// Virtualization Based Security (VBS). If VBS is not available, the method logs a warning
+ /// and returns null, allowing the caller to fall back to software-based key storage.
+ ///
+ private static CngKey CreateFresh(ILoggerAdapter logger)
+ {
+ var ckcParams = new CngKeyCreationParameters
+ {
+ Provider = new CngProvider(SoftwareKspName),
+ KeyUsage = CngKeyUsages.AllUsages,
+ ExportPolicy = CngExportPolicies.None,
+ KeyCreationOptions =
+ CngKeyCreationOptions.OverwriteExistingKey
+ | NCryptUseVirtualIsolationFlag
+ | NCryptUsePerBootKeyFlag
+ };
+
+ ckcParams.Parameters.Add(new CngProperty("Length",
+ BitConverter.GetBytes(Constants.RsaKeySize),
+ CngPropertyOptions.None));
+
+ try
+ {
+ return CngKey.Create(CngAlgorithm.Rsa, KeyGuardKeyName, ckcParams);
+ }
+ catch (CryptographicException ex)
+ when (IsVbsUnavailable(ex))
+ {
+ logger?.Warning(
+ $"[MI][KeyGuardHelper] {VbsNotAvailable}; falling back to software keys. " +
+ "Ensure that Virtualization Based Security (VBS) is enabled on this machine " +
+ "(e.g. Credential Guard, Hyper-V, or Windows Defender Application Guard). " +
+ "Inner exception: " + ex.Message);
+
+ return null;
+ }
+ }
+
+ ///
+ /// Determines whether the specified CNG key is protected by Key Guard.
+ ///
+ /// The CNG key to check for Key Guard protection.
+ /// true if the key has the Key Guard flag; otherwise, false.
+ ///
+ /// This method checks for the presence of the Virtual Iso property on the key,
+ /// which indicates that the key is protected by hardware-backed security features.
+ ///
+ public static bool IsKeyGuardProtected(CngKey key)
+ {
+ if (!key.HasProperty(KeyGuardVirtualIsoProperty, CngPropertyOptions.None))
+ return false;
+
+ byte[] val = key.GetProperty(KeyGuardVirtualIsoProperty, CngPropertyOptions.None).GetValue();
+ return val?.Length > 0 && val[0] != 0;
+ }
+
+ ///
+ /// Determines whether a cryptographic exception indicates that VBS is unavailable.
+ ///
+ /// The cryptographic exception to examine.
+ /// true if the exception indicates VBS is not supported; otherwise, false.
+ private static bool IsVbsUnavailable(CryptographicException ex)
+ {
+ // HResult for “NTE_NOT_SUPPORTED” = 0x80890014
+ const int NTE_NOT_SUPPORTED = unchecked((int)0x80890014);
+
+ return ex.HResult == NTE_NOT_SUPPORTED ||
+ ex.Message.Contains(VbsNotAvailable);
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/KeyProviders/WindowsManagedIdentityKeyProvider.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/KeyProviders/WindowsManagedIdentityKeyProvider.cs
new file mode 100644
index 0000000000..878787a8d5
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/KeyProviders/WindowsManagedIdentityKeyProvider.cs
@@ -0,0 +1,166 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client.Core;
+
+namespace Microsoft.Identity.Client.ManagedIdentity.KeyProviders
+{
+ ///
+ /// Windows-specific managed identity key provider that implements a hierarchical key selection strategy.
+ /// Attempts to use the most secure key source available in the following priority order:
+ /// 1. KeyGuard (CVM/TVM) if available - provides VBS (Virtualization-based Security) isolation
+ /// 2. Hardware (TPM/KSP via Microsoft Platform Crypto Provider) - hardware-backed keys
+ /// 3. In-memory fallback - software-based keys stored in memory
+ ///
+ ///
+ /// This provider ensures that only one key creation operation occurs at a time using a semaphore,
+ /// and caches the created key for subsequent requests to improve performance.
+ ///
+ internal sealed class WindowsManagedIdentityKeyProvider : IManagedIdentityKeyProvider
+ {
+ private static readonly SemaphoreSlim s_once = new (1, 1);
+ private volatile ManagedIdentityKeyInfo _cachedKey;
+
+ ///
+ /// Gets or creates a managed identity key using the best available security mechanism.
+ ///
+ /// Logger adapter for recording key creation attempts and results.
+ /// Cancellation token to cancel the operation if needed.
+ ///
+ /// A task that represents the asynchronous key creation operation.
+ /// The task result contains with the created key and its type.
+ ///
+ ///
+ /// Thrown when the operation is cancelled via the parameter.
+ ///
+ ///
+ ///
+ /// This method implements a thread-safe, single-creation pattern using a semaphore.
+ /// If a key has already been created and cached, it returns immediately.
+ ///
+ ///
+ /// The key creation follows this priority order:
+ ///
+ /// - KeyGuard: Uses VBS isolation for maximum security (RSA-2048)
+ /// - Hardware: Uses TPM or hardware security module (RSA-2048, non-exportable)
+ /// - In-memory: Software fallback when hardware options are unavailable
+ ///
+ ///
+ ///
+ /// Exceptions during key creation are logged but do not prevent fallback to the next option.
+ /// Only the final in-memory fallback can throw exceptions that terminate the operation.
+ ///
+ ///
+ public async Task GetOrCreateKeyAsync(
+ ILoggerAdapter logger,
+ CancellationToken ct)
+ {
+ // Return cached if available
+ if (_cachedKey != null)
+ {
+ logger?.Info("[MI][WinKeyProvider] Returning cached key.");
+ return _cachedKey;
+ }
+
+ // Ensure only one creation at a time
+ logger?.Info(() => "[MI][WinKeyProvider] Waiting on creation semaphore.");
+ await s_once.WaitAsync(ct).ConfigureAwait(false);
+
+ try
+ {
+ if (_cachedKey != null)
+ {
+ logger?.Info(() => "[MI][WinKeyProvider] Cached key created while waiting; returning it.");
+ return _cachedKey;
+ }
+
+ if (ct.IsCancellationRequested)
+ {
+ logger?.Info(() => "[MI][WinKeyProvider] Cancellation requested after entering critical section.");
+ ct.ThrowIfCancellationRequested();
+ }
+
+ var messageBuilder = new StringBuilder();
+
+ // 1) KeyGuard (RSA-2048 under VBS isolation)
+ try
+ {
+ logger.Info("[MI][WinKeyProvider] Trying KeyGuard key.");
+ if (WindowsCngKeyOperations.TryGetOrCreateKeyGuard(logger, out RSA kgRsa))
+ {
+ messageBuilder.AppendLine("KeyGuard RSA key created successfully.");
+ _cachedKey = new ManagedIdentityKeyInfo(kgRsa, ManagedIdentityKeyType.KeyGuard, messageBuilder.ToString());
+ logger?.Info("[MI][WinKeyProvider] Using KeyGuard key (RSA).");
+ return _cachedKey;
+ }
+ else
+ {
+ messageBuilder.AppendLine("KeyGuard RSA key creation not available or failed.");
+ logger?.Info(() => "[MI][WinKeyProvider] KeyGuard key not available.");
+ }
+ }
+ catch (Exception ex)
+ {
+ messageBuilder.AppendLine($"KeyGuard RSA key creation threw exception: {ex.GetType().Name}: {ex.Message}");
+ logger?.WarningPii(
+ $"[MI][WinKeyProvider] Exception creating KeyGuard key: {ex}",
+ $"[MI][WinKeyProvider] Exception creating KeyGuard key: {ex.GetType().Name}");
+ }
+
+ // 2) Hardware TPM/KSP (RSA-2048, non-exportable)
+ try
+ {
+ logger?.Info(() => "[MI][WinKeyProvider] Trying Hardware (TPM/KSP) key.");
+ if (WindowsCngKeyOperations.TryGetOrCreateHardwareRsa(logger, out RSA hwRsa))
+ {
+ messageBuilder.AppendLine("Hardware RSA key created successfully.");
+ _cachedKey = new ManagedIdentityKeyInfo(hwRsa, ManagedIdentityKeyType.Hardware, messageBuilder.ToString());
+ logger?.Info("[MI][WinKeyProvider] Using Hardware key (RSA).");
+ return _cachedKey;
+ }
+ else
+ {
+ messageBuilder.AppendLine("Hardware RSA key creation not available or failed.");
+ logger?.Info(() => "[MI][WinKeyProvider] Hardware key not available.");
+ }
+ }
+ catch (Exception ex)
+ {
+ messageBuilder.AppendLine($"Hardware RSA key creation threw exception: {ex.GetType().Name}: {ex.Message}");
+ logger?.WarningPii(
+ $"[MI][WinKeyProvider] Exception creating Hardware key: {ex}",
+ $"[MI][WinKeyProvider] Exception creating Hardware key: {ex.GetType().Name}");
+ }
+
+ // 3) In-memory fallback (software RSA)
+ logger?.Info("[MI][WinKeyProvider] Falling back to in-memory RSA key (software).");
+ if (ct.IsCancellationRequested)
+ {
+ logger?.Info(() => "[MI][WinKeyProvider] Cancellation requested before in-memory fallback.");
+ ct.ThrowIfCancellationRequested();
+ }
+
+ var fallbackIMMIKP = new InMemoryManagedIdentityKeyProvider();
+ _cachedKey = await fallbackIMMIKP.GetOrCreateKeyAsync(logger, ct).ConfigureAwait(false);
+
+ if (messageBuilder.Length > 0)
+ {
+ logger?.Info(() => "[MI][WinKeyProvider] Fallback reasons:\n" + messageBuilder.ToString().Trim());
+ }
+
+ return _cachedKey;
+
+ }
+ finally
+ {
+ s_once.Release();
+ }
+ }
+ }
+}
+
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityKeyInfo.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityKeyInfo.cs
new file mode 100644
index 0000000000..fd2684e881
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityKeyInfo.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Security.Cryptography;
+
+namespace Microsoft.Identity.Client.ManagedIdentity
+{
+ ///
+ /// Encapsulates information about a Managed Identity key used for authentication.
+ /// Provides the best available key and its type for Managed Identity scenarios.
+ /// The caller does not need to know how the key is sourced.
+ ///
+ /// Key types:
+ /// - : Key sourced from KeyGuard provider.
+ /// - : Key stored in hardware (e.g., TPM).
+ /// - : Key stored in memory only.
+ ///
+ internal sealed class ManagedIdentityKeyInfo
+ {
+ public RSA Key { get; }
+ public ManagedIdentityKeyType Type { get; }
+ public string ProviderMessage { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The RSA key instance to be used for cryptographic operations.
+ /// The type of the Managed Identity key indicating its storage method.
+ /// A message from the key provider with additional information.
+ public ManagedIdentityKeyInfo(RSA keyInfo, ManagedIdentityKeyType type, string providerMessage)
+ {
+ Key = keyInfo;
+ Type = type;
+ ProviderMessage = providerMessage;
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityKeyProviderFactory.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityKeyProviderFactory.cs
new file mode 100644
index 0000000000..b5c757f6e1
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityKeyProviderFactory.cs
@@ -0,0 +1,114 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Threading;
+using Microsoft.Identity.Client.ManagedIdentity.KeyProviders;
+using Microsoft.Identity.Client.PlatformsCommon.Shared;
+using Microsoft.Identity.Client.Core;
+
+namespace Microsoft.Identity.Client.ManagedIdentity
+{
+ ///
+ /// Creates (once) and caches the most suitable Managed Identity key provider for the current platform.
+ /// Thread-safe, lock-free (uses CompareExchange).
+ ///
+ ///
+ /// This factory class uses a singleton pattern with lazy initialization to ensure only one
+ /// key provider instance is created per application domain. The implementation is thread-safe
+ /// using to avoid locking overhead.
+ ///
+ /// The factory automatically selects the most appropriate key provider based on the current
+ /// platform capabilities:
+ ///
+ /// - Windows: Uses WindowsManagedIdentityKeyProvider with CNG support
+ /// - Non-Windows: Falls back to InMemoryManagedIdentityKeyProvider
+ ///
+ ///
+ internal static class ManagedIdentityKeyProviderFactory
+ {
+ // Cached singleton instance of the chosen key provider.
+ private static IManagedIdentityKeyProvider s_provider;
+
+ ///
+ /// Returns the cached provider if available; otherwise creates it in a thread-safe manner.
+ ///
+ ///
+ /// Logger adapter for recording operations and diagnostics. Can be null.
+ ///
+ ///
+ /// The singleton instance appropriate for the current platform.
+ ///
+ ///
+ /// This method implements the double-checked locking pattern using atomic operations
+ /// to ensure thread safety without the overhead of explicit locks. If multiple threads
+ /// call this method concurrently before initialization, only one provider instance
+ /// will be created and cached.
+ ///
+ internal static IManagedIdentityKeyProvider GetOrCreateProvider(ILoggerAdapter logger)
+ {
+ // Fast path: read the field once (Volatile ensures latest published value).
+ IManagedIdentityKeyProvider existing = Volatile.Read(ref s_provider);
+
+ if (existing != null)
+ {
+ logger?.Verbose(() => "[MI][KeyProviderFactory] Returning cached key provider instance.");
+ return existing;
+ }
+
+ logger?.Verbose(() => "[MI][KeyProviderFactory] Creating key provider instance (first use).");
+ IManagedIdentityKeyProvider created = CreateProviderCore(logger);
+
+ // Publish the created instance only if another thread has not already published one.
+ // If another thread won the race, discard our newly created instance and use theirs.
+ IManagedIdentityKeyProvider prior = Interlocked.CompareExchange(ref s_provider, created, null);
+
+ if (prior == null)
+ {
+ logger?.Info($"[MI][KeyProviderFactory] Key provider created: {created.GetType().Name}.");
+ return created;
+ }
+
+ logger?.Verbose(() => "[MI][KeyProviderFactory] Another thread already created the provider; using existing instance.");
+ return prior;
+ }
+
+ ///
+ /// Chooses an implementation based on compile-time and runtime platform capabilities.
+ ///
+ ///
+ /// Logger adapter for recording platform detection and provider selection. Can be null.
+ ///
+ ///
+ /// A new instance suitable for the detected platform.
+ ///
+ ///
+ /// This method performs platform detection and selects the most appropriate key provider:
+ ///
+ /// Windows Platform:
+ ///
+ /// - Detected using
+ /// - Returns WindowsManagedIdentityKeyProvider with CNG support
+ /// - Provides hardware-backed key storage when available
+ ///
+ ///
+ /// Non-Windows Platforms:
+ ///
+ /// - Includes Linux, macOS, and other Unix-like systems
+ /// - Returns InMemoryManagedIdentityKeyProvider as fallback
+ /// - Keys are stored in memory for the application lifetime
+ ///
+ ///
+ private static IManagedIdentityKeyProvider CreateProviderCore(ILoggerAdapter logger)
+ {
+ if (DesktopOsHelper.IsWindows())
+ {
+ logger?.Info("[MI][KeyProviderFactory] Windows detected with CNG support - using Windows managed identity key provider.");
+ return new WindowsManagedIdentityKeyProvider();
+ }
+
+ // Non-Windows OS - we will fall back to in-memory implementation.
+ logger?.Info("[MI][KeyProviderFactory] Non-Windows platform (with CNG) - using InMemory provider.");
+ return new InMemoryManagedIdentityKeyProvider();
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityKeyType.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityKeyType.cs
new file mode 100644
index 0000000000..2ab2047eae
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityKeyType.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Identity.Client.ManagedIdentity
+{
+ ///
+ /// Specifies the type of key storage mechanism used for managed identity authentication.
+ ///
+ internal enum ManagedIdentityKeyType
+ {
+ // Represents a key stored using a secure key guard mechanism that provides hardware-level protection.
+ KeyGuard,
+
+ // Represents a key stored directly in hardware security modules or trusted platform modules.
+ Hardware,
+
+ // Represents a key stored in memory with software-based protection mechanisms.
+ InMemory
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/Csr.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/Csr.cs
index ae124f7fbf..52cd10354e 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/Csr.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/Csr.cs
@@ -10,54 +10,35 @@ namespace Microsoft.Identity.Client.ManagedIdentity.V2
{
internal class Csr
{
- internal static (string csrPem, RSA privateKey) Generate(string clientId, string tenantId, CuidInfo cuid)
+ internal static (string csrPem, RSA privateKey) Generate(RSA rsa, string clientId, string tenantId, CuidInfo cuid)
{
- using (RSA rsa = CreateRsaKeyPair())
- {
- // Use custom polyfill for downlevel frameworks (net462, net472, netstandard2.0)
- // See CertificateRequest.cs
- var req = new CertificateRequest(
- new X500DistinguishedName($"CN={clientId}, DC={tenantId}"),
- rsa,
- HashAlgorithmName.SHA256,
- RSASignaturePadding.Pss);
-
- AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
- writer.WriteCharacterString(UniversalTagNumber.UTF8String, JsonHelper.SerializeToJson(cuid));
-
- req.OtherRequestAttributes.Add(
- new AsnEncodedData(
- "1.3.6.1.4.1.311.90.2.10",
- writer.Encode()));
-
- string pemCsr = req.CreateSigningRequestPem();
-
- // Remove PEM headers and format as single line
- string rawCsr = pemCsr
- .Replace("-----BEGIN CERTIFICATE REQUEST-----", "")
- .Replace("-----END CERTIFICATE REQUEST-----", "")
- .Replace("\r", "")
- .Replace("\n", "")
- .Trim();
-
- return (rawCsr, rsa);
- }
- }
-
- private static RSA CreateRsaKeyPair()
- {
- // TODO: use the strongest key on the machine i.e. a TPM key
- RSA rsa = null;
-
-#if NET462 || NET472
- // .NET Framework runs only on Windows, so RSACng (Windows-only) is always available
- rsa = new RSACng();
-#else
- // Cross-platform .NET - RSA.Create() returns appropriate PSS-capable implementation
- rsa = RSA.Create();
-#endif
- rsa.KeySize = 2048;
- return rsa;
+ // Use custom polyfill for downlevel frameworks (net462, net472, netstandard2.0)
+ // See CertificateRequest.cs
+ var req = new CertificateRequest(
+ new X500DistinguishedName($"CN={clientId}, DC={tenantId}"),
+ rsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pss);
+
+ AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
+ writer.WriteCharacterString(UniversalTagNumber.UTF8String, JsonHelper.SerializeToJson(cuid));
+
+ req.OtherRequestAttributes.Add(
+ new AsnEncodedData(
+ "1.3.6.1.4.1.311.90.2.10",
+ writer.Encode()));
+
+ string pemCsr = req.CreateSigningRequestPem();
+
+ // Remove PEM headers and format as single line
+ string rawCsr = pemCsr
+ .Replace("-----BEGIN CERTIFICATE REQUEST-----", "")
+ .Replace("-----END CERTIFICATE REQUEST-----", "")
+ .Replace("\r", "")
+ .Replace("\n", "")
+ .Trim();
+
+ return (rawCsr, rsa);
}
}
}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/DefaultCsrFactory.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/DefaultCsrFactory.cs
index edbd183edb..cdc2b9e526 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/DefaultCsrFactory.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/DefaultCsrFactory.cs
@@ -7,9 +7,9 @@ namespace Microsoft.Identity.Client.ManagedIdentity.V2
{
internal class DefaultCsrFactory : ICsrFactory
{
- public (string csrPem, RSA privateKey) Generate(string clientId, string tenantId, CuidInfo cuid)
+ public (string csrPem, RSA privateKey) Generate(RSA rsa, string clientId, string tenantId, CuidInfo cuid)
{
- return Csr.Generate(clientId, tenantId, cuid);
+ return Csr.Generate(rsa, clientId, tenantId, cuid);
}
}
}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ICsrFactory.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ICsrFactory.cs
index 84bae9409d..69f71f8079 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ICsrFactory.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ICsrFactory.cs
@@ -7,6 +7,6 @@ namespace Microsoft.Identity.Client.ManagedIdentity.V2
{
internal interface ICsrFactory
{
- (string csrPem, RSA privateKey) Generate(string clientId, string tenantId, CuidInfo cuid);
+ (string csrPem, RSA privateKey) Generate(RSA rsa, string clientId, string tenantId, CuidInfo cuid);
}
}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs
index ccb4602695..19e64f5c3a 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs
@@ -256,7 +256,11 @@ private async Task ExecuteCertificateRequestAsync(st
protected override async Task CreateRequestAsync(string resource)
{
var csrMetadata = await GetCsrMetadataAsync(_requestContext, false).ConfigureAwait(false);
- var (csr, privateKey) = _requestContext.ServiceBundle.Config.CsrFactory.Generate(csrMetadata.ClientId, csrMetadata.TenantId, csrMetadata.CuId);
+
+ var keyInfo = await _requestContext.ServiceBundle.PlatformProxy.ManagedIdentityKeyProvider
+ .GetOrCreateKeyAsync(_requestContext.Logger, _requestContext.UserCancellationToken).ConfigureAwait(false);
+
+ var (csr, privateKey) = _requestContext.ServiceBundle.Config.CsrFactory.Generate(keyInfo.Key, csrMetadata.ClientId, csrMetadata.TenantId, csrMetadata.CuId);
var certificateRequestResponse = await ExecuteCertificateRequestAsync(csr).ConfigureAwait(false);
diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
index 06d648391b..3279f0338a 100644
--- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
+++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
@@ -92,7 +92,7 @@
-
+
@@ -104,7 +104,7 @@
-
+
@@ -160,4 +160,5 @@
+
diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/IPlatformProxy.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/IPlatformProxy.cs
index 00b1a446bb..43ddeb032a 100644
--- a/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/IPlatformProxy.cs
+++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/IPlatformProxy.cs
@@ -5,6 +5,7 @@
using Microsoft.Identity.Client.AuthScheme.PoP;
using Microsoft.Identity.Client.Cache;
using Microsoft.Identity.Client.Internal.Broker;
+using Microsoft.Identity.Client.ManagedIdentity;
using Microsoft.Identity.Client.TelemetryCore.OpenTelemetry;
using Microsoft.Identity.Client.UI;
@@ -110,5 +111,7 @@ internal interface IPlatformProxy
bool BrokerSupportsWamAccounts { get; }
IMsalHttpClientFactory CreateDefaultHttpClientFactory();
+
+ IManagedIdentityKeyProvider ManagedIdentityKeyProvider { get; }
}
}
diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/AbstractPlatformProxy.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/AbstractPlatformProxy.cs
index 8f2301896c..d6a2b488a2 100644
--- a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/AbstractPlatformProxy.cs
+++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/AbstractPlatformProxy.cs
@@ -8,6 +8,8 @@
using Microsoft.Identity.Client.Cache;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.Internal.Broker;
+using Microsoft.Identity.Client.ManagedIdentity;
+
#if SUPPORTS_OTEL
using Microsoft.Identity.Client.Platforms.Features.OpenTelemetry;
#endif
@@ -34,6 +36,7 @@ internal abstract class AbstractPlatformProxy : IPlatformProxy
private readonly Lazy _productName;
private readonly Lazy _runtimeVersion;
private readonly Lazy _otelInstrumentation;
+ private readonly Lazy _miKeyProvider;
protected AbstractPlatformProxy(ILoggerAdapter logger)
{
@@ -49,6 +52,7 @@ protected AbstractPlatformProxy(ILoggerAdapter logger)
_platformLogger = new Lazy(InternalGetPlatformLogger);
_runtimeVersion = new Lazy(InternalGetRuntimeVersion);
_otelInstrumentation = new Lazy(InternalGetOtelInstrumentation);
+ _miKeyProvider = new Lazy(GetManagedIdentityKeyProvider);
}
private IOtelInstrumentation InternalGetOtelInstrumentation()
@@ -229,10 +233,17 @@ public virtual IMsalHttpClientFactory CreateDefaultHttpClientFactory()
return new SimpleHttpClientFactory();
}
+ internal virtual IManagedIdentityKeyProvider GetManagedIdentityKeyProvider()
+ {
+ return ManagedIdentityKeyProviderFactory.GetOrCreateProvider(Logger);
+ }
+
///
/// On Android and iOS, MSAL will save the legacy ADAL cache in a known location.
/// On other platforms, the app developer must use the serialization callbacks
///
public virtual bool LegacyCacheRequiresSerialization => true;
+
+ public IManagedIdentityKeyProvider ManagedIdentityKeyProvider => _miKeyProvider.Value;
}
}
diff --git a/tests/Microsoft.Identity.Test.E2e/ManagedIdentityKeyAcquisitionTests.cs b/tests/Microsoft.Identity.Test.E2e/ManagedIdentityKeyAcquisitionTests.cs
new file mode 100644
index 0000000000..22a3b4b047
--- /dev/null
+++ b/tests/Microsoft.Identity.Test.E2e/ManagedIdentityKeyAcquisitionTests.cs
@@ -0,0 +1,76 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Security.Cryptography;
+using Microsoft.Identity.Client.ManagedIdentity.KeyProviders;
+using Microsoft.Identity.Test.Common.Core.Helpers;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.Identity.Test.E2E
+{
+ [TestClass]
+ public class ManagedIdentityKeyAcquisitionTests
+ {
+ private const string SoftwareKspName = "Microsoft Software Key Storage Provider";
+
+ // Runs on the AzureArc agent: must obtain a VBS/KeyGuard key.
+ [TestMethod]
+ [TestCategory("MI_E2E_KeyAcquisition_KeyGuard")]
+ [RunOnAzureDevOps]
+ public void KeyAcquisition_Fetches_KeyGuard_Key()
+ {
+ if (!OperatingSystem.IsWindows())
+ {
+ Assert.Inconclusive("This test runs on Windows agents only.");
+ }
+
+ bool ok = WindowsCngKeyOperations.TryGetOrCreateKeyGuard(logger: null, out RSA rsa);
+ Assert.IsTrue(ok, "Expected KeyGuard key on AzureArc agent.");
+
+ using (rsa)
+ {
+ var rsacng = rsa as RSACng;
+ Assert.IsNotNull(rsacng, "Expected RSACng for KeyGuard.");
+ Assert.IsTrue(
+ WindowsCngKeyOperations.IsKeyGuardProtected(rsacng.Key),
+ "Expected KeyGuard (VBS) protected key on AzureArc agent.");
+ }
+ }
+
+ // Runs on the IMDS agent: must obtain a TPM/PCP hardware key (user scope).
+ [TestMethod]
+ [TestCategory("MI_E2E_KeyAcquisition_Hardware")]
+ [RunOnAzureDevOps]
+ public void KeyAcquisition_Fetches_Hardware_Key()
+ {
+ if (!OperatingSystem.IsWindows())
+ {
+ Assert.Inconclusive("This test runs on Windows agents only.");
+ }
+
+ bool ok = WindowsCngKeyOperations.TryGetOrCreateHardwareRsa(logger: null, out RSA rsa);
+ Assert.IsTrue(ok, "Expected TPM hardware key on IMDS agent.");
+
+ using (rsa)
+ {
+ var rsacng = rsa as RSACng;
+ Assert.IsNotNull(rsacng, "Expected RSACng for hardware key.");
+
+ Assert.AreEqual(
+ SoftwareKspName,
+ rsacng.Key.Provider.Provider,
+ "Expected TPM-backed key via Microsoft Software Key Storage Provider.");
+
+ // TPM keys created with ExportPolicy=None should not allow private export.
+ bool privateExportable = true;
+ try
+ { _ = rsacng.ExportParameters(true); }
+ catch (CryptographicException) { privateExportable = false; }
+ catch (NotSupportedException) { privateExportable = false; }
+
+ Assert.IsFalse(privateExportable, "Hardware (TPM) key should be non-exportable.");
+ }
+ }
+ }
+}
diff --git a/tests/Microsoft.Identity.Test.Unit/Helpers/TestCsrFactory.cs b/tests/Microsoft.Identity.Test.Unit/Helpers/TestCsrFactory.cs
index 6edd3936bb..f407550b07 100644
--- a/tests/Microsoft.Identity.Test.Unit/Helpers/TestCsrFactory.cs
+++ b/tests/Microsoft.Identity.Test.Unit/Helpers/TestCsrFactory.cs
@@ -8,8 +8,9 @@ namespace Microsoft.Identity.Test.Unit.Helpers
{
internal class TestCsrFactory : ICsrFactory
{
- public (string csrPem, RSA privateKey) Generate(string clientId, string tenantId, CuidInfo cuId)
+ public (string csrPem, RSA privateKey) Generate(RSA rsa, string clientId, string tenantId, CuidInfo cuId)
{
+ // we don't care about the RSA that's passed in, we will always return the same mock private key
return ("mock-csr", CreateMockRsa());
}
diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs
index 257ccc91cb..99b4ec57d8 100644
--- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs
@@ -10,6 +10,7 @@
using Microsoft.Identity.Client.AppConfig;
using Microsoft.Identity.Client.Internal.Logger;
using Microsoft.Identity.Client.ManagedIdentity;
+using Microsoft.Identity.Client.ManagedIdentity.KeyProviders;
using Microsoft.Identity.Client.ManagedIdentity.V2;
using Microsoft.Identity.Client.PlatformsCommon.Shared;
using Microsoft.Identity.Test.Common.Core.Mocks;
@@ -483,8 +484,9 @@ public void TestCsrGeneration_OnlyVmId()
{
VmId = TestConstants.VmId
};
-
- var (csr, _) = Csr.Generate(TestConstants.ClientId, TestConstants.TenantId, cuid);
+
+ var rsa = InMemoryManagedIdentityKeyProvider.CreateRsaKeyPair();
+ var (csr, _) = Csr.Generate(rsa, TestConstants.ClientId, TestConstants.TenantId, cuid);
CsrValidator.ValidateCsrContent(csr, TestConstants.ClientId, TestConstants.TenantId, cuid);
}
@@ -497,7 +499,8 @@ public void TestCsrGeneration_VmIdAndVmssId()
VmssId = TestConstants.VmssId
};
- var (csr, _) = Csr.Generate(TestConstants.ClientId, TestConstants.TenantId, cuid);
+ var rsa = InMemoryManagedIdentityKeyProvider.CreateRsaKeyPair();
+ var (csr, _) = Csr.Generate(rsa, TestConstants.ClientId, TestConstants.TenantId, cuid);
CsrValidator.ValidateCsrContent(csr, TestConstants.ClientId, TestConstants.TenantId, cuid);
}
#endregion
diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/InMemoryManagedIdentityKeyProviderTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/InMemoryManagedIdentityKeyProviderTests.cs
new file mode 100644
index 0000000000..ba501fce15
--- /dev/null
+++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/InMemoryManagedIdentityKeyProviderTests.cs
@@ -0,0 +1,110 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client.Internal;
+using Microsoft.Identity.Client.ManagedIdentity;
+using Microsoft.Identity.Client.ManagedIdentity.KeyProviders;
+using Microsoft.Identity.Client.Core;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NSubstitute; // For Substitute.For()
+
+namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests
+{
+ [TestClass]
+ public class InMemoryManagedIdentityKeyProviderTests
+ {
+ private static (InMemoryManagedIdentityKeyProvider keyProvider, ILoggerAdapter logger) CreateKeyProviderAndLogger()
+ {
+ return (new InMemoryManagedIdentityKeyProvider(), Substitute.For());
+ }
+
+ [TestMethod]
+ public async Task ReturnsRsa2048_AndCaches_Success()
+ {
+ var (keyProvider, logger) = CreateKeyProviderAndLogger();
+
+ ManagedIdentityKeyInfo k1 = await keyProvider.GetOrCreateKeyAsync(logger, CancellationToken.None).ConfigureAwait(false);
+ ManagedIdentityKeyInfo k2 = await keyProvider.GetOrCreateKeyAsync(logger, CancellationToken.None).ConfigureAwait(false);
+
+ Assert.IsNotNull(k1);
+ Assert.AreSame(k1, k2, "Provider should cache the same ManagedIdentityKeyInfo instance per process.");
+ Assert.IsInstanceOfType(k1.Key, typeof(RSA));
+ Assert.IsTrue(k1.Key.KeySize >= Constants.RsaKeySize);
+ Assert.AreEqual(ManagedIdentityKeyType.InMemory, k1.Type);
+ }
+
+ [TestMethod]
+ public async Task Concurrency_SingleCreation()
+ {
+ var (keyProvider, logger) = CreateKeyProviderAndLogger();
+
+ var tasks = Enumerable.Range(0, 32)
+ .Select(_ => keyProvider.GetOrCreateKeyAsync(logger, CancellationToken.None))
+ .ToArray();
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ var first = tasks[0].Result;
+ foreach (var task in tasks)
+ {
+ Assert.AreSame(first, task.Result, "All concurrent calls should return the same cached ManagedIdentityKeyInfo.");
+ }
+ }
+
+ [TestMethod]
+ public async Task Rsa_SignsAndVerifies()
+ {
+ var (keyProvider, logger) = CreateKeyProviderAndLogger();
+
+ var managedIdentityApp = await keyProvider.GetOrCreateKeyAsync(logger, CancellationToken.None).ConfigureAwait(false);
+
+ byte[] data = Encoding.UTF8.GetBytes("ping");
+ byte[] signature = managedIdentityApp.Key.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+ bool isSignatureValid = managedIdentityApp.Key.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+
+ Assert.IsTrue(isSignatureValid);
+ }
+
+ [TestMethod]
+ public async Task Cancellation_BeforeCreation_Throws_And_SubsequentCallSucceeds()
+ {
+ var (keyProvider, logger) = CreateKeyProviderAndLogger();
+
+ using (var cts = new CancellationTokenSource())
+ {
+ cts.Cancel(); // Pre-cancel so WaitAsync throws TaskCanceledException.
+
+ await Assert.ThrowsExceptionAsync(
+ () => keyProvider.GetOrCreateKeyAsync(logger, cts.Token)).ConfigureAwait(false);
+ }
+
+ // Subsequent non-cancelled call should create and cache the key.
+ var keyInfo = await keyProvider.GetOrCreateKeyAsync(logger, CancellationToken.None).ConfigureAwait(false);
+ Assert.IsNotNull(keyInfo);
+ Assert.IsNotNull(keyInfo.Key);
+ Assert.AreEqual(ManagedIdentityKeyType.InMemory, keyInfo.Type);
+ }
+
+ [TestMethod]
+ public async Task Cancellation_AfterCache_ReturnsCachedKey_IgnoringCancellation()
+ {
+ var (keyProvider, logger) = CreateKeyProviderAndLogger();
+
+ ManagedIdentityKeyInfo k1 = await keyProvider.GetOrCreateKeyAsync(logger, CancellationToken.None).ConfigureAwait(false);
+
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Cached path should not throw.
+ ManagedIdentityKeyInfo k2 = await keyProvider.GetOrCreateKeyAsync(logger, cts.Token).ConfigureAwait(false);
+
+ Assert.AreSame(k1, k2);
+ Assert.IsNotNull(k2.Key);
+ }
+ }
+}