-
Notifications
You must be signed in to change notification settings - Fork 378
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 3 commits
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
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
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
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
14 changes: 14 additions & 0 deletions
14
src/client/Microsoft.Identity.Client/ManagedIdentity/IManagedIdentityKeyProvider.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,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); | ||
Robbie-Microsoft marked this conversation as resolved.
Show resolved
Hide resolved
Robbie-Microsoft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
94 changes: 94 additions & 0 deletions
94
...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,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); | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| private volatile ManagedIdentityKeyInfo _cached; | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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}"; | ||
Robbie-Microsoft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| logger?.WarningPii( | ||
Robbie-Microsoft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| $"[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 | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| rsa.KeySize = 2048; | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return rsa; | ||
| } | ||
| } | ||
| } | ||
78 changes: 78 additions & 0 deletions
78
src/client/Microsoft.Identity.Client/ManagedIdentity/KeyProviders/KeyGuardHelper.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,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 | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| private const string ProviderName = "Microsoft Software Key Storage Provider"; | ||
| private const string KeyName = "KeyGuardRSAKey"; | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // 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 | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| Provider = new CngProvider(ProviderName), | ||
| KeyUsage = CngKeyUsages.AllUsages, | ||
| ExportPolicy = CngExportPolicies.None, | ||
| KeyCreationOptions = | ||
| CngKeyCreationOptions.OverwriteExistingKey | ||
| | NCryptUseVirtualIsolationFlag | ||
| | NCryptUsePerBootKeyFlag | ||
| }; | ||
|
|
||
| p.Parameters.Add(new CngProperty("Length", | ||
| BitConverter.GetBytes(2048), | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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)) | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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"); | ||
| } | ||
| } | ||
| } | ||
142 changes: 142 additions & 0 deletions
142
src/client/Microsoft.Identity.Client/ManagedIdentity/KeyProviders/WindowsCngKeyOperations.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,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> | ||
Robbie-Microsoft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// 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); | ||
Robbie-Microsoft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (rsa.KeySize < 2048) | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| try | ||
| { rsa.KeySize = 2048; } | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| catch { /* some providers don't allow */ } | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| return true; | ||
| } | ||
| catch (PlatformNotSupportedException) | ||
| { | ||
| // VBS/Core Isolation not available => KeyGuard unavailable | ||
| logger?.Verbose(() => "[MI][WinKeyProvider] Exception creating KeyGuard key."); | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return false; | ||
| } | ||
| catch (CryptographicException) | ||
| { | ||
| return false; | ||
Robbie-Microsoft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
Robbie-Microsoft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // --- 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) | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| try | ||
| { rsa.KeySize = 2048; } | ||
| catch { /* PCP typically ignores post-create change */ } | ||
| } | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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 | ||
Robbie-Microsoft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| 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); | ||
| } | ||
| } | ||
| } | ||
| } | ||
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.