Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -22,6 +22,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 @@ -71,7 +72,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)'
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 @@ -20,6 +19,7 @@ internal class ManagedIdentityAuthRequest : RequestBase
private readonly AcquireTokenForManagedIdentityParameters _managedIdentityParameters;
private static readonly SemaphoreSlim s_semaphoreSlim = new SemaphoreSlim(1, 1);
private readonly ICryptographyManager _cryptoManager;
private readonly IManagedIdentityKeyProvider _managedIdentityKeyProvider;

public ManagedIdentityAuthRequest(
IServiceBundle serviceBundle,
Expand All @@ -29,13 +29,17 @@ public ManagedIdentityAuthRequest(
{
_managedIdentityParameters = managedIdentityParameters;
_cryptoManager = serviceBundle.PlatformProxy.CryptographyManager;
_managedIdentityKeyProvider = serviceBundle.PlatformProxy.ManagedIdentityKeyProvider;
}

protected override async Task<AuthenticationResult> ExecuteAsync(CancellationToken cancellationToken)
{
AuthenticationResult authResult = null;
ILoggerAdapter logger = AuthenticationRequestParameters.RequestContext.Logger;

ManagedIdentityKeyInfo keyInfo = await _managedIdentityKeyProvider.GetOrCreateKeyAsync(
logger, cancellationToken).ConfigureAwait(false);

// 1. FIRST, handle ForceRefresh
if (_managedIdentityParameters.ForceRefresh)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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
{
internal interface IManagedIdentityKeyProvider
{
Task<ManagedIdentityKeyInfo> GetOrCreateKeyAsync(ILoggerAdapter logger, CancellationToken ct);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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;

namespace Microsoft.Identity.Client.ManagedIdentity.KeyProviders
{
/// <summary>
/// In-memory managed identity key provider.
/// </summary>
internal sealed class InMemoryManagedIdentityKeyProvider : IManagedIdentityKeyProvider
{
private static readonly SemaphoreSlim s_once = new(1, 1);
private volatile ManagedIdentityKeyInfo _cached;

public async Task<ManagedIdentityKeyInfo> GetOrCreateKeyAsync(
ILoggerAdapter logger,
CancellationToken ct)
{
// Return cached if available
if (_cached is not null)
{
logger?.Info("[MI][InMemoryKeyProvider] Returning cached key.");
return _cached;
}

// Ensure only one creation at a time
logger?.Verbose(() => "[MI][InMemoryKeyProvider] Waiting on creation semaphore.");
await s_once.WaitAsync(ct).ConfigureAwait(false);

try
{
if (_cached is not null)
{
logger?.Verbose(() => "[MI][InMemoryKeyProvider] Cached key created while waiting; returning it.");
return _cached;
}

if (ct.IsCancellationRequested)
{
logger?.Verbose(() => "[MI][InMemoryKeyProvider] Cancellation requested after entering critical section.");
ct.ThrowIfCancellationRequested();
}

logger?.Verbose(() => "[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}");
}

_cached = new ManagedIdentityKeyInfo(rsa, ManagedIdentityKeyType.InMemory, message);

logger?.Verbose(() =>
$"[MI][InMemoryKeyProvider] Caching key. Success={(rsa != null)}. HasMessage={!string.IsNullOrEmpty(message)}.");

return _cached;
}
finally
{
s_once.Release();
}
}

private static RSA CreateRsaKeyPair()
{
RSA rsa;
#if NETFRAMEWORK
// .NET Framework (Windows): use RSACng
rsa = new RSACng();
#else
// Cross‑platform: RSA.Create() -> CNG (Windows) / OpenSSL (Linux).
rsa = RSA.Create();
#endif
rsa.KeySize = 2048;
return rsa;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Security.Cryptography;
using Microsoft.Identity.Client.Core;

namespace Microsoft.Identity.Client.ManagedIdentity.KeyGuard
{
/// <summary>
/// Helper that creates or opens a Key Guard–protected RSA key.
/// </summary>
internal static class KeyGuardHelper
{
private const string ProviderName = "Microsoft Software Key Storage Provider";
private const string KeyName = "KeyGuardRSAKey";

// KeyGuard + per-boot flags
private const CngKeyCreationOptions NCryptUseVirtualIsolationFlag = (CngKeyCreationOptions)0x00020000;
private const CngKeyCreationOptions NCryptUsePerBootKeyFlag = (CngKeyCreationOptions)0x00040000;

/// <summary>
/// Creates a new RSA-2048 Key Guard key.
/// </summary>
public static CngKey CreateFresh(ILoggerAdapter logger)
{
var p = new CngKeyCreationParameters
{
Provider = new CngProvider(ProviderName),
KeyUsage = CngKeyUsages.AllUsages,
ExportPolicy = CngExportPolicies.None,
KeyCreationOptions =
CngKeyCreationOptions.OverwriteExistingKey
| NCryptUseVirtualIsolationFlag
| NCryptUsePerBootKeyFlag
};

p.Parameters.Add(new CngProperty("Length",
BitConverter.GetBytes(2048),
CngPropertyOptions.None));

try
{
return CngKey.Create(CngAlgorithm.Rsa, KeyName, p);
}
catch (CryptographicException ex)
when (IsVbsUnavailable(ex))
{
logger?.Warning(
"[MI][KeyGuardHelper] VBS key isolation is not available; 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;
}
}

/// <summary>Returns <c>true</c> if the key has the Key Guard flag.</summary>
public static bool IsKeyGuardProtected(CngKey key)
{
if (!key.HasProperty("Virtual Iso", CngPropertyOptions.None))
return false;

byte[] val = key.GetProperty("Virtual Iso", CngPropertyOptions.None).GetValue();
return val?.Length > 0 && val[0] != 0;
}

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("VBS key isolation is not available");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.ManagedIdentity.KeyGuard;

namespace Microsoft.Identity.Client.ManagedIdentity.KeyProviders
{
/// <summary>
/// CNG-backed key operations for Windows (KeyGuard + TPM/KSP).
/// </summary>
internal static class WindowsCngKeyOperations
{
private const string SoftwareKspName = "Microsoft Software Key Storage Provider";
private const string KeyGuardKeyName = "KeyGuardRSAKey";
private const string HardwareKeyName = "HardwareRSAKey";

// --- KeyGuard path (RSA) ---
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?.Verbose(() => "[MI][WinKeyProvider] KeyGuard key not found; creating fresh.");
key = KeyGuardHelper.CreateFresh(logger);
}

// If VBS is unavailable, CreateFresh() returns null. Bail out cleanly.
if (key == null)
{
logger?.Verbose(() => "[MI][WinKeyProvider] KeyGuard unavailable (VBS off or not supported).");
return false;
}

// Ensure actually KeyGuard-protected; recreate if not
if (!KeyGuardHelper.IsKeyGuardProtected(key))
{
logger?.Verbose(() => "[MI][WinKeyProvider] KeyGuard key found but not protected; recreating.");
key.Dispose();
key = KeyGuardHelper.CreateFresh(logger);

// Check again after recreate; still null or not protected -> give up KeyGuard path
if (key == null || !KeyGuardHelper.IsKeyGuardProtected(key))
{
key?.Dispose();
logger?.Verbose(() => "[MI][WinKeyProvider] Unable to obtain a KeyGuard-protected key.");
return false;
}
}

rsa = new RSACng(key);
if (rsa.KeySize < 2048)
{
try
{ rsa.KeySize = 2048; }
catch { /* some providers don't allow */ }
}
return true;
}
catch (PlatformNotSupportedException)
{
// VBS/Core Isolation not available => KeyGuard unavailable
logger?.Verbose(() => "[MI][WinKeyProvider] Exception creating KeyGuard key.");
return false;
}
catch (CryptographicException)
{
return false;
}
}

// --- Hardware (TPM/KSP) path (RSA) ---
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 < 2048)
{
try
{ rsa.KeySize = 2048; }
catch { /* PCP typically ignores post-create change */ }
}

logger?.Info("[MI][WinKeyProvider] Using Hardware key (RSA, PCP user).");
return true;
}
catch (CryptographicException e)
{
// Add HResult to make CI diagnostics actionable
logger?.Verbose(() => "[MI][WinKeyProvider] Hardware key creation/open failed. " +
$"HR=0x{e.HResult:X8}. {e.GetType().Name}: {e.Message}");
return false;
}

static CngKey CreateUserPcpRsa(CngProvider provider, string name)
{
var p = new CngKeyCreationParameters
{
Provider = provider,
KeyUsage = CngKeyUsages.Signing,
ExportPolicy = CngExportPolicies.None, // non-exportable (expected for TPM)
KeyCreationOptions = CngKeyCreationOptions.None // USER scope
};

p.Parameters.Add(new CngProperty("Length", BitConverter.GetBytes(2048), CngPropertyOptions.None));

return CngKey.Create(CngAlgorithm.Rsa, name, p);
}
}
}
}
Loading