From cf72c384c25fc7bcc2743800f0ba33bf20bad4a5 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Mon, 8 Dec 2025 13:06:58 -0500 Subject: [PATCH 1/6] Implemented feature request --- .../ManagedIdentityPopExtensions.cs | 85 +----------- .../PublicApi/net8.0/PublicAPI.Shipped.txt | 3 - .../netstandard2.0/PublicAPI.Shipped.txt | 3 - .../ManagedIdentityPopExtensions.cs | 40 ++++++ .../V2/ImdsV2ManagedIdentitySource.cs | 42 ++++-- .../Properties/InternalsVisibleTo.cs | 1 + .../PublicApi/net462/PublicAPI.Unshipped.txt | 3 +- .../PublicApi/net472/PublicAPI.Unshipped.txt | 3 +- .../net8.0-android/PublicAPI.Unshipped.txt | 3 +- .../net8.0-ios/PublicAPI.Unshipped.txt | 3 +- .../PublicApi/net8.0/PublicAPI.Unshipped.txt | 3 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 3 +- .../Msal.KeyAttestation/AttestationClient.cs | 111 ++++++++++++++++ .../AttestationClientLib.cs | 45 +++++++ .../Msal.KeyAttestation/AttestationErrors.cs | 27 ++++ .../Msal.KeyAttestation/AttestationLogger.cs | 45 +++++++ .../Msal.KeyAttestation/AttestationResult.cs | 28 ++++ .../AttestationResultErrorCode.cs | 125 ++++++++++++++++++ .../Msal.KeyAttestation/AttestationStatus.cs | 30 +++++ .../Msal.KeyAttestation/IsExternalInit.cs | 11 ++ .../ManagedIdentityAttestationExtensions.cs | 67 ++++++++++ .../Msal.KeyAttestation.csproj | 44 ++++++ .../Msal.KeyAttestation/NativeDiagnostics.cs | 46 +++++++ .../Msal.KeyAttestation/PopKeyAttestor.cs | 62 +++++++++ .../PublicAPI/net8.0/PublicAPI.Shipped.txt | 0 .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 20 +++ .../netstandard2.0/PublicAPI.Shipped.txt | 0 .../netstandard2.0/PublicAPI.Unshipped.txt | 20 +++ .../Msal.KeyAttestation/WindowsDllLoader.cs | 65 +++++++++ .../KeyGuardAttestationTests.cs | 4 +- .../ManagedIdentityAzureArcTests.cs | 1 - .../ManagedIdentityImdsTests.cs | 1 - .../Microsoft.Identity.Test.E2E.MSI.csproj | 3 +- .../ManagedIdentityTests/ImdsV2Tests.cs | 22 +-- 34 files changed, 853 insertions(+), 116 deletions(-) create mode 100644 src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityPopExtensions.cs create mode 100644 src/client/Msal.KeyAttestation/AttestationClient.cs create mode 100644 src/client/Msal.KeyAttestation/AttestationClientLib.cs create mode 100644 src/client/Msal.KeyAttestation/AttestationErrors.cs create mode 100644 src/client/Msal.KeyAttestation/AttestationLogger.cs create mode 100644 src/client/Msal.KeyAttestation/AttestationResult.cs create mode 100644 src/client/Msal.KeyAttestation/AttestationResultErrorCode.cs create mode 100644 src/client/Msal.KeyAttestation/AttestationStatus.cs create mode 100644 src/client/Msal.KeyAttestation/IsExternalInit.cs create mode 100644 src/client/Msal.KeyAttestation/ManagedIdentityAttestationExtensions.cs create mode 100644 src/client/Msal.KeyAttestation/Msal.KeyAttestation.csproj create mode 100644 src/client/Msal.KeyAttestation/NativeDiagnostics.cs create mode 100644 src/client/Msal.KeyAttestation/PopKeyAttestor.cs create mode 100644 src/client/Msal.KeyAttestation/PublicAPI/net8.0/PublicAPI.Shipped.txt create mode 100644 src/client/Msal.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt create mode 100644 src/client/Msal.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt create mode 100644 src/client/Msal.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt create mode 100644 src/client/Msal.KeyAttestation/WindowsDllLoader.cs diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/ManagedIdentityPopExtensions.cs b/src/client/Microsoft.Identity.Client.MtlsPop/ManagedIdentityPopExtensions.cs index 742df7b00f..d79d4b2ab4 100644 --- a/src/client/Microsoft.Identity.Client.MtlsPop/ManagedIdentityPopExtensions.cs +++ b/src/client/Microsoft.Identity.Client.MtlsPop/ManagedIdentityPopExtensions.cs @@ -1,82 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Runtime.InteropServices; -using Microsoft.Identity.Client.MtlsPop.Attestation; -using Microsoft.Identity.Client.PlatformsCommon.Shared; - -namespace Microsoft.Identity.Client.MtlsPop -{ - /// - /// Registers the mTLS PoP attestation runtime (interop) by installing a provider - /// function into MSAL's internal config. - /// - public static class ManagedIdentityPopExtensions - { - /// - /// App-level registration: tells MSAL how to obtain a KeyGuard/CNG handle - /// and perform attestation to get the JWT needed for mTLS PoP. - /// - public static AcquireTokenForManagedIdentityParameterBuilder WithMtlsProofOfPossession( - this AcquireTokenForManagedIdentityParameterBuilder builder) - { - void MtlsNotSupportedForManagedIdentity(string message) - { - throw new MsalClientException( - MsalError.MtlsNotSupportedForManagedIdentity, - message); - } - - if (!DesktopOsHelper.IsWindows()) - { - MtlsNotSupportedForManagedIdentity(MsalErrorMessage.MtlsNotSupportedForNonWindowsMessage); - } - -#if NET462 - MtlsNotSupportedForManagedIdentity(MsalErrorMessage.MtlsNotSupportedForManagedIdentityMessage); -#endif - - builder.CommonParameters.IsMtlsPopRequested = true; - AddRuntimeSupport(builder); - return builder; - } - - /// - /// Adds the runtime support by registering the attestation function. - /// - /// - /// - private static void AddRuntimeSupport( - AcquireTokenForManagedIdentityParameterBuilder builder) - { - // Register the "runtime" function that PoP operation will invoke. - builder.CommonParameters.AttestationTokenProvider = - async (req, ct) => - { - // 1) Get the caller-provided KeyGuard/CNG handle - SafeHandle keyHandle = req.KeyHandle; - - // 2) Call the native interop via PopKeyAttestor - AttestationResult attestationResult = await PopKeyAttestor.AttestKeyGuardAsync( - req.AttestationEndpoint.AbsoluteUri, // expects string - keyHandle, - req.ClientId ?? string.Empty, - ct).ConfigureAwait(false); - - // 3) Map to MSAL's internal response - if (attestationResult != null && - attestationResult.Status == AttestationStatus.Success && - !string.IsNullOrWhiteSpace(attestationResult.Jwt)) - { - return new ManagedIdentity.AttestationTokenResponse { AttestationToken = attestationResult.Jwt }; - } - - throw new MsalClientException( - "attestation_failure", - $"Key Attestation failed " + - $"(status={attestationResult?.Status}, " + - $"code={attestationResult?.NativeErrorCode}). {attestationResult?.ErrorMessage}"); - }; - } - } -} +// This file intentionally left empty. +// The WithMtlsProofOfPossession extension method has been moved to the main MSAL package: +// Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession() +// +// For attestation support, reference the Msal.KeyAttestation package and call: +// .WithAttestationSupport() diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Shipped.txt index a068838189..e69de29bb2 100644 --- a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Shipped.txt @@ -1,3 +0,0 @@ -Microsoft.Identity.Client.MtlsPop.ManagedIdentityPopExtensions -static Microsoft.Identity.Client.MtlsPop.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder - diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Shipped.txt index a068838189..e69de29bb2 100644 --- a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Shipped.txt @@ -1,3 +0,0 @@ -Microsoft.Identity.Client.MtlsPop.ManagedIdentityPopExtensions -static Microsoft.Identity.Client.MtlsPop.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder - diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityPopExtensions.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityPopExtensions.cs new file mode 100644 index 0000000000..61e220fb4b --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityPopExtensions.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Client.PlatformsCommon.Shared; + +namespace Microsoft.Identity.Client +{ + /// + /// Extension methods for enabling mTLS Proof-of-Possession in managed identity flows. + /// + public static class ManagedIdentityPopExtensions + { + /// + /// Enables mTLS Proof-of-Possession for managed identity token acquisition. + /// When attestation is required (KeyGuard scenarios), use the Msal.KeyAttestation package + /// and call .WithAttestationSupport() after this method. + /// + /// The AcquireTokenForManagedIdentityParameterBuilder instance. + /// The builder to chain .With methods. + public static AcquireTokenForManagedIdentityParameterBuilder WithMtlsProofOfPossession( + this AcquireTokenForManagedIdentityParameterBuilder builder) + { + if (!DesktopOsHelper.IsWindows()) + { + throw new MsalClientException( + MsalError.MtlsNotSupportedForManagedIdentity, + MsalErrorMessage.MtlsNotSupportedForNonWindowsMessage); + } + +#if NET462 + throw new MsalClientException( + MsalError.MtlsNotSupportedForManagedIdentity, + MsalErrorMessage.MtlsNotSupportedForManagedIdentityMessage); +#else + builder.CommonParameters.IsMtlsPopRequested = true; + return builder; +#endif + } + } +} diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs index 404c619d8b..88af5ed92e 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs @@ -222,14 +222,8 @@ private async Task ExecuteCertificateRequestAsync( { OAuth2Header.XMsCorrelationId, _requestContext.CorrelationId.ToString() } }; - if (managedIdentityKeyInfo.Type != ManagedIdentityKeyType.KeyGuard) - { - throw new MsalClientException( - "mtls_pop_requires_keyguard", - "[ImdsV2] mTLS Proof-of-Possession requires a KeyGuard-backed key. Enable KeyGuard or use a KeyGuard-supported environment."); - } - - // Ask helper for JWT only for KeyGuard keys + // Attempt attestation only for KeyGuard keys when provider is available + // For non-KeyGuard keys (Hardware, InMemory), proceed with non-attested flow string attestationJwt = string.Empty; var attestationUri = new Uri(attestationEndpoint); @@ -241,6 +235,10 @@ private async Task ExecuteCertificateRequestAsync( managedIdentityKeyInfo, _requestContext.UserCancellationToken).ConfigureAwait(false); } + else + { + _requestContext.Logger.Info($"[ImdsV2] Using {managedIdentityKeyInfo.Type} key. Proceeding with non-attested mTLS PoP flow."); + } var certificateRequestBody = new CertificateRequestBody() { @@ -302,6 +300,22 @@ protected override async Task CreateRequestAsync(string { CsrMetadata csrMetadata = await GetCsrMetadataAsync(_requestContext, false).ConfigureAwait(false); + // Validate that mTLS PoP requires KeyGuard - fail fast before network calls + if (_isMtlsPopRequested) + { + IManagedIdentityKeyProvider keyProvider = _requestContext.ServiceBundle.PlatformProxy.ManagedIdentityKeyProvider; + ManagedIdentityKeyInfo keyInfo = await keyProvider + .GetOrCreateKeyAsync(_requestContext.Logger, _requestContext.UserCancellationToken) + .ConfigureAwait(false); + + if (keyInfo.Type != ManagedIdentityKeyType.KeyGuard) + { + throw new MsalClientException( + "mtls_pop_requires_keyguard", + $"[ImdsV2] mTLS Proof-of-Possession requires KeyGuard keys. Current key type: {keyInfo.Type}"); + } + } + string certCacheKey = _requestContext.ServiceBundle.Config.ClientId; MtlsBindingInfo mtlsBinding = await GetOrCreateMtlsBindingAsync( @@ -415,9 +429,19 @@ private async Task GetAttestationJwtAsync( ManagedIdentityKeyInfo keyInfo, CancellationToken cancellationToken) { - // Provider is a local dependency; missing provider is a client error + // Get the attestation provider if available var provider = _requestContext.AttestationTokenProvider; + // If no provider is configured: + // - For KeyGuard keys: proceed with ephemeral keys (non-attested flow) + // - For non-KeyGuard keys: proceed with non-attested flow + // This allows mTLS PoP to work without the attestation package + if (provider == null) + { + _requestContext.Logger.Info("[ImdsV2] No attestation provider configured. Proceeding with non-attested flow."); + return string.Empty; // Empty attestation token indicates non-attested flow + } + // KeyGuard requires RSACng on Windows if (keyInfo.Type == ManagedIdentityKeyType.KeyGuard && keyInfo.Key is not System.Security.Cryptography.RSACng rsaCng) diff --git a/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs b/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs index d6c67f8270..7e79f4511f 100644 --- a/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs +++ b/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs @@ -8,6 +8,7 @@ [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("Msal.KeyAttestation" + KeyTokens.MSAL)] [assembly: InternalsVisibleTo("Microsoft.Identity.Test.Unit" + KeyTokens.MSAL)] [assembly: InternalsVisibleTo("Microsoft.Identity.Test.Common" + KeyTokens.MSAL)] diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index 5f282702bb..f16bc964ac 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ - \ No newline at end of file +Microsoft.Identity.Client.ManagedIdentityPopExtensions +static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt index 5f282702bb..f16bc964ac 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ - \ No newline at end of file +Microsoft.Identity.Client.ManagedIdentityPopExtensions +static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index 5f282702bb..790ca24546 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ - \ No newline at end of file +Microsoft.Identity.Client.ManagedIdentityPopExtensions +static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index 5f282702bb..790ca24546 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ - \ No newline at end of file +Microsoft.Identity.Client.ManagedIdentityPopExtensions +static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index 5f282702bb..f16bc964ac 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ - \ No newline at end of file +Microsoft.Identity.Client.ManagedIdentityPopExtensions +static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt index 5f282702bb..f16bc964ac 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ - \ No newline at end of file +Microsoft.Identity.Client.ManagedIdentityPopExtensions +static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder diff --git a/src/client/Msal.KeyAttestation/AttestationClient.cs b/src/client/Msal.KeyAttestation/AttestationClient.cs new file mode 100644 index 0000000000..469e5dd5af --- /dev/null +++ b/src/client/Msal.KeyAttestation/AttestationClient.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace Msal.KeyAttestation +{ + /// + /// Managed façade for AttestationClientLib.dll. Holds initialization state, + /// does ref-count hygiene on , and returns a JWT. + /// + internal sealed class AttestationClient : IDisposable + { + private bool _initialized; + + /// + /// AttestationClient constructor. Relies on the default OS loader to locate the native DLL. + /// + /// + public AttestationClient() + { + string dllError = NativeDiagnostics.ProbeNativeDll(); + // intentionally not throwing on dllError + + // Load & initialize (logger is required by native lib) + var info = new AttestationClientLib.AttestationLogInfo + { + Log = AttestationLogger.ConsoleLogger, + 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/Msal.KeyAttestation/AttestationClientLib.cs b/src/client/Msal.KeyAttestation/AttestationClientLib.cs new file mode 100644 index 0000000000..844dbaa56c --- /dev/null +++ b/src/client/Msal.KeyAttestation/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 Msal.KeyAttestation +{ + 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/Msal.KeyAttestation/AttestationErrors.cs b/src/client/Msal.KeyAttestation/AttestationErrors.cs new file mode 100644 index 0000000000..b543266955 --- /dev/null +++ b/src/client/Msal.KeyAttestation/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 Msal.KeyAttestation +{ + 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/Msal.KeyAttestation/AttestationLogger.cs b/src/client/Msal.KeyAttestation/AttestationLogger.cs new file mode 100644 index 0000000000..99668452a2 --- /dev/null +++ b/src/client/Msal.KeyAttestation/AttestationLogger.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Msal.KeyAttestation +{ + 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); + } + 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/Msal.KeyAttestation/AttestationResult.cs b/src/client/Msal.KeyAttestation/AttestationResult.cs new file mode 100644 index 0000000000..45e3c25256 --- /dev/null +++ b/src/client/Msal.KeyAttestation/AttestationResult.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Msal.KeyAttestation +{ + /// + /// AttestationResult is the result of an attestation operation. + /// + /// High-level outcome category. + /// JWT on success; null otherwise (caller may pass null). + /// Raw native return code (0 on success). + /// Optional descriptive text for non-success cases. + /// + /// This is a positional record. The compiler synthesizes init-only auto-properties: + /// public AttestationStatus Status { get; init; } + /// public string Jwt { get; init; } + /// public int NativeErrorCode { get; init; } + /// public string ErrorMessage { get; init; } + /// Because they are init-only, values are fixed after construction; to "modify" use a 'with' + /// expression, e.g.: var updated = result with { Jwt = newJwt }; + /// The netstandard2.0 target relies on the IsExternalInit shim (see IsExternalInit.cs) to enable 'init'. + /// + public sealed record AttestationResult( + AttestationStatus Status, + string Jwt, + int NativeErrorCode, + string ErrorMessage); +} diff --git a/src/client/Msal.KeyAttestation/AttestationResultErrorCode.cs b/src/client/Msal.KeyAttestation/AttestationResultErrorCode.cs new file mode 100644 index 0000000000..3bec394e04 --- /dev/null +++ b/src/client/Msal.KeyAttestation/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 Msal.KeyAttestation +{ + /// + /// 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/Msal.KeyAttestation/AttestationStatus.cs b/src/client/Msal.KeyAttestation/AttestationStatus.cs new file mode 100644 index 0000000000..f72aeb709f --- /dev/null +++ b/src/client/Msal.KeyAttestation/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 Msal.KeyAttestation +{ + /// + /// 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/Msal.KeyAttestation/IsExternalInit.cs b/src/client/Msal.KeyAttestation/IsExternalInit.cs new file mode 100644 index 0000000000..6eeb15edba --- /dev/null +++ b/src/client/Msal.KeyAttestation/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/Msal.KeyAttestation/ManagedIdentityAttestationExtensions.cs b/src/client/Msal.KeyAttestation/ManagedIdentityAttestationExtensions.cs new file mode 100644 index 0000000000..b6cd6e82f2 --- /dev/null +++ b/src/client/Msal.KeyAttestation/ManagedIdentityAttestationExtensions.cs @@ -0,0 +1,67 @@ +// 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; +using Microsoft.Identity.Client.ManagedIdentity; + +namespace Msal.KeyAttestation +{ + /// + /// Extension methods for enabling KeyGuard attestation support in managed identity mTLS PoP flows. + /// + public static class ManagedIdentityAttestationExtensions + { + /// + /// Enables KeyGuard attestation support for managed identity mTLS Proof-of-Possession flows. + /// This method should be called after . + /// + /// The AcquireTokenForManagedIdentityParameterBuilder instance. + /// The builder to chain .With methods. + public static AcquireTokenForManagedIdentityParameterBuilder WithAttestationSupport( + this AcquireTokenForManagedIdentityParameterBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + // Register the attestation token provider + return builder.WithAttestationProviderForTests(async (req, ct) => + { + // Get the caller-provided KeyGuard/CNG handle + var keyHandle = req.KeyHandle; + + if (keyHandle == null) + { + throw new MsalClientException( + "attestation_key_handle_missing", + "KeyHandle is required for attestation but was not provided."); + } + + // Call the native interop via PopKeyAttestor + AttestationResult attestationResult = await PopKeyAttestor.AttestKeyGuardAsync( + req.AttestationEndpoint.AbsoluteUri, + keyHandle, + req.ClientId ?? string.Empty, + ct).ConfigureAwait(false); + + // Map to MSAL's internal response + if (attestationResult != null && + attestationResult.Status == AttestationStatus.Success && + !string.IsNullOrWhiteSpace(attestationResult.Jwt)) + { + return new AttestationTokenResponse { AttestationToken = attestationResult.Jwt }; + } + + throw new MsalClientException( + "attestation_failure", + $"Key Attestation failed " + + $"(status={attestationResult?.Status}, " + + $"code={attestationResult?.NativeErrorCode}). {attestationResult?.ErrorMessage}"); + }); + } + } +} diff --git a/src/client/Msal.KeyAttestation/Msal.KeyAttestation.csproj b/src/client/Msal.KeyAttestation/Msal.KeyAttestation.csproj new file mode 100644 index 0000000000..eb1e7dbeae --- /dev/null +++ b/src/client/Msal.KeyAttestation/Msal.KeyAttestation.csproj @@ -0,0 +1,44 @@ + + + + + netstandard2.0 + net8.0 + AnyCPU + + + $(TargetFrameworkNetStandard);$(TargetFrameworkNet) + + + Debug;Release + + + + + $(MsalInternalVersion) + + $(MicrosoftIdentityClientVersion)-preview + + MSAL.NET extension for KeyGuard attestation support + + This package contains binaries needed to enable KeyGuard attestation in managed identity proof-of-possession (mTLS PoP) flows using MSAL.NET. + + Microsoft Authentication Library Managed Identity MSAL KeyGuard Attestation Proof-of-Possession + Microsoft Authentication Library + + + + + + + + + + + + + + + + + diff --git a/src/client/Msal.KeyAttestation/NativeDiagnostics.cs b/src/client/Msal.KeyAttestation/NativeDiagnostics.cs new file mode 100644 index 0000000000..77ff1bd401 --- /dev/null +++ b/src/client/Msal.KeyAttestation/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 Msal.KeyAttestation +{ + 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/Msal.KeyAttestation/PopKeyAttestor.cs b/src/client/Msal.KeyAttestation/PopKeyAttestor.cs new file mode 100644 index 0000000000..44bbfc55fa --- /dev/null +++ b/src/client/Msal.KeyAttestation/PopKeyAttestor.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace Msal.KeyAttestation +{ + /// + /// 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, + SafeHandle keyHandle, + string clientId, + CancellationToken cancellationToken = default) + { + if (keyHandle is null) + throw new ArgumentNullException(nameof(keyHandle)); + + if (string.IsNullOrWhiteSpace(endpoint)) + throw new ArgumentNullException(nameof(endpoint)); + + if (keyHandle.IsInvalid) + throw new ArgumentException("keyHandle is invalid", nameof(keyHandle)); + + var safeNCryptKeyHandle = keyHandle as SafeNCryptKeyHandle + ?? throw new ArgumentException("keyHandle must be a SafeNCryptKeyHandle. Only Windows CNG keys are supported.", nameof(keyHandle)); + + cancellationToken.ThrowIfCancellationRequested(); + + return Task.Run(() => + { + try + { + using var client = new AttestationClient(); + return client.Attest(endpoint, safeNCryptKeyHandle, 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/Msal.KeyAttestation/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/client/Msal.KeyAttestation/PublicAPI/net8.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/client/Msal.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/client/Msal.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..9ab771bc96 --- /dev/null +++ b/src/client/Msal.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1,20 @@ +Msal.KeyAttestation.AttestationResult +Msal.KeyAttestation.AttestationResult.AttestationResult(Msal.KeyAttestation.AttestationStatus Status, string! Jwt, int NativeErrorCode, string! ErrorMessage) -> void +Msal.KeyAttestation.AttestationResult.ErrorMessage.get -> string! +Msal.KeyAttestation.AttestationResult.ErrorMessage.init -> void +Msal.KeyAttestation.AttestationResult.Jwt.get -> string! +Msal.KeyAttestation.AttestationResult.Jwt.init -> void +Msal.KeyAttestation.AttestationResult.NativeErrorCode.get -> int +Msal.KeyAttestation.AttestationResult.NativeErrorCode.init -> void +Msal.KeyAttestation.AttestationResult.Status.get -> Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.AttestationResult.Status.init -> void +Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.AttestationStatus.Exception = 4 -> Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.AttestationStatus.NativeError = 1 -> Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.AttestationStatus.NotInitialized = 3 -> Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.AttestationStatus.Success = 0 -> Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.AttestationStatus.TokenEmpty = 2 -> Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.ManagedIdentityAttestationExtensions +Msal.KeyAttestation.PopKeyAttestor +static Msal.KeyAttestation.ManagedIdentityAttestationExtensions.WithAttestationSupport(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! +static Msal.KeyAttestation.PopKeyAttestor.AttestKeyGuardAsync(string! endpoint, System.Runtime.InteropServices.SafeHandle! keyHandle, string! clientId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/client/Msal.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/src/client/Msal.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/client/Msal.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Msal.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..9ab771bc96 --- /dev/null +++ b/src/client/Msal.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,20 @@ +Msal.KeyAttestation.AttestationResult +Msal.KeyAttestation.AttestationResult.AttestationResult(Msal.KeyAttestation.AttestationStatus Status, string! Jwt, int NativeErrorCode, string! ErrorMessage) -> void +Msal.KeyAttestation.AttestationResult.ErrorMessage.get -> string! +Msal.KeyAttestation.AttestationResult.ErrorMessage.init -> void +Msal.KeyAttestation.AttestationResult.Jwt.get -> string! +Msal.KeyAttestation.AttestationResult.Jwt.init -> void +Msal.KeyAttestation.AttestationResult.NativeErrorCode.get -> int +Msal.KeyAttestation.AttestationResult.NativeErrorCode.init -> void +Msal.KeyAttestation.AttestationResult.Status.get -> Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.AttestationResult.Status.init -> void +Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.AttestationStatus.Exception = 4 -> Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.AttestationStatus.NativeError = 1 -> Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.AttestationStatus.NotInitialized = 3 -> Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.AttestationStatus.Success = 0 -> Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.AttestationStatus.TokenEmpty = 2 -> Msal.KeyAttestation.AttestationStatus +Msal.KeyAttestation.ManagedIdentityAttestationExtensions +Msal.KeyAttestation.PopKeyAttestor +static Msal.KeyAttestation.ManagedIdentityAttestationExtensions.WithAttestationSupport(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! +static Msal.KeyAttestation.PopKeyAttestor.AttestKeyGuardAsync(string! endpoint, System.Runtime.InteropServices.SafeHandle! keyHandle, string! clientId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/client/Msal.KeyAttestation/WindowsDllLoader.cs b/src/client/Msal.KeyAttestation/WindowsDllLoader.cs new file mode 100644 index 0000000000..5f2216e7ba --- /dev/null +++ b/src/client/Msal.KeyAttestation/WindowsDllLoader.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Microsoft.Identity.Client; + +namespace Msal.KeyAttestation +{ + /// + /// 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 MsalClientException( + "attestationmodule_load_failure", + $"Key Attestation Module load failed " + + $"(error={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/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs b/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs index 4542e9c210..d6687efaa6 100644 --- a/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs +++ b/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs @@ -28,13 +28,11 @@ If any prerequisite is missing (e.g., VBS off, endpoint unset, native DLL absent the test exits early with Assert.Inconclusive instead of failing the overall build. */ +using Microsoft.Identity.Client.MtlsPop; 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; diff --git a/tests/Microsoft.Identity.Test.E2e/ManagedIdentityAzureArcTests.cs b/tests/Microsoft.Identity.Test.E2e/ManagedIdentityAzureArcTests.cs index bf541b8396..2c99cf63e2 100644 --- a/tests/Microsoft.Identity.Test.E2e/ManagedIdentityAzureArcTests.cs +++ b/tests/Microsoft.Identity.Test.E2e/ManagedIdentityAzureArcTests.cs @@ -5,7 +5,6 @@ using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; using System.Threading.Tasks; namespace Microsoft.Identity.Test.E2E diff --git a/tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsTests.cs b/tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsTests.cs index d0b149c232..1947b1d083 100644 --- a/tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsTests.cs +++ b/tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsTests.cs @@ -3,7 +3,6 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; -using Microsoft.Identity.Client.MtlsPop; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; 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 ae7c12399c..6ed2dad75e 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,8 +7,9 @@ - + + diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs index da89c2c3fa..2b5318e060 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs @@ -646,7 +646,7 @@ public void AttachPrivateKeyToCert_NullPrivateKey_ThrowsArgumentNullException() #region Attestation Tests [TestMethod] - public async Task MtlsPop_AttestationProviderMissing_ThrowsClientException() + public async Task MtlsPop_NoAttestationProvider_UsesNonAttestedFlow() { using (new EnvVariableContext()) using (var httpManager = new MockHttpManager()) @@ -655,17 +655,19 @@ public async Task MtlsPop_AttestationProviderMissing_ThrowsClientException() var mi = await CreateManagedIdentityAsync(httpManager, managedIdentityKeyType: ManagedIdentityKeyType.KeyGuard).ConfigureAwait(false); - // CreateManagedIdentityAsync does a probe; Add one more CSR response for the actual acquire. - httpManager.AddMockHandler(MockHelpers.MockCsrResponse()); + // Add mocks for successful non-attested flow (CSR + issuecredential + token) + // Note: No attestation token in the certificate request + AddMocksToGetEntraToken(httpManager); - var ex = await Assert.ThrowsExceptionAsync(async () => - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .WithMtlsProofOfPossession() - // Intentionally DO NOT call .WithAttestationProviderForTests(...) - .ExecuteAsync().ConfigureAwait(false) - ).ConfigureAwait(false); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .WithMtlsProofOfPossession() + // Intentionally DO NOT call .WithAttestationProviderForTests(...) + .ExecuteAsync().ConfigureAwait(false); - Assert.AreEqual("attestation_failure", ex.ErrorCode); + Assert.IsNotNull(result); + Assert.AreEqual(MTLSPoP, result.TokenType, "Should get mTLS PoP token without attestation provider"); + Assert.IsNotNull(result.BindingCertificate, "Should have binding certificate even without attestation"); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); } } From ba29c763a2f5bc0d76f1ed363dd33be008138ac1 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 10 Dec 2025 17:34:46 -0500 Subject: [PATCH 2/6] renamed attestation package --- .../AttestationClient.cs | 2 +- .../AttestationClientLib.cs | 2 +- .../AttestationErrors.cs | 2 +- .../AttestationLogger.cs | 2 +- .../AttestationResult.cs | 2 +- .../AttestationResultErrorCode.cs | 2 +- .../AttestationStatus.cs | 2 +- .../IsExternalInit.cs | 0 .../ManagedIdentityAttestationExtensions.cs | 2 +- ...oft.Identity.Client.KeyAttestation.csproj} | 5 +++++ .../NativeDiagnostics.cs | 2 +- .../PopKeyAttestor.cs | 2 +- .../PublicAPI/net8.0/PublicAPI.Shipped.txt | 0 .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 20 +++++++++++++++++++ .../netstandard2.0/PublicAPI.Shipped.txt | 0 .../netstandard2.0/PublicAPI.Unshipped.txt | 20 +++++++++++++++++++ .../WindowsDllLoader.cs | 2 +- .../Properties/InternalsVisibleTo.cs | 2 +- .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 20 ------------------- .../netstandard2.0/PublicAPI.Unshipped.txt | 20 ------------------- 20 files changed, 57 insertions(+), 52 deletions(-) rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/AttestationClient.cs (98%) rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/AttestationClientLib.cs (96%) rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/AttestationErrors.cs (95%) rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/AttestationLogger.cs (96%) rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/AttestationResult.cs (96%) rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/AttestationResultErrorCode.cs (99%) rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/AttestationStatus.cs (94%) rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/IsExternalInit.cs (100%) rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/ManagedIdentityAttestationExtensions.cs (98%) rename src/client/{Msal.KeyAttestation/Msal.KeyAttestation.csproj => Microsoft.Identity.Client.KeyAttestation/Microsoft.Identity.Client.KeyAttestation.csproj} (92%) rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/NativeDiagnostics.cs (96%) rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/PopKeyAttestor.cs (98%) rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/PublicAPI/net8.0/PublicAPI.Shipped.txt (100%) create mode 100644 src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt (100%) create mode 100644 src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt rename src/client/{Msal.KeyAttestation => Microsoft.Identity.Client.KeyAttestation}/WindowsDllLoader.cs (97%) delete mode 100644 src/client/Msal.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt delete mode 100644 src/client/Msal.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt diff --git a/src/client/Msal.KeyAttestation/AttestationClient.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClient.cs similarity index 98% rename from src/client/Msal.KeyAttestation/AttestationClient.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClient.cs index 469e5dd5af..79b4142b4e 100644 --- a/src/client/Msal.KeyAttestation/AttestationClient.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClient.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; -namespace Msal.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation { /// /// Managed façade for AttestationClientLib.dll. Holds initialization state, diff --git a/src/client/Msal.KeyAttestation/AttestationClientLib.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClientLib.cs similarity index 96% rename from src/client/Msal.KeyAttestation/AttestationClientLib.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClientLib.cs index 844dbaa56c..247e7af182 100644 --- a/src/client/Msal.KeyAttestation/AttestationClientLib.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClientLib.cs @@ -6,7 +6,7 @@ using System.IO; using System.Runtime.InteropServices; -namespace Msal.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation { internal static class AttestationClientLib { diff --git a/src/client/Msal.KeyAttestation/AttestationErrors.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationErrors.cs similarity index 95% rename from src/client/Msal.KeyAttestation/AttestationErrors.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/AttestationErrors.cs index b543266955..8074af02ce 100644 --- a/src/client/Msal.KeyAttestation/AttestationErrors.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationErrors.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Text; -namespace Msal.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation { internal static class AttestationErrors { diff --git a/src/client/Msal.KeyAttestation/AttestationLogger.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationLogger.cs similarity index 96% rename from src/client/Msal.KeyAttestation/AttestationLogger.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/AttestationLogger.cs index 99668452a2..22e37e9d93 100644 --- a/src/client/Msal.KeyAttestation/AttestationLogger.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationLogger.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; -namespace Msal.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation { internal static class AttestationLogger { diff --git a/src/client/Msal.KeyAttestation/AttestationResult.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationResult.cs similarity index 96% rename from src/client/Msal.KeyAttestation/AttestationResult.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/AttestationResult.cs index 45e3c25256..e78408d496 100644 --- a/src/client/Msal.KeyAttestation/AttestationResult.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationResult.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Msal.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation { /// /// AttestationResult is the result of an attestation operation. diff --git a/src/client/Msal.KeyAttestation/AttestationResultErrorCode.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationResultErrorCode.cs similarity index 99% rename from src/client/Msal.KeyAttestation/AttestationResultErrorCode.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/AttestationResultErrorCode.cs index 3bec394e04..0b15eab683 100644 --- a/src/client/Msal.KeyAttestation/AttestationResultErrorCode.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationResultErrorCode.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Text; -namespace Msal.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation { /// /// Error codes returned by AttestationClientLib.dll. diff --git a/src/client/Msal.KeyAttestation/AttestationStatus.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationStatus.cs similarity index 94% rename from src/client/Msal.KeyAttestation/AttestationStatus.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/AttestationStatus.cs index f72aeb709f..b49abadb00 100644 --- a/src/client/Msal.KeyAttestation/AttestationStatus.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationStatus.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Text; -namespace Msal.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation { /// /// High-level outcome categories returned by . diff --git a/src/client/Msal.KeyAttestation/IsExternalInit.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/IsExternalInit.cs similarity index 100% rename from src/client/Msal.KeyAttestation/IsExternalInit.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/IsExternalInit.cs diff --git a/src/client/Msal.KeyAttestation/ManagedIdentityAttestationExtensions.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs similarity index 98% rename from src/client/Msal.KeyAttestation/ManagedIdentityAttestationExtensions.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs index b6cd6e82f2..958bad28cb 100644 --- a/src/client/Msal.KeyAttestation/ManagedIdentityAttestationExtensions.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs @@ -7,7 +7,7 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.ManagedIdentity; -namespace Msal.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation { /// /// Extension methods for enabling KeyGuard attestation support in managed identity mTLS PoP flows. diff --git a/src/client/Msal.KeyAttestation/Msal.KeyAttestation.csproj b/src/client/Microsoft.Identity.Client.KeyAttestation/Microsoft.Identity.Client.KeyAttestation.csproj similarity index 92% rename from src/client/Msal.KeyAttestation/Msal.KeyAttestation.csproj rename to src/client/Microsoft.Identity.Client.KeyAttestation/Microsoft.Identity.Client.KeyAttestation.csproj index eb1e7dbeae..68a9cd6c12 100644 --- a/src/client/Msal.KeyAttestation/Msal.KeyAttestation.csproj +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Microsoft.Identity.Client.KeyAttestation.csproj @@ -36,6 +36,11 @@ + + + $(NoWarn);RS0016;RS0017;RS0036;RS0041 + + diff --git a/src/client/Msal.KeyAttestation/NativeDiagnostics.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/NativeDiagnostics.cs similarity index 96% rename from src/client/Msal.KeyAttestation/NativeDiagnostics.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/NativeDiagnostics.cs index 77ff1bd401..f47b85bae3 100644 --- a/src/client/Msal.KeyAttestation/NativeDiagnostics.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/NativeDiagnostics.cs @@ -5,7 +5,7 @@ using System.ComponentModel; using System.IO; -namespace Msal.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation { internal static class NativeDiagnostics { diff --git a/src/client/Msal.KeyAttestation/PopKeyAttestor.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs similarity index 98% rename from src/client/Msal.KeyAttestation/PopKeyAttestor.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs index 44bbfc55fa..2ddcefda3a 100644 --- a/src/client/Msal.KeyAttestation/PopKeyAttestor.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; -namespace Msal.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation { /// /// Static facade for attesting a KeyGuard/CNG key and getting a JWT back. diff --git a/src/client/Msal.KeyAttestation/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Shipped.txt similarity index 100% rename from src/client/Msal.KeyAttestation/PublicAPI/net8.0/PublicAPI.Shipped.txt rename to src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Shipped.txt diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..d89d18cd59 --- /dev/null +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1,20 @@ +Microsoft.Identity.Client.KeyAttestation.AttestationResult +Microsoft.Identity.Client.KeyAttestation.AttestationResult.AttestationResult(Microsoft.Identity.Client.KeyAttestation.AttestationStatus Status, string! Jwt, int NativeErrorCode, string! ErrorMessage) -> void +Microsoft.Identity.Client.KeyAttestation.AttestationResult.ErrorMessage.get -> string! +Microsoft.Identity.Client.KeyAttestation.AttestationResult.ErrorMessage.init -> void +Microsoft.Identity.Client.KeyAttestation.AttestationResult.Jwt.get -> string! +Microsoft.Identity.Client.KeyAttestation.AttestationResult.Jwt.init -> void +Microsoft.Identity.Client.KeyAttestation.AttestationResult.NativeErrorCode.get -> int +Microsoft.Identity.Client.KeyAttestation.AttestationResult.NativeErrorCode.init -> void +Microsoft.Identity.Client.KeyAttestation.AttestationResult.Status.get -> Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.AttestationResult.Status.init -> void +Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.AttestationStatus.Exception = 4 -> Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.AttestationStatus.NativeError = 1 -> Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.AttestationStatus.NotInitialized = 3 -> Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.AttestationStatus.Success = 0 -> Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.AttestationStatus.TokenEmpty = 2 -> Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.ManagedIdentityAttestationExtensions +Microsoft.Identity.Client.KeyAttestation.PopKeyAttestor +static Microsoft.Identity.Client.KeyAttestation.ManagedIdentityAttestationExtensions.WithAttestationSupport(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! +static Microsoft.Identity.Client.KeyAttestation.PopKeyAttestor.AttestKeyGuardAsync(string! endpoint, System.Runtime.InteropServices.SafeHandle! keyHandle, string! clientId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/client/Msal.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt similarity index 100% rename from src/client/Msal.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt rename to src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..d89d18cd59 --- /dev/null +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,20 @@ +Microsoft.Identity.Client.KeyAttestation.AttestationResult +Microsoft.Identity.Client.KeyAttestation.AttestationResult.AttestationResult(Microsoft.Identity.Client.KeyAttestation.AttestationStatus Status, string! Jwt, int NativeErrorCode, string! ErrorMessage) -> void +Microsoft.Identity.Client.KeyAttestation.AttestationResult.ErrorMessage.get -> string! +Microsoft.Identity.Client.KeyAttestation.AttestationResult.ErrorMessage.init -> void +Microsoft.Identity.Client.KeyAttestation.AttestationResult.Jwt.get -> string! +Microsoft.Identity.Client.KeyAttestation.AttestationResult.Jwt.init -> void +Microsoft.Identity.Client.KeyAttestation.AttestationResult.NativeErrorCode.get -> int +Microsoft.Identity.Client.KeyAttestation.AttestationResult.NativeErrorCode.init -> void +Microsoft.Identity.Client.KeyAttestation.AttestationResult.Status.get -> Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.AttestationResult.Status.init -> void +Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.AttestationStatus.Exception = 4 -> Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.AttestationStatus.NativeError = 1 -> Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.AttestationStatus.NotInitialized = 3 -> Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.AttestationStatus.Success = 0 -> Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.AttestationStatus.TokenEmpty = 2 -> Microsoft.Identity.Client.KeyAttestation.AttestationStatus +Microsoft.Identity.Client.KeyAttestation.ManagedIdentityAttestationExtensions +Microsoft.Identity.Client.KeyAttestation.PopKeyAttestor +static Microsoft.Identity.Client.KeyAttestation.ManagedIdentityAttestationExtensions.WithAttestationSupport(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! +static Microsoft.Identity.Client.KeyAttestation.PopKeyAttestor.AttestKeyGuardAsync(string! endpoint, System.Runtime.InteropServices.SafeHandle! keyHandle, string! clientId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/client/Msal.KeyAttestation/WindowsDllLoader.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/WindowsDllLoader.cs similarity index 97% rename from src/client/Msal.KeyAttestation/WindowsDllLoader.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/WindowsDllLoader.cs index 5f2216e7ba..3084769f06 100644 --- a/src/client/Msal.KeyAttestation/WindowsDllLoader.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/WindowsDllLoader.cs @@ -6,7 +6,7 @@ using System.Runtime.InteropServices; using Microsoft.Identity.Client; -namespace Msal.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation { /// /// Windows‑only helper that loads a native DLL from an absolute path. diff --git a/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs b/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs index 7e79f4511f..e10a685979 100644 --- a/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs +++ b/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs @@ -8,7 +8,7 @@ [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("Msal.KeyAttestation" + KeyTokens.MSAL)] +[assembly: InternalsVisibleTo("Microsoft.Identity.Client.KeyAttestation" + KeyTokens.MSAL)] [assembly: InternalsVisibleTo("Microsoft.Identity.Test.Unit" + KeyTokens.MSAL)] [assembly: InternalsVisibleTo("Microsoft.Identity.Test.Common" + KeyTokens.MSAL)] diff --git a/src/client/Msal.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/client/Msal.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 9ab771bc96..0000000000 --- a/src/client/Msal.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,20 +0,0 @@ -Msal.KeyAttestation.AttestationResult -Msal.KeyAttestation.AttestationResult.AttestationResult(Msal.KeyAttestation.AttestationStatus Status, string! Jwt, int NativeErrorCode, string! ErrorMessage) -> void -Msal.KeyAttestation.AttestationResult.ErrorMessage.get -> string! -Msal.KeyAttestation.AttestationResult.ErrorMessage.init -> void -Msal.KeyAttestation.AttestationResult.Jwt.get -> string! -Msal.KeyAttestation.AttestationResult.Jwt.init -> void -Msal.KeyAttestation.AttestationResult.NativeErrorCode.get -> int -Msal.KeyAttestation.AttestationResult.NativeErrorCode.init -> void -Msal.KeyAttestation.AttestationResult.Status.get -> Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.AttestationResult.Status.init -> void -Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.AttestationStatus.Exception = 4 -> Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.AttestationStatus.NativeError = 1 -> Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.AttestationStatus.NotInitialized = 3 -> Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.AttestationStatus.Success = 0 -> Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.AttestationStatus.TokenEmpty = 2 -> Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.ManagedIdentityAttestationExtensions -Msal.KeyAttestation.PopKeyAttestor -static Msal.KeyAttestation.ManagedIdentityAttestationExtensions.WithAttestationSupport(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! -static Msal.KeyAttestation.PopKeyAttestor.AttestKeyGuardAsync(string! endpoint, System.Runtime.InteropServices.SafeHandle! keyHandle, string! clientId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/client/Msal.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Msal.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 9ab771bc96..0000000000 --- a/src/client/Msal.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,20 +0,0 @@ -Msal.KeyAttestation.AttestationResult -Msal.KeyAttestation.AttestationResult.AttestationResult(Msal.KeyAttestation.AttestationStatus Status, string! Jwt, int NativeErrorCode, string! ErrorMessage) -> void -Msal.KeyAttestation.AttestationResult.ErrorMessage.get -> string! -Msal.KeyAttestation.AttestationResult.ErrorMessage.init -> void -Msal.KeyAttestation.AttestationResult.Jwt.get -> string! -Msal.KeyAttestation.AttestationResult.Jwt.init -> void -Msal.KeyAttestation.AttestationResult.NativeErrorCode.get -> int -Msal.KeyAttestation.AttestationResult.NativeErrorCode.init -> void -Msal.KeyAttestation.AttestationResult.Status.get -> Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.AttestationResult.Status.init -> void -Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.AttestationStatus.Exception = 4 -> Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.AttestationStatus.NativeError = 1 -> Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.AttestationStatus.NotInitialized = 3 -> Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.AttestationStatus.Success = 0 -> Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.AttestationStatus.TokenEmpty = 2 -> Msal.KeyAttestation.AttestationStatus -Msal.KeyAttestation.ManagedIdentityAttestationExtensions -Msal.KeyAttestation.PopKeyAttestor -static Msal.KeyAttestation.ManagedIdentityAttestationExtensions.WithAttestationSupport(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! -static Msal.KeyAttestation.PopKeyAttestor.AttestKeyGuardAsync(string! endpoint, System.Runtime.InteropServices.SafeHandle! keyHandle, string! clientId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! From 595aaa83ff1d76824afc562f3a03f85f57ec14c0 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 10 Dec 2025 17:45:09 -0500 Subject: [PATCH 3/6] Removed MtlsPop package and updated references --- prototype/MsiV2DemoApp/MsiV2DemoApp.csproj | 1 - .../Attestation/AttestationClient.cs | 111 ---------------- .../Attestation/AttestationClientLib.cs | 45 ------- .../Attestation/AttestationErrors.cs | 27 ---- .../Attestation/AttestationLogger.cs | 45 ------- .../Attestation/AttestationResult.cs | 28 ---- .../Attestation/AttestationResultErrorCode.cs | 125 ------------------ .../Attestation/AttestationStatus.cs | 30 ----- .../Attestation/NativeDiagnostics.cs | 46 ------- .../Attestation/WindowsDllLoader.cs | 64 --------- .../IsExternalInit.cs | 11 -- .../ManagedIdentityPopExtensions.cs | 9 -- .../Microsoft.Identity.Client.MtlsPop.csproj | 50 ------- .../PopKeyAttestor.cs | 63 --------- .../PublicApi/net8.0/PublicAPI.Shipped.txt | 0 .../PublicApi/net8.0/PublicAPI.Unshipped.txt | 1 - .../netstandard2.0/PublicAPI.Shipped.txt | 0 .../netstandard2.0/PublicAPI.Unshipped.txt | 1 - .../Properties/InternalsVisibleTo.cs | 1 - .../Microsoft.Identity.Test.E2E.MSI.csproj | 3 +- .../Microsoft.Identity.Test.Unit.csproj | 1 - .../ManagedIdentityAppVM.csproj | 1 - 22 files changed, 1 insertion(+), 662 deletions(-) delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClient.cs delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClientLib.cs delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationErrors.cs delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationLogger.cs delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResult.cs delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResultErrorCode.cs delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationStatus.cs delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/Attestation/NativeDiagnostics.cs delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/Attestation/WindowsDllLoader.cs delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/IsExternalInit.cs delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/ManagedIdentityPopExtensions.cs delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/Microsoft.Identity.Client.MtlsPop.csproj delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/PopKeyAttestor.cs delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Shipped.txt delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Unshipped.txt delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Shipped.txt delete mode 100644 src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt diff --git a/prototype/MsiV2DemoApp/MsiV2DemoApp.csproj b/prototype/MsiV2DemoApp/MsiV2DemoApp.csproj index e8ae98ddef..db688e9072 100644 --- a/prototype/MsiV2DemoApp/MsiV2DemoApp.csproj +++ b/prototype/MsiV2DemoApp/MsiV2DemoApp.csproj @@ -9,7 +9,6 @@ - diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClient.cs b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClient.cs deleted file mode 100644 index c0c8faf588..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClient.cs +++ /dev/null @@ -1,111 +0,0 @@ -// 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. - /// - internal sealed class AttestationClient : IDisposable - { - private bool _initialized; - - /// - /// AttestationClient constructor. Relies on the default OS loader to locate the native DLL. - /// - /// - public AttestationClient() - { - string dllError = NativeDiagnostics.ProbeNativeDll(); - // intentionally not throwing on dllError - - // Load & initialize (logger is required by native lib) - var info = new AttestationClientLib.AttestationLogInfo - { - Log = AttestationLogger.ConsoleLogger, - 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 deleted file mode 100644 index df84387024..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationClientLib.cs +++ /dev/null @@ -1,45 +0,0 @@ -// 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 deleted file mode 100644 index 0c47ceed76..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationErrors.cs +++ /dev/null @@ -1,27 +0,0 @@ -// 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 deleted file mode 100644 index a59f601bdf..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationLogger.cs +++ /dev/null @@ -1,45 +0,0 @@ -// 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); - } - 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 deleted file mode 100644 index 79b3f647f5..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResult.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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. - /// - /// High-level outcome category. - /// JWT on success; null otherwise (caller may pass null). - /// Raw native return code (0 on success). - /// Optional descriptive text for non-success cases. - /// - /// This is a positional record. The compiler synthesizes init-only auto-properties: - /// public AttestationStatus Status { get; init; } - /// public string Jwt { get; init; } - /// public int NativeErrorCode { get; init; } - /// public string ErrorMessage { get; init; } - /// Because they are init-only, values are fixed after construction; to "modify" use a 'with' - /// expression, e.g.: var updated = result with { Jwt = newJwt }; - /// The netstandard2.0 target relies on the IsExternalInit shim (see IsExternalInit.cs) to enable 'init'. - /// - internal 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 deleted file mode 100644 index 4f02375292..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationResultErrorCode.cs +++ /dev/null @@ -1,125 +0,0 @@ -// 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 deleted file mode 100644 index ff20df8aa9..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/AttestationStatus.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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 . - /// - internal 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 deleted file mode 100644 index 9482039c8e..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/NativeDiagnostics.cs +++ /dev/null @@ -1,46 +0,0 @@ -// 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/WindowsDllLoader.cs b/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/WindowsDllLoader.cs deleted file mode 100644 index aaee9eadb2..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/Attestation/WindowsDllLoader.cs +++ /dev/null @@ -1,64 +0,0 @@ -// 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 MsalClientException( - "attestationmodule_load_failure", - $"Key Attestation Module load failed " + - $"(error={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 deleted file mode 100644 index dfb6a17acc..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/IsExternalInit.cs +++ /dev/null @@ -1,11 +0,0 @@ -// 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 deleted file mode 100644 index d79d4b2ab4..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/ManagedIdentityPopExtensions.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// This file intentionally left empty. -// The WithMtlsProofOfPossession extension method has been moved to the main MSAL package: -// Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession() -// -// For attestation support, reference the Msal.KeyAttestation package and call: -// .WithAttestationSupport() 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 deleted file mode 100644 index 281eaacf00..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/Microsoft.Identity.Client.MtlsPop.csproj +++ /dev/null @@ -1,50 +0,0 @@ - - - - - netstandard2.0 - net8.0 - AnyCPU - - - $(TargetFrameworkNetStandard);$(TargetFrameworkNet) - - - Debug;Release - - - - - $(MsalInternalVersion) - - $(MicrosoftIdentityClientVersion)-preview - - 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 - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PopKeyAttestor.cs b/src/client/Microsoft.Identity.Client.MtlsPop/PopKeyAttestor.cs deleted file mode 100644 index f855041bce..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/PopKeyAttestor.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Runtime.InteropServices; -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. - /// - internal 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, - SafeHandle keyHandle, - string clientId, - CancellationToken cancellationToken = default) - { - if (keyHandle is null) - throw new ArgumentNullException(nameof(keyHandle)); - - if (string.IsNullOrWhiteSpace(endpoint)) - throw new ArgumentNullException(nameof(endpoint)); - - if (keyHandle.IsInvalid) - throw new ArgumentException("keyHandle is invalid", nameof(keyHandle)); - - var safeNCryptKeyHandle = keyHandle as SafeNCryptKeyHandle - ?? throw new ArgumentException("keyHandle must be a SafeNCryptKeyHandle. Only Windows CNG keys are supported.", nameof(keyHandle)); - - cancellationToken.ThrowIfCancellationRequested(); - - return Task.Run(() => - { - try - { - using var client = new AttestationClient(); - return client.Attest(endpoint, safeNCryptKeyHandle, 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/net8.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 5f282702bb..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 5f282702bb..0000000000 --- a/src/client/Microsoft.Identity.Client.MtlsPop/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs b/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs index e10a685979..36547b51ab 100644 --- a/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs +++ b/src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs @@ -7,7 +7,6 @@ [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.Client.KeyAttestation" + KeyTokens.MSAL)] [assembly: InternalsVisibleTo("Microsoft.Identity.Test.Unit" + KeyTokens.MSAL)] 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 6ed2dad75e..377a72a0f7 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,9 +7,8 @@ - - + diff --git a/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj b/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj index a6f295c6d2..eb6a842e0f 100644 --- a/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj +++ b/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj @@ -16,7 +16,6 @@ - {3433eb33-114a-4db7-bc57-14f17f55da3c} Microsoft.Identity.Client diff --git a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/ManagedIdentityAppVM.csproj b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/ManagedIdentityAppVM.csproj index ea3a7b6aec..150d7f869c 100644 --- a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/ManagedIdentityAppVM.csproj +++ b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/ManagedIdentityAppVM.csproj @@ -8,7 +8,6 @@ - From 7acfbedd8e6b552a56c216d27665275162594888 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 10 Dec 2025 18:16:44 -0500 Subject: [PATCH 4/6] Removed references to mtlspop package --- LibsAndSamples.sln | 47 +------------------ .../KeyGuardAttestationTests.cs | 3 +- .../ManagedIdentityTests/ImdsV2Tests.cs | 1 - .../ManagedIdentityAppVM/Program.cs | 1 - 4 files changed, 2 insertions(+), 50 deletions(-) diff --git a/LibsAndSamples.sln b/LibsAndSamples.sln index b2d234b638..aa205da089 100644 --- a/LibsAndSamples.sln +++ b/LibsAndSamples.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.0.11217.181 d18.0 +VisualStudioVersion = 18.0.11217.181 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9B0B5396-4D95-4C15-82ED-DC22B5A3123F}" ProjectSection(SolutionItems) = preProject @@ -192,8 +192,6 @@ 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", "{3E1C29E5-6E67-D9B2-28DF-649A609937A2}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinUI3PackagedSampleApp", "tests\devapps\WinUI3PackagedSampleApp\WinUI3PackagedSampleApp.csproj", "{CE282240-0806-EB91-87E4-D791DC86DEE8}" EndProject Global @@ -1948,48 +1946,6 @@ 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 - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|Any CPU.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|Any CPU.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|ARM.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|ARM.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|ARM64.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|ARM64.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|iPhone.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|iPhone.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|iPhoneSimulator.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|x64.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|x64.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|x86.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug + MobileApps|x86.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|ARM.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|ARM.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|ARM64.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|ARM64.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|iPhone.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|x64.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|x64.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|x86.ActiveCfg = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Debug|x86.Build.0 = Debug|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|Any CPU.Build.0 = Release|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|ARM.ActiveCfg = Release|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|ARM.Build.0 = Release|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|ARM64.ActiveCfg = Release|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|ARM64.Build.0 = Release|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|iPhone.ActiveCfg = Release|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|iPhone.Build.0 = Release|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|x64.ActiveCfg = Release|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|x64.Build.0 = Release|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|x86.ActiveCfg = Release|Any CPU - {3E1C29E5-6E67-D9B2-28DF-649A609937A2}.Release|x86.Build.0 = Release|Any CPU {CE282240-0806-EB91-87E4-D791DC86DEE8}.Debug + MobileApps|Any CPU.ActiveCfg = Debug|x64 {CE282240-0806-EB91-87E4-D791DC86DEE8}.Debug + MobileApps|Any CPU.Build.0 = Debug|x64 {CE282240-0806-EB91-87E4-D791DC86DEE8}.Debug + MobileApps|ARM.ActiveCfg = Debug|x64 @@ -2089,7 +2045,6 @@ 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} - {3E1C29E5-6E67-D9B2-28DF-649A609937A2} = {1A37FD75-94E9-4D6F-953A-0DABBD7B49E9} {CE282240-0806-EB91-87E4-D791DC86DEE8} = {34BE693E-3496-45A4-B1D2-D3A0E068EEDB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs b/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs index d6687efaa6..cbbae36056 100644 --- a/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs +++ b/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs @@ -28,8 +28,7 @@ If any prerequisite is missing (e.g., VBS off, endpoint unset, native DLL absent the test exits early with Assert.Inconclusive instead of failing the overall build. */ -using Microsoft.Identity.Client.MtlsPop; -using Microsoft.Identity.Client.MtlsPop.Attestation; +using Microsoft.Identity.Client.KeyAttestation; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Security.Cryptography; diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs index f817e575f3..a3f6cc5b42 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs @@ -15,7 +15,6 @@ using Microsoft.Identity.Client.ManagedIdentity; using Microsoft.Identity.Client.ManagedIdentity.KeyProviders; using Microsoft.Identity.Client.ManagedIdentity.V2; -using Microsoft.Identity.Client.MtlsPop; using Microsoft.Identity.Client.PlatformsCommon.Interfaces; using Microsoft.Identity.Client.PlatformsCommon.Shared; using Microsoft.Identity.Test.Common.Core.Helpers; diff --git a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs index 68eade0c26..48175a3e70 100644 --- a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs +++ b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs @@ -9,7 +9,6 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; using Microsoft.IdentityModel.Abstractions; -using Microsoft.Identity.Client.MtlsPop; internal class Program { From f9324df05be1aa19691668dfcbb0ffd5584a1422 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 10 Dec 2025 18:22:00 -0500 Subject: [PATCH 5/6] changed visiblity of attestation --- .../AttestationClient.cs | 2 +- .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 4 ++++ .../PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClient.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClient.cs index 79b4142b4e..b71df2723d 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClient.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClient.cs @@ -11,7 +11,7 @@ namespace Microsoft.Identity.Client.KeyAttestation /// Managed façade for AttestationClientLib.dll. Holds initialization state, /// does ref-count hygiene on , and returns a JWT. /// - internal sealed class AttestationClient : IDisposable + public sealed class AttestationClient : IDisposable { private bool _initialized; diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt index d89d18cd59..c24fbe5105 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1,3 +1,7 @@ +Microsoft.Identity.Client.KeyAttestation.AttestationClient +Microsoft.Identity.Client.KeyAttestation.AttestationClient.AttestationClient() -> void +Microsoft.Identity.Client.KeyAttestation.AttestationClient.Attest(string! endpoint, Microsoft.Win32.SafeHandles.SafeNCryptKeyHandle! keyHandle, string! clientId) -> Microsoft.Identity.Client.KeyAttestation.AttestationResult! +Microsoft.Identity.Client.KeyAttestation.AttestationClient.Dispose() -> void Microsoft.Identity.Client.KeyAttestation.AttestationResult Microsoft.Identity.Client.KeyAttestation.AttestationResult.AttestationResult(Microsoft.Identity.Client.KeyAttestation.AttestationStatus Status, string! Jwt, int NativeErrorCode, string! ErrorMessage) -> void Microsoft.Identity.Client.KeyAttestation.AttestationResult.ErrorMessage.get -> string! diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index d89d18cd59..c24fbe5105 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,3 +1,7 @@ +Microsoft.Identity.Client.KeyAttestation.AttestationClient +Microsoft.Identity.Client.KeyAttestation.AttestationClient.AttestationClient() -> void +Microsoft.Identity.Client.KeyAttestation.AttestationClient.Attest(string! endpoint, Microsoft.Win32.SafeHandles.SafeNCryptKeyHandle! keyHandle, string! clientId) -> Microsoft.Identity.Client.KeyAttestation.AttestationResult! +Microsoft.Identity.Client.KeyAttestation.AttestationClient.Dispose() -> void Microsoft.Identity.Client.KeyAttestation.AttestationResult Microsoft.Identity.Client.KeyAttestation.AttestationResult.AttestationResult(Microsoft.Identity.Client.KeyAttestation.AttestationStatus Status, string! Jwt, int NativeErrorCode, string! ErrorMessage) -> void Microsoft.Identity.Client.KeyAttestation.AttestationResult.ErrorMessage.get -> string! From fac72990a19c0d3d5370b6c29561865ae41de274 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 10 Dec 2025 18:34:03 -0500 Subject: [PATCH 6/6] Fixed empty attestation bug --- .../ManagedIdentity/V2/CertificateRequestBody.cs | 6 ++++++ .../ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/CertificateRequestBody.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/CertificateRequestBody.cs index 64b27ccc45..33a88671b6 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/CertificateRequestBody.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/CertificateRequestBody.cs @@ -3,6 +3,7 @@ #if SUPPORTS_SYSTEM_TEXT_JSON using JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute; + using JsonIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute; #else using Microsoft.Identity.Json; #endif @@ -14,7 +15,12 @@ internal class CertificateRequestBody [JsonProperty("csr")] public string Csr { get; set; } +#if SUPPORTS_SYSTEM_TEXT_JSON [JsonProperty("attestation_token")] + [JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] +#else + [JsonProperty("attestation_token", NullValueHandling = NullValueHandling.Ignore)] +#endif public string AttestationToken { get; set; } public static bool IsNullOrEmpty(CertificateRequestBody certificateRequestBody) diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs index 9431163de9..1fb18efaee 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs @@ -381,7 +381,7 @@ private async Task GetAttestationJwtAsync( if (provider == null) { _requestContext.Logger.Info("[ImdsV2] No attestation provider configured. Proceeding with non-attested flow."); - return string.Empty; // Empty attestation token indicates non-attested flow + return null; // Null attestation token indicates non-attested flow (field will be omitted from JSON) } // KeyGuard requires RSACng on Windows