-
Notifications
You must be signed in to change notification settings - Fork 373
MSI V2 client side keys #5448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Robbie-Microsoft
merged 19 commits into
rginsburg/msiv2_feature_branch
from
gladjohn/msi_vs_keys
Sep 19, 2025
Merged
MSI V2 client side keys #5448
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
18b51d3
draft
gladjohn c813ed0
updated
gladjohn df59d37
MSI V2 client-side keys: add e2e + unit tests, fixes, hardware KSP up…
gladjohn 59a5998
Merge branch 'main' into gladjohn/msi_vs_keys
gladjohn 04cad71
Merge branch 'main' into gladjohn/msi_vs_keys
Robbie-Microsoft 9292e24
draft
gladjohn f7f4081
updated
gladjohn 287c2aa
MSI V2 client-side keys: add e2e + unit tests, fixes, hardware KSP up…
gladjohn b3bf762
fixed rebase error
Robbie-Microsoft c435bd7
Merge branch 'gladjohn/msi_vs_keys' of https://github.com/AzureAD/mic…
Robbie-Microsoft 6e7544c
Fixed build error
Robbie-Microsoft c9df653
Fixed rebase error
Robbie-Microsoft 34eb19d
Implemented feedback and added XML docs
Robbie-Microsoft 09564a4
All existing ImdsV2 unit tests run in both net48 and net8.0
Robbie-Microsoft 4c0bb4e
added a comment
Robbie-Microsoft 290470b
Cleaned up InMemoryManagedIdentityKeyProviderTests
Robbie-Microsoft 357098f
Implemented some feedback
Robbie-Microsoft fd3ba4c
Fixed logging
Robbie-Microsoft afa1d62
Merge branch 'rginsburg/msiv2_feature_branch' into gladjohn/msi_vs_keys
Robbie-Microsoft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
...rosoft.Identity.Client/ManagedIdentity/KeyProviders/InMemoryManagedIdentityKeyProvider.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
// ManagedIdentity/Providers/InMemoryManagedIdentityKeyProvider.cs | ||
using System; | ||
using System.Security.Cryptography; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace Microsoft.Identity.Client.ManagedIdentity.Providers | ||
{ | ||
internal sealed class InMemoryManagedIdentityKeyProvider : IManagedIdentityKeyProvider | ||
{ | ||
private static readonly SemaphoreSlim s_once = new(1, 1); | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
private volatile MiKeyInfo _cached; | ||
|
||
public async Task<MiKeyInfo> GetOrCreateKeyAsync(CancellationToken ct) | ||
{ | ||
if (_cached is not null) | ||
return _cached; | ||
|
||
await s_once.WaitAsync(ct).ConfigureAwait(false); | ||
try | ||
{ | ||
if (_cached is not null) | ||
return _cached; | ||
|
||
var rsa = CreateRsaKeyPair(); | ||
_cached = new MiKeyInfo(rsa, MiKeyType.InMemory); | ||
return _cached; | ||
} | ||
finally | ||
{ | ||
s_once.Release(); | ||
} | ||
} | ||
|
||
private static RSA CreateRsaKeyPair() | ||
{ | ||
RSA rsa; | ||
#if NET462 || NET472 | ||
gladjohn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
// .NET Framework (Windows): use RSACng | ||
rsa = new RSACng(); | ||
#else | ||
// Cross-platform (.NET Core/5+/Standard) | ||
rsa = RSA.Create(); | ||
#endif | ||
rsa.KeySize = 2048; | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
return rsa; | ||
} | ||
} | ||
} |
82 changes: 82 additions & 0 deletions
82
src/client/Microsoft.Identity.Client/ManagedIdentity/KeyProviders/KeyGuardKey.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
#if !NETSTANDARD2_0 | ||
using System; | ||
using System.Security.Cryptography; | ||
|
||
namespace Microsoft.Identity.Client.ManagedIdentity.KeyGuard | ||
{ | ||
/// <summary> | ||
/// Helper for creating and validating Key Guard–isolated RSA keys. | ||
/// </summary> | ||
internal static class KeyGuardKey | ||
gladjohn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
{ | ||
// Flags not exposed in the public enum; values from NCrypt.h | ||
private const CngKeyCreationOptions NCRYPT_USE_VIRTUAL_ISOLATION_FLAG = (CngKeyCreationOptions)0x00020000; | ||
private const CngKeyCreationOptions NCRYPT_USE_PER_BOOT_KEY_FLAG = (CngKeyCreationOptions)0x00040000; | ||
|
||
/// <summary> | ||
/// Create a fresh RSA-2048 key with Key Guard (VBS) isolation. | ||
/// Overwrites any existing key with the same name. | ||
/// </summary> | ||
/// <param name="providerName">Typically "Microsoft Software Key Storage Provider".</param> | ||
/// <param name="keyName">The CNG key container name to create.</param> | ||
public static CngKey CreateFresh(string providerName, string keyName) | ||
{ | ||
var parms = new CngKeyCreationParameters | ||
{ | ||
Provider = new CngProvider(providerName), | ||
KeyUsage = CngKeyUsages.AllUsages, | ||
ExportPolicy = CngExportPolicies.None, | ||
// Per-boot, VBS/KeyGuard isolation; overwrite if exists | ||
KeyCreationOptions = CngKeyCreationOptions.OverwriteExistingKey | ||
| NCRYPT_USE_VIRTUAL_ISOLATION_FLAG | ||
| NCRYPT_USE_PER_BOOT_KEY_FLAG | ||
}; | ||
|
||
// Set length = 2048 bits | ||
parms.Parameters.Add(new CngProperty( | ||
"Length", | ||
BitConverter.GetBytes(2048), | ||
CngPropertyOptions.None)); | ||
|
||
try | ||
{ | ||
return CngKey.Create(CngAlgorithm.Rsa, keyName, parms); | ||
} | ||
catch (CryptographicException ex) | ||
{ | ||
if (IsVbsUnavailable(ex)) | ||
{ | ||
// Clear, actionable signal to callers; provider can fall back to TPM/in-memory | ||
throw new PlatformNotSupportedException( | ||
"Key Guard requires Windows Core Isolation (VBS).", ex); | ||
} | ||
throw; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Returns true if the key has the Key Guard (VBS) isolation 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 != null && val.Length > 0 && val[0] != 0; | ||
} | ||
|
||
// NTE_NOT_SUPPORTED or recognizable message => VBS isolation not available | ||
private static bool IsVbsUnavailable(CryptographicException ex) | ||
{ | ||
const int NTE_NOT_SUPPORTED = unchecked((int)0x80890014); | ||
return ex.HResult == NTE_NOT_SUPPORTED | ||
|| (ex.Message != null && ex.Message.IndexOf("VBS key isolation", StringComparison.OrdinalIgnoreCase) >= 0) | ||
|| (ex.Message != null && ex.Message.IndexOf("Virtualization-based", StringComparison.OrdinalIgnoreCase) >= 0); | ||
} | ||
} | ||
} | ||
#endif |
154 changes: 154 additions & 0 deletions
154
...crosoft.Identity.Client/ManagedIdentity/KeyProviders/WindowsManagedIdentityKeyProvider.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
// src/client/Microsoft.Identity.Client/ManagedIdentity/Providers/WindowsManagedIdentityKeyProvider.cs | ||
#if !NETSTANDARD2_0 | ||
gladjohn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
using System; | ||
using System.Security.Cryptography; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Identity.Client.ManagedIdentity.KeyGuard; | ||
|
||
namespace Microsoft.Identity.Client.ManagedIdentity.Providers | ||
{ | ||
/// <summary> | ||
/// Windows policy: | ||
/// 1) KeyGuard (CVM/TVM) if available | ||
/// 2) Hardware (TPM/KSP via Microsoft Platform Crypto Provider) | ||
/// 3) In-memory fallback (delegates to InMemoryManagedIdentityKeyProvider) | ||
/// No certs; no attestation; no long-lived handles kept. | ||
/// </summary> | ||
internal sealed class WindowsManagedIdentityKeyProvider : IManagedIdentityKeyProvider | ||
{ | ||
private static readonly SemaphoreSlim s_once = new SemaphoreSlim(1, 1); | ||
private volatile MiKeyInfo _cached; | ||
|
||
private const string KgProviderName = "Microsoft Software Key Storage Provider"; | ||
private const string KgKeyName = "KeyGuardRSAKey"; | ||
private const string TpmProvider = "Microsoft Platform Crypto Provider"; | ||
private const string TpmKeyName = "MSAL_MI_PLATFORM_RSA"; | ||
|
||
public async Task<MiKeyInfo> GetOrCreateKeyAsync(CancellationToken ct) | ||
{ | ||
if (_cached != null) | ||
return _cached; | ||
|
||
await s_once.WaitAsync(ct).ConfigureAwait(false); | ||
try | ||
{ | ||
if (_cached != null) | ||
return _cached; | ||
|
||
// 1) KeyGuard (RSA-2048 under VBS isolation) | ||
RSA kgRsa; | ||
if (TryGetOrCreateKeyGuard(out kgRsa)) | ||
{ | ||
_cached = new MiKeyInfo(kgRsa, MiKeyType.KeyGuard); | ||
return _cached; | ||
} | ||
|
||
// 2) Hardware TPM/KSP (RSA-2048, non-exportable) | ||
RSA hwRsa; | ||
if (TryGetOrCreateHardwareRsa(out hwRsa)) | ||
{ | ||
_cached = new MiKeyInfo(hwRsa, MiKeyType.Hardware); | ||
return _cached; | ||
} | ||
|
||
// 3) Delegate fallback to portable in-memory provider | ||
var memProvider = new InMemoryManagedIdentityKeyProvider(); | ||
_cached = await memProvider.GetOrCreateKeyAsync(ct).ConfigureAwait(false); | ||
return _cached; | ||
} | ||
finally | ||
{ | ||
s_once.Release(); | ||
} | ||
} | ||
|
||
// --- KeyGuard path (RSA) --- | ||
private static bool TryGetOrCreateKeyGuard(out RSA rsa) | ||
{ | ||
rsa = default(RSA); | ||
|
||
try | ||
{ | ||
CngProvider provider = new CngProvider(KgProviderName); | ||
|
||
CngKey key; | ||
if (CngKey.Exists(KgKeyName, provider)) | ||
{ | ||
key = CngKey.Open(KgKeyName, provider); | ||
|
||
// Ensure actually KeyGuard-protected; if not, recreate as KeyGuard. | ||
if (!KeyGuardKey.IsKeyGuardProtected(key)) | ||
gladjohn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
{ | ||
key.Dispose(); | ||
key = KeyGuardKey.CreateFresh(KgProviderName, KgKeyName); | ||
} | ||
} | ||
else | ||
{ | ||
key = KeyGuardKey.CreateFresh(KgProviderName, KgKeyName); | ||
} | ||
|
||
rsa = new RSACng(key); | ||
if (rsa.KeySize < 2048) | ||
{ | ||
try | ||
{ rsa.KeySize = 2048; } | ||
catch { } | ||
} | ||
return true; | ||
} | ||
catch (PlatformNotSupportedException) | ||
{ | ||
// VBS/Core Isolation not available => KeyGuard unavailable | ||
return false; | ||
} | ||
catch (CryptographicException) | ||
{ | ||
return false; | ||
} | ||
} | ||
|
||
// --- Hardware (TPM/KSP) path (RSA) --- | ||
private static bool TryGetOrCreateHardwareRsa(out RSA rsa) | ||
{ | ||
rsa = default(RSA); | ||
|
||
try | ||
{ | ||
CngProvider provider = new CngProvider(TpmProvider); | ||
CngKeyOpenOptions openOpts = CngKeyOpenOptions.MachineKey; | ||
|
||
CngKey key = CngKey.Exists(TpmKeyName, provider, openOpts) | ||
? CngKey.Open(TpmKeyName, provider, openOpts) | ||
: CngKey.Create( | ||
CngAlgorithm.Rsa, | ||
TpmKeyName, | ||
new CngKeyCreationParameters | ||
{ | ||
Provider = provider, | ||
KeyUsage = CngKeyUsages.Signing, | ||
ExportPolicy = CngExportPolicies.None, // non-exportable | ||
KeyCreationOptions = CngKeyCreationOptions.MachineKey | ||
}); | ||
|
||
rsa = new RSACng(key); | ||
if (rsa.KeySize < 2048) | ||
{ | ||
try | ||
{ rsa.KeySize = 2048; } | ||
catch { } | ||
} | ||
return true; | ||
} | ||
catch (CryptographicException) | ||
{ | ||
return false; | ||
} | ||
} | ||
} | ||
} | ||
#endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityKeyProviderFactory.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using System; | ||
using System.Threading; | ||
using Microsoft.Identity.Client.ManagedIdentity.Providers; | ||
using Microsoft.Identity.Client.PlatformsCommon.Shared; | ||
|
||
namespace Microsoft.Identity.Client.ManagedIdentity | ||
{ | ||
internal static class ManagedIdentityKeyProviderFactory | ||
{ | ||
private static IManagedIdentityKeyProvider s_provider; | ||
|
||
internal static IManagedIdentityKeyProvider GetOrCreateProvider() | ||
{ | ||
var p = Volatile.Read(ref s_provider); | ||
if (p != null) | ||
return p; | ||
|
||
IManagedIdentityKeyProvider created = CreateProviderCore(); | ||
Interlocked.CompareExchange(ref s_provider, created, null); | ||
return s_provider!; | ||
gladjohn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
} | ||
|
||
private static IManagedIdentityKeyProvider CreateProviderCore() | ||
gladjohn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
{ | ||
#if NETSTANDARD2_0 | ||
return new InMemoryManagedIdentityKeyProvider(); | ||
#else | ||
if (DesktopOsHelper.IsWindows()) | ||
{ | ||
return new WindowsManagedIdentityKeyProvider(); | ||
} | ||
else | ||
{ | ||
return new InMemoryManagedIdentityKeyProvider(); | ||
} | ||
#endif | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.