diff --git a/LibsAndSamples.sln b/LibsAndSamples.sln
index 9fa92d549d..a353f606eb 100644
--- a/LibsAndSamples.sln
+++ b/LibsAndSamples.sln
@@ -46,7 +46,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "all", "all", "{C44AADC0-1E9
.editorconfig = .editorconfig
.gitattributes = .gitattributes
.gitignore = .gitignore
- CHANGELOG.md = CHANGELOG.md
build\CodeCoverage.runsettings = build\CodeCoverage.runsettings
build\credscan-exclusion.json = build\credscan-exclusion.json
Directory.Build.props = Directory.Build.props
@@ -196,6 +195,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MacMauiAppWithBroker", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MacConsoleAppWithBroker", "tests\devapps\MacConsoleAppWithBroker\MacConsoleAppWithBroker.csproj", "{DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Client.MtlsPop", "src\client\Microsoft.Identity.Client.MtlsPop\Microsoft.Identity.Client.MtlsPop.csproj", "{269A7A67-E48E-49A6-936B-60F1BE51F0CD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeyGuardAttestation", "tests\devapps\Managed Identity apps\KeyGuardAttestationApp\KeyGuardAttestation\KeyGuardAttestation.csproj", "{701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug + MobileApps|Any CPU = Debug + MobileApps|Any CPU
@@ -2022,6 +2025,90 @@ Global
{DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|x64.Build.0 = Release|Any CPU
{DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|x86.ActiveCfg = Release|Any CPU
{DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|x86.Build.0 = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|Any CPU.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|Any CPU.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|ARM.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|ARM.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|ARM64.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|ARM64.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|iPhone.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|iPhone.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|x64.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|x64.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|x86.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug + MobileApps|x86.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|ARM.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|x64.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Debug|x86.Build.0 = Debug|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|ARM.ActiveCfg = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|ARM.Build.0 = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|ARM64.Build.0 = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|iPhone.Build.0 = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|x64.ActiveCfg = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|x64.Build.0 = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|x86.ActiveCfg = Release|Any CPU
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD}.Release|x86.Build.0 = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|Any CPU.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|Any CPU.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|ARM.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|ARM.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|ARM64.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|ARM64.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|iPhone.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|iPhone.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|x64.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|x64.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|x86.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug + MobileApps|x86.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|ARM.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|x64.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Debug|x86.Build.0 = Debug|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|Any CPU.Build.0 = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|ARM.ActiveCfg = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|ARM.Build.0 = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|ARM64.Build.0 = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|iPhone.Build.0 = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|x64.ActiveCfg = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|x64.Build.0 = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|x86.ActiveCfg = Release|Any CPU
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2081,6 +2168,8 @@ Global
{97995B86-AA0F-3AF9-DA40-85A6263E4391} = {9B0B5396-4D95-4C15-82ED-DC22B5A3123F}
{AEF6BB00-931F-4638-955D-24D735625C34} = {34BE693E-3496-45A4-B1D2-D3A0E068EEDB}
{DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0} = {34BE693E-3496-45A4-B1D2-D3A0E068EEDB}
+ {269A7A67-E48E-49A6-936B-60F1BE51F0CD} = {1A37FD75-94E9-4D6F-953A-0DABBD7B49E9}
+ {701B8CD0-6D28-4B06-B8B4-9C47AF6E0793} = {BCAEE9AE-8D3E-4C77-A2E4-134E1552D5F8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {020399A9-DC27-4B82-9CAA-EF488665AC27}
diff --git a/build/template-pack-and-sign-all-nugets.yaml b/build/template-pack-and-sign-all-nugets.yaml
index b5d5377a21..e3a722d77d 100644
--- a/build/template-pack-and-sign-all-nugets.yaml
+++ b/build/template-pack-and-sign-all-nugets.yaml
@@ -37,6 +37,13 @@ steps:
ProjectRootPath: '$(Build.SourcesDirectory)\$(MsalSourceDir)src\client'
AssemblyName: 'Microsoft.Identity.Client.Extensions.Msal'
+# Sign binary and pack Microsoft.Identity.Client.MtlsPop
+- template: template-pack-and-sign-nuget.yaml
+ parameters:
+ BuildConfiguration: ${{ parameters.BuildConfiguration }}
+ ProjectRootPath: '$(Build.SourcesDirectory)\$(MsalSourceDir)src\client'
+ AssemblyName: 'Microsoft.Identity.Client.MtlsPop'
+
# Copy all packages out to staging
- task: CopyFiles@2
displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)\packages'
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClient.cs b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClient.cs
new file mode 100644
index 0000000000..1dc4b88419
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClient.cs
@@ -0,0 +1,118 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Runtime.InteropServices;
+using Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.Identity.Client.MtlsPop.Attestation
+{
+ ///
+ /// Managed façade for AttestationClientLib.dll. Holds initialization state,
+ /// does ref-count hygiene on , and returns a JWT.
+ ///
+ public sealed class AttestationClient : IDisposable
+ {
+ private bool _initialized;
+
+ ///
+ /// AttestationClient constructor. Pro-actively verifies the native DLL.
+ ///
+ ///
+ public AttestationClient()
+ {
+ /* step 0 ── ensure the resolver probes all valid locations
+ (env override → app base → System32/SysWOW64 → PATH) */
+ NativeDllResolver.EnsureLoaded();
+
+ /* step 1 ── optional proactive verification (non-fatal)
+ Keep the probe for diagnostics, but do NOT throw here; if the DLL
+ is truly unavailable/mismatched, InitAttestationLib will fail. */
+ string dllError = NativeDiagnostics.ProbeNativeDll();
+ // intentionally not throwing on dllError to avoid path-specific false negatives
+
+ /* step 2 ── load & initialize (logger is required by native lib) */
+ var info = new AttestationClientLib.AttestationLogInfo
+ {
+ Log = AttestationLogger.ConsoleLogger, // minimal rooted delegate; works on netstandard2.0 & net8.0
+ Ctx = IntPtr.Zero
+ };
+
+ _initialized = AttestationClientLib.InitAttestationLib(ref info) == 0;
+ if (!_initialized)
+ throw new InvalidOperationException("Failed to initialize AttestationClientLib.");
+ }
+
+ ///
+ /// Calls the native AttestKeyGuardImportKey and returns a structured result.
+ ///
+ public AttestationResult Attest(string endpoint,
+ SafeNCryptKeyHandle keyHandle,
+ string clientId)
+ {
+ if (!_initialized)
+ return new(AttestationStatus.NotInitialized, null, -1,
+ "Native library not initialized.");
+
+ IntPtr buf = IntPtr.Zero;
+ bool addRef = false;
+
+ try
+ {
+ keyHandle.DangerousAddRef(ref addRef);
+
+ int rc = AttestationClientLib.AttestKeyGuardImportKey(
+ endpoint, null, null, keyHandle, out buf, clientId);
+
+ if (rc != 0)
+ return new(AttestationStatus.NativeError, null, rc, null);
+
+ if (buf == IntPtr.Zero)
+ return new(AttestationStatus.TokenEmpty, null, 0,
+ "rc==0 but token buffer was null.");
+
+ string jwt = Marshal.PtrToStringAnsi(buf)!;
+ return new(AttestationStatus.Success, jwt, 0, null);
+ }
+ catch (DllNotFoundException ex)
+ {
+ return new(AttestationStatus.Exception, null, -1,
+ $"Native DLL not found: {ex.Message}");
+ }
+ catch (BadImageFormatException ex)
+ {
+ return new(AttestationStatus.Exception, null, -1,
+ $"Architecture mismatch (x86/x64) or corrupted DLL: {ex.Message}");
+ }
+ catch (SEHException ex)
+ {
+ return new(AttestationStatus.Exception, null, -1,
+ $"Native library raised SEHException: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ return new(AttestationStatus.Exception, null, -1, ex.Message);
+ }
+ finally
+ {
+ if (buf != IntPtr.Zero)
+ AttestationClientLib.FreeAttestationToken(buf);
+ if (addRef)
+ keyHandle.DangerousRelease();
+ }
+ }
+
+ ///
+ /// Disposes the client, releasing any resources and un-initializing the native library.
+ ///
+ public void Dispose()
+ {
+ if (_initialized)
+ {
+ AttestationClientLib.UninitAttestationLib();
+ _initialized = false;
+ }
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClientLib.cs b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClientLib.cs
new file mode 100644
index 0000000000..df84387024
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClientLib.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.Identity.Client.MtlsPop.Attestation
+{
+ internal static class AttestationClientLib
+ {
+ internal enum LogLevel { Error, Warn, Info, Debug }
+
+ internal delegate void LogFunc(
+ IntPtr ctx, string tag, LogLevel lvl, string func, int line, string msg);
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct AttestationLogInfo
+ {
+ public LogFunc Log;
+ public IntPtr Ctx;
+ }
+
+ [DllImport("AttestationClientLib.dll", CallingConvention = CallingConvention.Cdecl,
+ CharSet = CharSet.Ansi)]
+ internal static extern int InitAttestationLib(ref AttestationLogInfo info);
+
+ [DllImport("AttestationClientLib.dll", CallingConvention = CallingConvention.Cdecl,
+ CharSet = CharSet.Ansi)]
+ internal static extern int AttestKeyGuardImportKey(
+ string endpoint,
+ string authToken,
+ string clientPayload,
+ SafeNCryptKeyHandle keyHandle,
+ out IntPtr token,
+ string clientId);
+
+ [DllImport("AttestationClientLib.dll", CallingConvention = CallingConvention.Cdecl)]
+ internal static extern void FreeAttestationToken(IntPtr token);
+
+ [DllImport("AttestationClientLib.dll", CallingConvention = CallingConvention.Cdecl)]
+ internal static extern void UninitAttestationLib();
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationErrors.cs b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationErrors.cs
new file mode 100644
index 0000000000..0c47ceed76
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationErrors.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.Identity.Client.MtlsPop.Attestation
+{
+ internal static class AttestationErrors
+ {
+ internal static string Describe(AttestationResultErrorCode rc) => rc switch
+ {
+ AttestationResultErrorCode.ERRORCURLINITIALIZATION
+ => "libcurl failed to initialize (DLL missing or version mismatch).",
+ AttestationResultErrorCode.ERRORHTTPREQUESTFAILED
+ => "Could not reach the attestation service (network / proxy?).",
+ AttestationResultErrorCode.ERRORATTESTATIONFAILED
+ => "The enclave rejected the evidence (key type / PCR policy).",
+ AttestationResultErrorCode.ERRORJWTDECRYPTIONFAILED
+ => "The JWT returned by the service could not be decrypted.",
+ AttestationResultErrorCode.ERRORLOGGERINITIALIZATION
+ => "Native logger setup failed (rare).",
+ _ => rc.ToString() // default: enum name
+ };
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationLogger.cs b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationLogger.cs
new file mode 100644
index 0000000000..574a1d1821
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationLogger.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.Identity.Client.MtlsPop.Attestation
+{
+ internal static class AttestationLogger
+ {
+ ///
+ /// Attestation Logger
+ ///
+ internal static readonly AttestationClientLib.LogFunc ConsoleLogger = (ctx, tag, lvl, func, line, msg) =>
+ {
+ try
+ {
+ string sTag = ToText(tag);
+ string sFunc = ToText(func);
+ string sMsg = ToText(msg);
+
+ var lineText = $"[MtlsPop][{lvl}] {sTag} {sFunc}:{line} {sMsg}";
+
+ // Default: Trace (respects listeners; safe for all app types)
+ Trace.WriteLine(lineText);
+
+ // Opt-in console mirroring for local debugging
+ if (Environment.GetEnvironmentVariable("MSAL_MTLSPOP_LOG_TO_CONSOLE") == "1")
+ {
+ Console.WriteLine(lineText);
+ }
+ }
+ catch
+ {
+ }
+ };
+
+ // Converts either string or IntPtr (char*) to text. Works with any LogFunc variant.
+ private static string ToText(object value)
+ {
+ if (value is IntPtr p && p != IntPtr.Zero)
+ {
+ try
+ { return Marshal.PtrToStringAnsi(p) ?? string.Empty; }
+ catch { return string.Empty; }
+ }
+ return value?.ToString() ?? string.Empty;
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResult.cs b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResult.cs
new file mode 100644
index 0000000000..61e78efc0c
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResult.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Identity.Client.MtlsPop.Attestation
+{
+ ///
+ /// AttestationResult is the result of an attestation operation.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public sealed record AttestationResult(
+ AttestationStatus Status,
+ string Jwt,
+ int NativeErrorCode,
+ string ErrorMessage);
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResultErrorCode.cs b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResultErrorCode.cs
new file mode 100644
index 0000000000..4f02375292
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResultErrorCode.cs
@@ -0,0 +1,125 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.Identity.Client.MtlsPop.Attestation
+{
+ ///
+ /// Error codes returned by AttestationClientLib.dll.
+ /// A value of (0) indicates success; all other
+ /// values are negative and represent specific failure categories.
+ ///
+ internal enum AttestationResultErrorCode
+ {
+ /// The operation completed successfully.
+ SUCCESS = 0,
+
+ /// libcurl could not be initialized inside the native library.
+ ERRORCURLINITIALIZATION = -1,
+
+ /// The HTTP response body could not be parsed (malformed JSON, invalid JWT, etc.).
+ ERRORRESPONSEPARSING = -2,
+
+ /// Managed-Identity (MSI) access token could not be obtained.
+ ERRORMSITOKENNOTFOUND = -3,
+
+ /// The HTTP request exceeded the maximum retry count configured by the native client.
+ ERRORHTTPREQUESTEXCEEDEDRETRIES = -4,
+
+ /// An HTTP request to the attestation service failed (network error, non-200 status, timeout, etc.).
+ ERRORHTTPREQUESTFAILED = -5,
+
+ /// The attestation enclave rejected the supplied evidence (policy or signature failure).
+ ERRORATTESTATIONFAILED = -6,
+
+ /// libcurl reported “couldn’t send” (DNS resolution, TLS handshake, or socket error).
+ ERRORSENDINGCURLREQUESTFAILED = -7,
+
+ /// One or more input parameters passed to the native API were invalid or null.
+ ERRORINVALIDINPUTPARAMETER = -8,
+
+ /// Validation of the attestation parameters failed on the client side.
+ ERRORATTESTATIONPARAMETERSVALIDATIONFAILED = -9,
+
+ /// Native client failed to allocate heap memory.
+ ERRORFAILEDMEMORYALLOCATION = -10,
+
+ /// Could not retrieve OS build / version information required for the attestation payload.
+ ERRORFAILEDTOGETOSINFO = -11,
+
+ /// Internal TPM failure while gathering quotes or PCR values.
+ ERRORTPMINTERNALFAILURE = -12,
+
+ /// TPM operation (e.g., signing the quote) failed.
+ ERRORTPMOPERATIONFAILURE = -13,
+
+ /// The returned JWT could not be decrypted on the client.
+ ERRORJWTDECRYPTIONFAILED = -14,
+
+ /// JWT decryption failed due to a TPM error.
+ ERRORJWTDECRYPTIONTPMERROR = -15,
+
+ /// JSON in the service response was invalid or lacked required fields.
+ ERRORINVALIDJSONRESPONSE = -16,
+
+ /// The VCEK certificate blob returned from the service was empty.
+ ERROREMPTYVCEKCERT = -17,
+
+ /// The service response body was empty.
+ ERROREMPTYRESPONSE = -18,
+
+ /// The HTTP request body generated by the client was empty.
+ ERROREMPTYREQUESTBODY = -19,
+
+ /// Failed to parse the host-configuration-level (HCL) report.
+ ERRORHCLREPORTPARSINGFAILURE = -20,
+
+ /// The retrieved HCL report was empty.
+ ERRORHCLREPORTEMPTY = -21,
+
+ /// Could not extract JWK information from the attestation evidence.
+ ERROREXTRACTINGJWKINFO = -22,
+
+ /// Failed converting a JWK structure to an RSA public key.
+ ERRORCONVERTINGJWKTORSAPUB = -23,
+
+ /// EVP initialization for RSA encryption failed (OpenSSL).
+ ERROREVPPKEYENCRYPTINITFAILED = -24,
+
+ /// EVP encryption failed when building the attestation claim.
+ ERROREVPPKEYENCRYPTFAILED = -25,
+
+ /// Failed to decrypt data due to a TPM error.
+ ERRORDATADECRYPTIONTPMERROR = -26,
+
+ /// Parsing DNS information for the attestation service endpoint failed.
+ ERRORPARSINGDNSINFO = -27,
+
+ /// Failed to parse the attestation response envelope.
+ ERRORPARSINGATTESTATIONRESPONSE = -28,
+
+ /// Provisioning of the Attestation Key (AK) certificate failed.
+ ERRORAKCERTPROVISIONINGFAILED = -29,
+
+ /// Initialising the native attestation client failed.
+ ERRORCLIENTINITFAILED = -30,
+
+ /// The service returned an empty JWT.
+ ERROREMPTYJWTRESPONSE = -31,
+
+ /// Creating the KeyGuard attestation report failed on the client.
+ ERRORCREATEKGATTESTATIONREPORT = -32,
+
+ /// Failed to extract the public key from the import-only key.
+ ERROREXTRACTIMPORTKEYPUB = -33,
+
+ /// An unexpected C++ exception occurred inside the native client.
+ ERRORUNEXPECTEDEXCEPTION = -34,
+
+ /// Initialising the native logger failed (file I/O / permissions / path issues).
+ ERRORLOGGERINITIALIZATION = -35
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationStatus.cs b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationStatus.cs
new file mode 100644
index 0000000000..f8efeea78f
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationStatus.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.Identity.Client.MtlsPop.Attestation
+{
+ ///
+ /// High-level outcome categories returned by .
+ ///
+ public enum AttestationStatus
+ {
+ /// Everything succeeded; is populated.
+ Success = 0,
+
+ /// Native library returned a non-zero AttestationResultErrorCode.
+ NativeError = 1,
+
+ /// rc == 0 but the token buffer was null/empty.
+ TokenEmpty = 2,
+
+ /// could not initialize the native DLL.
+ NotInitialized = 3,
+
+ /// Any managed exception thrown while attempting the call.
+ Exception = 4
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/NativeDiagnostics.cs b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/NativeDiagnostics.cs
new file mode 100644
index 0000000000..9482039c8e
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/NativeDiagnostics.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.ComponentModel;
+using System.IO;
+
+namespace Microsoft.Identity.Client.MtlsPop.Attestation
+{
+ internal static class NativeDiagnostics
+ {
+ private const string NativeDll = "AttestationClientLib.dll";
+
+ internal static string ProbeNativeDll()
+ {
+ string path = Path.Combine(AppContext.BaseDirectory, NativeDll);
+
+ if (!File.Exists(path))
+ return $"Native DLL not found at: {path}";
+
+ IntPtr h;
+
+ try
+ {
+ h = WindowsDllLoader.Load(path);
+ }
+ catch (Win32Exception w32)
+ {
+ return w32.NativeErrorCode switch
+ {
+ 193 or 216 => $"{NativeDll} is the wrong architecture for this process.",
+ 126 => $"{NativeDll} found but one of its dependencies is missing (libcurl, OpenSSL, or VC++ runtime).",
+ _ => $"{NativeDll} could not be loaded (Win32 error 0x{w32.NativeErrorCode:X})."
+ };
+ }
+ catch (Exception ex)
+ {
+ return $"Unable to load {NativeDll}: {ex.Message}";
+ }
+
+ // success – unload and return null (meaning “no error”)
+ WindowsDllLoader.Free(h);
+ return null;
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/NativeDllResolver.cs b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/NativeDllResolver.cs
new file mode 100644
index 0000000000..8a127e461d
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/NativeDllResolver.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace Microsoft.Identity.Client.MtlsPop.Attestation
+{
+ ///
+ /// Ensures AttestationClientLib.dll is resolved from an override path, the app folder,
+ /// the system directories (System32/SysWOW64), or the default DLL search order (PATH).
+ ///
+ internal static class NativeDllResolver
+ {
+ private const string NativeDll = "AttestationClientLib.dll";
+ private static IntPtr s_module;
+
+ static NativeDllResolver()
+ {
+ // 1) Env override (per-job / per-process)
+ if (TryLoadFromEnv())
+ return;
+
+ // 2) App base directory
+ if (TryLoadFromAppBase())
+ return;
+
+ // 3) System directory (System32 for x64 process, SysWOW64 for x86 process)
+ if (TryLoadFromSystemDir())
+ return;
+
+ // 4) Let Windows search PATH / SxS / Known DLL dirs
+ s_module = WindowsDllLoader.Load(NativeDll);
+ }
+
+ /// Touch this method from startup code to trigger the static ctor.
+ internal static void EnsureLoaded() { }
+
+ private static bool TryLoadFromEnv()
+ {
+ var overrideDir = Environment.GetEnvironmentVariable("MSAL_MTLSPOP_NATIVE_PATH");
+ if (string.IsNullOrWhiteSpace(overrideDir))
+ {
+ return false;
+ }
+
+ var candidate = Path.Combine(overrideDir, NativeDll);
+ if (!File.Exists(candidate))
+ {
+ return false;
+ }
+
+ s_module = WindowsDllLoader.Load(candidate);
+ return s_module != IntPtr.Zero;
+ }
+
+ private static bool TryLoadFromAppBase()
+ {
+ var exePath = Path.Combine(AppContext.BaseDirectory, NativeDll);
+ if (!File.Exists(exePath))
+ {
+ return false;
+ }
+
+ s_module = WindowsDllLoader.Load(exePath);
+ return s_module != IntPtr.Zero;
+ }
+
+ private static bool TryLoadFromSystemDir()
+ {
+ var windowsRoot = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
+ if (string.IsNullOrEmpty(windowsRoot))
+ {
+ return false;
+ }
+
+ // x64 process -> System32, x86 process -> SysWOW64
+ var sysDir = Path.Combine(
+ windowsRoot,
+ Environment.Is64BitProcess ? "System32" : "SysWOW64");
+
+ var sysPath = Path.Combine(sysDir, NativeDll);
+ if (!File.Exists(sysPath))
+ {
+ return false;
+ }
+
+ s_module = WindowsDllLoader.Load(sysPath);
+ return s_module != IntPtr.Zero;
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/WindowsDllLoader.cs b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/WindowsDllLoader.cs
new file mode 100644
index 0000000000..315ee1e272
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/WindowsDllLoader.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.Identity.Client.MtlsPop.Attestation
+{
+ ///
+ /// Windows‑only helper that loads a native DLL from an absolute path.
+ ///
+ internal static class WindowsDllLoader
+ {
+ ///
+ /// Load the DLL and throw when the OS loader fails.
+ ///
+ /// Absolute path to AttestationClientLib.dll
+ /// Module handle (never zero on success).
+ ///
+ /// Thrown when kernel32!LoadLibraryW returns NULL.
+ ///
+ [DllImport("kernel32",
+ EntryPoint = "LoadLibraryW",
+ CharSet = CharSet.Unicode,
+ SetLastError = true,
+ ExactSpelling = true)]
+ private static extern IntPtr LoadLibraryW(string path);
+
+ internal static IntPtr Load(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ throw new ArgumentNullException(nameof(path));
+
+ IntPtr h = LoadLibraryW(path);
+
+ if (h == IntPtr.Zero)
+ {
+ // Preserve Win32 error code for diagnosis
+ int err = Marshal.GetLastWin32Error();
+ throw new Win32Exception(err, $"Unable to load {path}");
+ }
+
+ return h;
+ }
+
+ ///
+ /// Optionally expose a Free helper so callers can unload if needed.
+ ///
+ [DllImport("kernel32", SetLastError = true)]
+ private static extern bool FreeLibrary(IntPtr hModule);
+
+ internal static void Free(IntPtr handle)
+ {
+ if (handle != IntPtr.Zero)
+ FreeLibrary(handle);
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/IsExternalInit.cs b/src/client/Microsoft.Identity.Client.MtlsPop/IsExternalInit.cs
new file mode 100644
index 0000000000..dfb6a17acc
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/IsExternalInit.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#if NETSTANDARD
+namespace System.Runtime.CompilerServices
+{
+ internal static class IsExternalInit
+ {
+ }
+}
+#endif
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/ManagedIdentityPopExtensions.cs b/src/client/Microsoft.Identity.Client.MtlsPop/ManagedIdentityPopExtensions.cs
new file mode 100644
index 0000000000..1c0de4e6bf
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/ManagedIdentityPopExtensions.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Identity.Client.MtlsPop
+{
+ ///
+ /// Builder‑level opt‑in to Mutual‑TLS Proof‑of‑Possession for all
+ /// Managed‑Identity token requests produced by this application instance.
+ ///
+ ///
+ /// Requires the Microsoft.Identity.Client.MtlsPop package.
+ ///
+ public static class ManagedIdentityApplicationPopExtension
+ {
+ ///
+ /// Enables mTLS POP across the entire .
+ ///
+ public static AcquireTokenForManagedIdentityParameterBuilder WithMtlsProofOfPossession(
+ this AcquireTokenForManagedIdentityParameterBuilder builder)
+ {
+ builder.CommonParameters.IsManagedIdentityPopEnabled = true;
+ return builder;
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Microsoft.Identity.Client.MtlsPop.csproj b/src/client/Microsoft.Identity.Client.MtlsPop/Microsoft.Identity.Client.MtlsPop.csproj
new file mode 100644
index 0000000000..60159a76fb
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/Microsoft.Identity.Client.MtlsPop.csproj
@@ -0,0 +1,64 @@
+
+
+
+
+ netstandard2.0
+ net8.0
+ AnyCPU
+
+
+ $(TargetFrameworkNetStandard)
+
+
+ $(TargetFrameworkNetStandard);$(TargetFrameworkNet)
+
+
+ Debug;Release
+
+
+
+
+ $(MsalInternalVersion)
+
+ $(MicrosoftIdentityClientVersion)
+
+ MSAL.NET extension for managed identity proof-of-possession flows
+
+ This package contains binaries needed to use managed identity proof-of-possession (MTLS PoP) flows in applications using MSAL.NET.
+
+ Microsoft Authentication Library Managed Identity MSAL Proof-of-Possession
+ Microsoft Authentication Library Broker
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PopKeyAttestor.cs b/src/client/Microsoft.Identity.Client.MtlsPop/PopKeyAttestor.cs
new file mode 100644
index 0000000000..446a986e6c
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/PopKeyAttestor.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client.MtlsPop.Attestation;
+using Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.Identity.Client.MtlsPop
+{
+ ///
+ /// Static facade for attesting a KeyGuard/CNG key and getting a JWT back.
+ /// Key discovery / rotation is the caller's responsibility.
+ ///
+ public static class PopKeyAttestor
+ {
+ ///
+ /// Asynchronously attests a KeyGuard/CNG key with the remote attestation service and returns a JWT.
+ /// Wraps the synchronous in a Task.Run so callers can
+ /// avoid blocking. Cancellation only applies before the native call starts.
+ ///
+ /// Attestation service endpoint (required).
+ /// Valid SafeNCryptKeyHandle (must remain valid for duration of call).
+ /// Optional client identifier (may be null/empty).
+ /// Cancellation token (cooperative before scheduling / start).
+ public static Task AttestKeyGuardAsync(
+ string endpoint,
+ SafeNCryptKeyHandle keyHandle,
+ string clientId,
+ CancellationToken cancellationToken = default)
+ {
+ if (keyHandle is null || keyHandle.IsInvalid)
+ throw new ArgumentException("keyHandle must be a valid SafeNCryptKeyHandle", nameof(keyHandle));
+
+ if (string.IsNullOrWhiteSpace(endpoint))
+ throw new ArgumentException("endpoint must be provided", nameof(endpoint));
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ using var client = new AttestationClient();
+ return client.Attest(endpoint, keyHandle, clientId ?? string.Empty);
+ }
+ catch (Exception ex)
+ {
+ // Map any managed exception to AttestationStatus.Exception for consistency.
+ return new AttestationResult(AttestationStatus.Exception, string.Empty, -1, ex.Message);
+ }
+ }, cancellationToken);
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/PublicAPI.Shipped.txt
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+
diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/PublicAPI.Unshipped.txt
new file mode 100644
index 0000000000..a4e6266ba4
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/PublicAPI.Unshipped.txt
@@ -0,0 +1,24 @@
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationClient
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationClient.Attest(string endpoint, Microsoft.Win32.SafeHandles.SafeNCryptKeyHandle keyHandle, string clientId) -> Microsoft.Identity.Client.MtlsPop.Attestation.AttestationResult
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationClient.AttestationClient() -> void
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationClient.Dispose() -> void
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationResult
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationResult.AttestationResult(Microsoft.Identity.Client.MtlsPop.Attestation.AttestationStatus Status, string Jwt, int NativeErrorCode, string ErrorMessage) -> void
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationResult.ErrorMessage.get -> string
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationResult.ErrorMessage.init -> void
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationResult.Jwt.get -> string
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationResult.Jwt.init -> void
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationResult.NativeErrorCode.get -> int
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationResult.NativeErrorCode.init -> void
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationResult.Status.get -> Microsoft.Identity.Client.MtlsPop.Attestation.AttestationStatus
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationResult.Status.init -> void
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationStatus
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationStatus.Exception = 4 -> Microsoft.Identity.Client.MtlsPop.Attestation.AttestationStatus
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationStatus.NativeError = 1 -> Microsoft.Identity.Client.MtlsPop.Attestation.AttestationStatus
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationStatus.NotInitialized = 3 -> Microsoft.Identity.Client.MtlsPop.Attestation.AttestationStatus
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationStatus.Success = 0 -> Microsoft.Identity.Client.MtlsPop.Attestation.AttestationStatus
+Microsoft.Identity.Client.MtlsPop.Attestation.AttestationStatus.TokenEmpty = 2 -> Microsoft.Identity.Client.MtlsPop.Attestation.AttestationStatus
+Microsoft.Identity.Client.MtlsPop.ManagedIdentityApplicationPopExtension
+Microsoft.Identity.Client.MtlsPop.PopKeyAttestor
+static Microsoft.Identity.Client.MtlsPop.ManagedIdentityApplicationPopExtension.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
+static Microsoft.Identity.Client.MtlsPop.PopKeyAttestor.AttestKeyGuardAsync(string endpoint, Microsoft.Win32.SafeHandles.SafeNCryptKeyHandle keyHandle, string clientId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task
diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs
index e5bd4fdac8..6738da626b 100644
--- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs
+++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
@@ -38,6 +38,7 @@ internal class AcquireTokenCommonParameters
public SortedList>> CacheKeyComponents { get; internal set; }
public string FmiPathSuffix { get; internal set; }
public string ClientAssertionFmiPath { get; internal set; }
+ public bool IsManagedIdentityPopEnabled { get; set; }
public bool IsMtlsPopRequested { get; set; }
internal async Task InitMtlsPopParametersAsync(IServiceBundle serviceBundle, CancellationToken ct)
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs
index 3619c73864..69e0e160b4 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs
@@ -127,6 +127,8 @@ public string Claims
}
}
+ public bool IsManagedIdentityPopEnabled => _commonParameters.IsManagedIdentityPopEnabled;
+
public IAuthenticationOperation AuthenticationScheme => _commonParameters.AuthenticationOperation;
public IEnumerable PersistedCacheParameters => _commonParameters.AdditionalCacheParameters;
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs
index 67e9590e6a..9c0b799d91 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Collections.Generic;
@@ -91,7 +91,7 @@ protected override async Task ExecuteAsync(CancellationTok
logger.Info("[ManagedIdentityRequest] Access token retrieved from cache.");
try
- {
+ {
var proactivelyRefresh = SilentRequestHelper.NeedsRefresh(cachedAccessTokenItem);
// If needed, refreshes token in the background
@@ -137,7 +137,7 @@ protected override async Task ExecuteAsync(CancellationTok
}
private async Task GetAccessTokenAsync(
- CancellationToken cancellationToken,
+ CancellationToken cancellationToken,
ILoggerAdapter logger)
{
AuthenticationResult authResult;
@@ -157,7 +157,7 @@ private async Task GetAccessTokenAsync(
// 1) ForceRefresh is requested
// 2) Proactive refresh is in effect
// 3) Claims are present (revocation flow)
- if (_managedIdentityParameters.ForceRefresh ||
+ if (_managedIdentityParameters.ForceRefresh ||
AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo == CacheRefreshReason.ProactivelyRefreshed ||
!string.IsNullOrEmpty(_managedIdentityParameters.Claims))
{
@@ -194,7 +194,7 @@ private async Task SendTokenRequestForManagedIdentityAsync
await ResolveAuthorityAsync().ConfigureAwait(false);
- ManagedIdentityClient managedIdentityClient =
+ ManagedIdentityClient managedIdentityClient =
new ManagedIdentityClient(AuthenticationRequestParameters.RequestContext);
ManagedIdentityResponse managedIdentityResponse =
diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
index 578bb27e45..b9672089c2 100644
--- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
+++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
@@ -163,4 +163,9 @@
+
+
+
+
+
diff --git a/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs b/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs
index 769ebfcbf0..d6c67f8270 100644
--- a/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs
+++ b/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs
@@ -7,6 +7,7 @@
[assembly: InternalsVisibleTo("Microsoft.Identity.Client.Desktop" + KeyTokens.MSAL)]
[assembly: InternalsVisibleTo("Microsoft.Identity.Client.Desktop.WinUI3" + KeyTokens.MSAL)]
[assembly: InternalsVisibleTo("Microsoft.Identity.Client.Broker" + KeyTokens.MSAL)]
+[assembly: InternalsVisibleTo("Microsoft.Identity.Client.MtlsPop" + KeyTokens.MSAL)]
[assembly: InternalsVisibleTo("Microsoft.Identity.Test.Unit" + KeyTokens.MSAL)]
[assembly: InternalsVisibleTo("Microsoft.Identity.Test.Common" + KeyTokens.MSAL)]
diff --git a/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs b/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs
new file mode 100644
index 0000000000..54de6895b6
--- /dev/null
+++ b/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs
@@ -0,0 +1,169 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.Identity.Client.MtlsPop.Attestation;
+using Microsoft.Identity.Test.Common.Core.Helpers;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using Microsoft.Identity.Client.MtlsPop;
+using System.Threading.Tasks;
+using System.Threading;
+
+namespace Microsoft.Identity.Test.E2E
+{
+ [TestClass]
+ public class KeyGuardAttestationTests
+ {
+ private static CngKey CreateKeyGuardKey(string keyName)
+ {
+ const string ProviderName = "Microsoft Software Key Storage Provider";
+ const int NCRYPT_USE_VIRTUAL_ISOLATION_FLAG = 0x00020000;
+ const int NCRYPT_USE_PER_BOOT_KEY_FLAG = 0x00040000;
+
+ var p = new CngKeyCreationParameters
+ {
+ Provider = new CngProvider(ProviderName),
+ ExportPolicy = CngExportPolicies.None,
+ KeyUsage = CngKeyUsages.AllUsages,
+ KeyCreationOptions =
+ CngKeyCreationOptions.OverwriteExistingKey |
+ (CngKeyCreationOptions)NCRYPT_USE_VIRTUAL_ISOLATION_FLAG |
+ (CngKeyCreationOptions)NCRYPT_USE_PER_BOOT_KEY_FLAG,
+ };
+
+ // Set 2048-bit RSA length
+ p.Parameters.Add(new CngProperty(
+ "Length",
+ BitConverter.GetBytes(2048),
+ CngPropertyOptions.None));
+
+ return CngKey.Create(CngAlgorithm.Rsa, keyName, p);
+ }
+
+ private static bool IsKeyGuardProtected(CngKey key)
+ {
+ try
+ {
+ // KeyGuard exposes a "Virtual Iso" property that is non-zero when protected.
+ // Same check used in #5448. :contentReference[oaicite:1]{index=1}
+ var prop = key.GetProperty("Virtual Iso", CngPropertyOptions.None);
+ var bytes = prop.GetValue();
+ return bytes != null && bytes.Length >= 4 && BitConverter.ToInt32(bytes, 0) != 0;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ [TestCategory("MI_E2E_AzureArc")]
+ [RunOnAzureDevOps]
+ [TestMethod]
+ public void Attest_KeyGuardKey_OnAzureArc_Succeeds()
+ {
+ var endpoint = Environment.GetEnvironmentVariable("TOKEN_ATTESTATION_ENDPOINT");
+ if (string.IsNullOrWhiteSpace(endpoint))
+ {
+ Assert.Inconclusive($"Set {"TOKEN_ATTESTATION_ENDPOINT"} on the Azure Arc agent to run this test.");
+ }
+
+ var clientId = "MSI_CLIENT_ID";
+ string keyName = "MsalE2E_Keyguard";
+
+ CngKey key = null;
+ try
+ {
+ key = CreateKeyGuardKey(keyName);
+
+ if (!IsKeyGuardProtected(key))
+ {
+ Assert.Inconclusive("Key was created but not KeyGuard-protected. Is KeyGuard/VBS enabled on this machine?");
+ }
+
+ // Use the new public AttestationClient from the MtlsPop package. :contentReference[oaicite:2]{index=2}
+ using var client = new AttestationClient();
+ var result = client.Attest(endpoint, key.Handle, clientId);
+
+ // Validate success + JWT shape (3 parts).
+ Assert.AreEqual(AttestationStatus.Success, result.Status,
+ $"Attestation failed: status={result.Status}, nativeRc={result.NativeErrorCode}, msg={result.ErrorMessage}");
+ Assert.IsFalse(string.IsNullOrEmpty(result.Jwt), "Expected a non-empty attestation JWT.");
+
+ var parts = result.Jwt.Split('.');
+ Assert.AreEqual(3, parts.Length, "Expected a JWT (3 parts).");
+ }
+ catch (CryptographicException ex)
+ {
+ Assert.Inconclusive("CNG/KeyGuard is not available or access is denied on this machine: " + ex.Message);
+ }
+ catch (InvalidOperationException ex)
+ {
+ // Thrown by AttestationClient when the native DLL cannot be found/initialized.
+ Assert.Inconclusive("Attestation native lib not available on this runner: " + ex.Message);
+ }
+ finally
+ {
+ try { key?.Delete(); } catch { /* best-effort cleanup */ }
+ }
+ }
+
+ [TestCategory("MI_E2E_AzureArc")]
+ [RunOnAzureDevOps]
+ [TestMethod]
+ public async Task Attest_KeyGuardKey_OnAzureArc_Async_Succeeds()
+ {
+ var endpoint = Environment.GetEnvironmentVariable("TOKEN_ATTESTATION_ENDPOINT");
+ if (string.IsNullOrWhiteSpace(endpoint))
+ {
+ Assert.Inconclusive($"Set {"TOKEN_ATTESTATION_ENDPOINT"} on the Azure Arc agent to run this test.");
+ }
+
+ var clientId = "MSI_CLIENT_ID";
+ string keyName = "MsalE2E_Keyguard_Async";
+
+ CngKey key = null;
+ try
+ {
+ key = CreateKeyGuardKey(keyName);
+
+ if (!IsKeyGuardProtected(key))
+ {
+ Assert.Inconclusive("Key was created but not KeyGuard-protected. Is KeyGuard/VBS enabled on this machine?");
+ }
+
+ // Exercise the async facade (PopKeyAttestor) which wraps the synchronous native call in Task.Run.
+ var result = await PopKeyAttestor.AttestKeyGuardAsync(
+ endpoint,
+ key.Handle,
+ clientId: clientId,
+ cancellationToken: CancellationToken.None).ConfigureAwait(false);
+
+ Assert.AreEqual(AttestationStatus.Success, result.Status,
+ $"Async attestation failed: status={result.Status}, nativeRc={result.NativeErrorCode}, msg={result.ErrorMessage}");
+ Assert.IsFalse(string.IsNullOrEmpty(result.Jwt), "Expected a non-empty attestation JWT from async path.");
+
+ var parts = result.Jwt.Split('.');
+ Assert.AreEqual(3, parts.Length, "Expected a JWT (3 parts) from async path.");
+ }
+ catch (CryptographicException ex)
+ {
+ Assert.Inconclusive("CNG/KeyGuard is not available or access is denied on this machine: " + ex.Message);
+ }
+ catch (InvalidOperationException ex)
+ {
+ // Could originate from native initialization inside PopKeyAttestor (AttestationClient constructor).
+ Assert.Inconclusive("Attestation native lib not available on this runner (async path): " + ex.Message);
+ }
+ catch (ArgumentException ex)
+ {
+ Assert.Inconclusive("Handle or parameters invalid for async attestation path: " + ex.Message);
+ }
+ finally
+ {
+ try { key?.Delete(); } catch { /* best-effort cleanup */ }
+ }
+ }
+ }
+}
diff --git a/tests/Microsoft.Identity.Test.E2e/Microsoft.Identity.Test.E2E.MSI.csproj b/tests/Microsoft.Identity.Test.E2e/Microsoft.Identity.Test.E2E.MSI.csproj
index 6b85de84ce..11833e9e31 100644
--- a/tests/Microsoft.Identity.Test.E2e/Microsoft.Identity.Test.E2E.MSI.csproj
+++ b/tests/Microsoft.Identity.Test.E2e/Microsoft.Identity.Test.E2E.MSI.csproj
@@ -7,6 +7,7 @@
+
diff --git a/tests/devapps/Managed Identity apps/KeyGuardAttestationApp/KeyGuardAttestation/KeyGuardAttestation.csproj b/tests/devapps/Managed Identity apps/KeyGuardAttestationApp/KeyGuardAttestation/KeyGuardAttestation.csproj
new file mode 100644
index 0000000000..9485e36af6
--- /dev/null
+++ b/tests/devapps/Managed Identity apps/KeyGuardAttestationApp/KeyGuardAttestation/KeyGuardAttestation.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/tests/devapps/Managed Identity apps/KeyGuardAttestationApp/KeyGuardAttestation/Program.cs b/tests/devapps/Managed Identity apps/KeyGuardAttestationApp/KeyGuardAttestation/Program.cs
new file mode 100644
index 0000000000..35a0bcce80
--- /dev/null
+++ b/tests/devapps/Managed Identity apps/KeyGuardAttestationApp/KeyGuardAttestation/Program.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Security.Cryptography;
+using Microsoft.Identity.Client.MtlsPop.Attestation;
+
+class Program
+{
+ static void Main(string[] args)
+ {
+ var endpoint = Environment.GetEnvironmentVariable("TOKEN_ATTESTATION_ENDPOINT");
+ if (string.IsNullOrWhiteSpace(endpoint))
+ {
+ Console.WriteLine("TOKEN_ATTESTATION_ENDPOINT not set.");
+ return;
+ }
+ Console.WriteLine($"Endpoint = {endpoint}");
+
+ string clientId = Environment.GetEnvironmentVariable("MSI_CLIENT_ID") ?? "Test_Client_Id";
+ Console.WriteLine($"ClientId = {clientId}");
+
+ // Create a KeyGuard key
+ string keyName = "TestKey_" + Guid.NewGuid().ToString("N");
+ const string ProviderName = "Microsoft Software Key Storage Provider";
+ const int NCRYPT_USE_VIRTUAL_ISOLATION_FLAG = 0x00020000;
+ const int NCRYPT_USE_PER_BOOT_KEY_FLAG = 0x00040000;
+
+ var p = new CngKeyCreationParameters
+ {
+ Provider = new CngProvider(ProviderName),
+ ExportPolicy = CngExportPolicies.None,
+ KeyUsage = CngKeyUsages.AllUsages,
+ KeyCreationOptions =
+ CngKeyCreationOptions.OverwriteExistingKey |
+ (CngKeyCreationOptions)NCRYPT_USE_VIRTUAL_ISOLATION_FLAG |
+ (CngKeyCreationOptions)NCRYPT_USE_PER_BOOT_KEY_FLAG,
+ };
+ p.Parameters.Add(new CngProperty("Length", BitConverter.GetBytes(2048), CngPropertyOptions.None));
+
+ using var key = CngKey.Create(CngAlgorithm.Rsa, keyName, p);
+
+ // Try attestation
+ try
+ {
+ using var client = new AttestationClient();
+ var result = client.Attest(endpoint, key.Handle, clientId);
+
+ Console.WriteLine($"Status = {result.Status}, NativeRc = {result.NativeErrorCode}");
+ if (!string.IsNullOrEmpty(result.Jwt))
+ {
+ Console.WriteLine(string.Concat("JWT (truncated): ", result.Jwt.AsSpan(0, 50), "..."));
+ }
+ else
+ {
+ Console.WriteLine($"Error: {result.ErrorMessage}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Exception: " + ex);
+ }
+ finally
+ {
+ try
+ { CngKey.Open(keyName, new CngProvider(ProviderName)).Delete(); }
+ catch { }
+ }
+ }
+}
+
diff --git a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/ManagedIdentityAppVM.csproj b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/ManagedIdentityAppVM.csproj
index 150d7f869c..ea3a7b6aec 100644
--- a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/ManagedIdentityAppVM.csproj
+++ b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/ManagedIdentityAppVM.csproj
@@ -8,6 +8,7 @@
+
diff --git a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs
index 427b7ca149..f9f72091a9 100644
--- a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs
+++ b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs
@@ -4,6 +4,7 @@
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.AppConfig;
using Microsoft.IdentityModel.Abstractions;
+using Microsoft.Identity.Client.MtlsPop;
IIdentityLogger identityLogger = new IdentityLogger();
@@ -20,6 +21,7 @@
try
{
var result = await mi.AcquireTokenForManagedIdentity(scope)
+ .WithMtlsProofOfPossession()
.ExecuteAsync().ConfigureAwait(false);
Console.WriteLine("Success");