Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<PackageVersion Include="System.Runtime.Serialization.Formatters" Version="4.3.0" />
<PackageVersion Include="System.Runtime.Serialization.Json" Version="4.3.0" />
<PackageVersion Include="System.Runtime.Serialization.Primitives" Version="4.3.0" />
<PackageVersion Include="System.Security.Cryptography.Cng" Version="5.0.0" PrivateAssets="All" />
<PackageVersion Include="System.Security.Cryptography.ProtectedData" Version="4.5.0" />
<PackageVersion Include="System.Security.SecureString" Version="4.3.0" />
<PackageVersion Include="System.ServiceModel.Http" Version="4.5.3" />
Expand Down Expand Up @@ -74,7 +75,6 @@
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="6.35.0" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
<PackageVersion Include="System.Security.Cryptography.Cng" Version="5.0.0" />
<PackageVersion Include="System.Text.Json" Version="6.0.10" />
<PackageVersion Include="System.Threading" Version="4.3.0" />
<PackageVersion Include="System.Threading.Tasks" Version="4.3.0" />
Expand Down
2 changes: 1 addition & 1 deletion build/template-run-mi-e2e-azurearc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)'
2 changes: 1 addition & 1 deletion build/template-run-mi-e2e-imds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)'
2 changes: 2 additions & 0 deletions src/client/Microsoft.Identity.Client/Internal/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -32,6 +32,7 @@ public ManagedIdentityAuthRequest(
_managedIdentityParameters = managedIdentityParameters;
_managedIdentityClient = managedIdentityClient;
_cryptoManager = serviceBundle.PlatformProxy.CryptographyManager;
_managedIdentityKeyProvider = serviceBundle.PlatformProxy.ManagedIdentityKeyProvider;
}

protected override async Task<AuthenticationResult> ExecuteAsync(CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
internal interface IManagedIdentityKeyProvider
{
/// <summary>
/// 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.
/// </summary>
/// <param name="logger">Logger adapter for recording operations and diagnostics.</param>
/// <param name="ct">Cancellation token to observe while waiting for the task to complete.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains
/// a <see cref="ManagedIdentityKeyInfo"/> object with the key, its type, and provider message.
/// </returns>
/// <exception cref="System.OperationCanceledException">
/// Thrown when the operation is canceled via the cancellation token.
/// </exception>
Task<ManagedIdentityKeyInfo> GetOrCreateKeyAsync(ILoggerAdapter logger, CancellationToken ct);
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// In-memory RSA key provider for managed identity authentication.
/// </summary>
internal sealed class InMemoryManagedIdentityKeyProvider : IManagedIdentityKeyProvider
{
private static readonly SemaphoreSlim s_once = new (1, 1);
private volatile ManagedIdentityKeyInfo _cachedKey;

/// <summary>
/// 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.
/// </summary>
/// <param name="logger">Logger adapter for recording key creation operations and diagnostics.</param>
/// <param name="ct">Cancellation token to support cooperative cancellation of the key creation process.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains a
/// <see cref="ManagedIdentityKeyInfo"/> with the RSA key, key type, and provider message.
/// </returns>
public async Task<ManagedIdentityKeyInfo> 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();
}
}

/// <summary>
/// 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.
/// </summary>
/// <returns>
/// An <see cref="RSA"/> instance configured with a 2048-bit key size.
/// On .NET Framework, returns <see cref="RSACng"/>; on other platforms, returns the default RSA implementation.
/// </returns>
/// <remarks>
/// This method is public instead of private because it is used in unit tests
/// </remarks>
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);
}
}
}
}
Loading