From cf72c384c25fc7bcc2743800f0ba33bf20bad4a5 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Mon, 8 Dec 2025 13:06:58 -0500 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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 From 8f0dd8fd1e5c792a3005d158b92d9f20fade83e3 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Mon, 15 Dec 2025 13:16:28 -0500 Subject: [PATCH 07/13] modified solution file --- LibsAndSamples.sln | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/LibsAndSamples.sln b/LibsAndSamples.sln index aa205da089..9d6551c7bc 100644 --- a/LibsAndSamples.sln +++ b/LibsAndSamples.sln @@ -194,6 +194,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MacConsoleAppWithBroker", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinUI3PackagedSampleApp", "tests\devapps\WinUI3PackagedSampleApp\WinUI3PackagedSampleApp.csproj", "{CE282240-0806-EB91-87E4-D791DC86DEE8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Client.KeyAttestation", "src\client\Microsoft.Identity.Client.KeyAttestation\Microsoft.Identity.Client.KeyAttestation.csproj", "{425EAEBE-595F-0037-6FDC-2D08D5184705}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug + MobileApps|Any CPU = Debug + MobileApps|Any CPU @@ -1988,6 +1990,48 @@ Global {CE282240-0806-EB91-87E4-D791DC86DEE8}.Release|x64.Build.0 = Release|x64 {CE282240-0806-EB91-87E4-D791DC86DEE8}.Release|x86.ActiveCfg = Release|x86 {CE282240-0806-EB91-87E4-D791DC86DEE8}.Release|x86.Build.0 = Release|x86 + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|Any CPU.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|Any CPU.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|ARM.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|ARM.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|ARM64.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|ARM64.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|iPhone.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|iPhone.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|iPhoneSimulator.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|x64.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|x64.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|x86.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug + MobileApps|x86.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|Any CPU.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|ARM.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|ARM.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|ARM64.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|iPhone.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|x64.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|x64.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|x86.ActiveCfg = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Debug|x86.Build.0 = Debug|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|Any CPU.ActiveCfg = Release|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|Any CPU.Build.0 = Release|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|ARM.ActiveCfg = Release|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|ARM.Build.0 = Release|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|ARM64.ActiveCfg = Release|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|ARM64.Build.0 = Release|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|iPhone.ActiveCfg = Release|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|iPhone.Build.0 = Release|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|x64.ActiveCfg = Release|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|x64.Build.0 = Release|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|x86.ActiveCfg = Release|Any CPU + {425EAEBE-595F-0037-6FDC-2D08D5184705}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2046,6 +2090,7 @@ Global {AEF6BB00-931F-4638-955D-24D735625C34} = {34BE693E-3496-45A4-B1D2-D3A0E068EEDB} {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0} = {34BE693E-3496-45A4-B1D2-D3A0E068EEDB} {CE282240-0806-EB91-87E4-D791DC86DEE8} = {34BE693E-3496-45A4-B1D2-D3A0E068EEDB} + {425EAEBE-595F-0037-6FDC-2D08D5184705} = {1A37FD75-94E9-4D6F-953A-0DABBD7B49E9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {020399A9-DC27-4B82-9CAA-EF488665AC27} From 83aa4c40e42839dc710b02d5cf8b8fd56009eebf Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Tue, 16 Dec 2025 15:11:12 -0500 Subject: [PATCH 08/13] Implemented some GitHub feedback --- .../{ => Attestation}/AttestationClient.cs | 4 ++-- .../{ => Attestation}/AttestationClientLib.cs | 3 +-- .../{ => Attestation}/AttestationErrors.cs | 6 +---- .../{ => Attestation}/AttestationLogger.cs | 2 +- .../{ => Attestation}/AttestationResult.cs | 4 ++-- .../AttestationResultErrorCode.cs | 2 +- .../{ => Attestation}/AttestationStatus.cs | 4 ++-- .../{ => Attestation}/NativeDiagnostics.cs | 2 +- .../{ => Attestation}/WindowsDllLoader.cs | 3 +-- .../ManagedIdentityAttestationExtensions.cs | 4 +--- ...soft.Identity.Client.KeyAttestation.csproj | 12 +++++----- .../PopKeyAttestor.cs | 3 ++- .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 24 +------------------ .../netstandard2.0/PublicAPI.Unshipped.txt | 24 +------------------ .../KeyGuardAttestationTests.cs | 3 ++- 15 files changed, 25 insertions(+), 75 deletions(-) rename src/client/Microsoft.Identity.Client.KeyAttestation/{ => Attestation}/AttestationClient.cs (97%) rename src/client/Microsoft.Identity.Client.KeyAttestation/{ => Attestation}/AttestationClientLib.cs (95%) rename src/client/Microsoft.Identity.Client.KeyAttestation/{ => Attestation}/AttestationErrors.cs (89%) rename src/client/Microsoft.Identity.Client.KeyAttestation/{ => Attestation}/AttestationLogger.cs (95%) rename src/client/Microsoft.Identity.Client.KeyAttestation/{ => Attestation}/AttestationResult.cs (92%) rename src/client/Microsoft.Identity.Client.KeyAttestation/{ => Attestation}/AttestationResultErrorCode.cs (98%) rename src/client/Microsoft.Identity.Client.KeyAttestation/{ => Attestation}/AttestationStatus.cs (90%) rename src/client/Microsoft.Identity.Client.KeyAttestation/{ => Attestation}/NativeDiagnostics.cs (95%) rename src/client/Microsoft.Identity.Client.KeyAttestation/{ => Attestation}/WindowsDllLoader.cs (96%) diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClient.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClient.cs similarity index 97% rename from src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClient.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClient.cs index b71df2723d..9d75c6fce8 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClient.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClient.cs @@ -5,13 +5,13 @@ using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; -namespace Microsoft.Identity.Client.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation.Attestation { /// /// Managed façade for AttestationClientLib.dll. Holds initialization state, /// does ref-count hygiene on , and returns a JWT. /// - public sealed class AttestationClient : IDisposable + internal sealed class AttestationClient : IDisposable { private bool _initialized; diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClientLib.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClientLib.cs similarity index 95% rename from src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClientLib.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClientLib.cs index 247e7af182..07f0daa659 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationClientLib.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClientLib.cs @@ -3,10 +3,9 @@ using Microsoft.Win32.SafeHandles; using System; -using System.IO; using System.Runtime.InteropServices; -namespace Microsoft.Identity.Client.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation.Attestation { internal static class AttestationClientLib { diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationErrors.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationErrors.cs similarity index 89% rename from src/client/Microsoft.Identity.Client.KeyAttestation/AttestationErrors.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationErrors.cs index 8074af02ce..2dee80fbd3 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationErrors.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationErrors.cs @@ -1,11 +1,7 @@ // 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.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation.Attestation { internal static class AttestationErrors { diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationLogger.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationLogger.cs similarity index 95% rename from src/client/Microsoft.Identity.Client.KeyAttestation/AttestationLogger.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationLogger.cs index 22e37e9d93..c4a20a3e8b 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationLogger.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationLogger.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; -namespace Microsoft.Identity.Client.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation.Attestation { internal static class AttestationLogger { diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationResult.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationResult.cs similarity index 92% rename from src/client/Microsoft.Identity.Client.KeyAttestation/AttestationResult.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationResult.cs index e78408d496..930ebde5e6 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationResult.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationResult.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Identity.Client.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation.Attestation { /// /// AttestationResult is the result of an attestation operation. @@ -20,7 +20,7 @@ namespace Microsoft.Identity.Client.KeyAttestation /// 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( + internal sealed record AttestationResult( AttestationStatus Status, string Jwt, int NativeErrorCode, diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationResultErrorCode.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationResultErrorCode.cs similarity index 98% rename from src/client/Microsoft.Identity.Client.KeyAttestation/AttestationResultErrorCode.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationResultErrorCode.cs index 0b15eab683..d23ce55103 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationResultErrorCode.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationResultErrorCode.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Text; -namespace Microsoft.Identity.Client.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation.Attestation { /// /// Error codes returned by AttestationClientLib.dll. diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationStatus.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationStatus.cs similarity index 90% rename from src/client/Microsoft.Identity.Client.KeyAttestation/AttestationStatus.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationStatus.cs index b49abadb00..c91ed09822 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/AttestationStatus.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationStatus.cs @@ -5,12 +5,12 @@ using System.Collections.Generic; using System.Text; -namespace Microsoft.Identity.Client.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation.Attestation { /// /// High-level outcome categories returned by . /// - public enum AttestationStatus + internal enum AttestationStatus { /// Everything succeeded; is populated. Success = 0, diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/NativeDiagnostics.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/NativeDiagnostics.cs similarity index 95% rename from src/client/Microsoft.Identity.Client.KeyAttestation/NativeDiagnostics.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/NativeDiagnostics.cs index f47b85bae3..456aca8bfa 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/NativeDiagnostics.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/NativeDiagnostics.cs @@ -5,7 +5,7 @@ using System.ComponentModel; using System.IO; -namespace Microsoft.Identity.Client.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation.Attestation { internal static class NativeDiagnostics { diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/WindowsDllLoader.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/WindowsDllLoader.cs similarity index 96% rename from src/client/Microsoft.Identity.Client.KeyAttestation/WindowsDllLoader.cs rename to src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/WindowsDllLoader.cs index 3084769f06..54b52586a1 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/WindowsDllLoader.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/WindowsDllLoader.cs @@ -4,9 +4,8 @@ using System; using System.ComponentModel; using System.Runtime.InteropServices; -using Microsoft.Identity.Client; -namespace Microsoft.Identity.Client.KeyAttestation +namespace Microsoft.Identity.Client.KeyAttestation.Attestation { /// /// Windows‑only helper that loads a native DLL from an absolute path. diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs index 958bad28cb..db07879f21 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs @@ -2,9 +2,7 @@ // Licensed under the MIT License. using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Identity.Client; +using Microsoft.Identity.Client.KeyAttestation.Attestation; using Microsoft.Identity.Client.ManagedIdentity; namespace Microsoft.Identity.Client.KeyAttestation diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/Microsoft.Identity.Client.KeyAttestation.csproj b/src/client/Microsoft.Identity.Client.KeyAttestation/Microsoft.Identity.Client.KeyAttestation.csproj index 68a9cd6c12..c29569dfb7 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/Microsoft.Identity.Client.KeyAttestation.csproj +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Microsoft.Identity.Client.KeyAttestation.csproj @@ -27,6 +27,11 @@ Microsoft Authentication Library + + + + + @@ -36,14 +41,9 @@ - - - $(NoWarn);RS0016;RS0017;RS0036;RS0041 - - - + \ No newline at end of file diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs index 2ddcefda3a..ac80bdb7ec 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/PopKeyAttestor.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.Identity.Client.KeyAttestation.Attestation; using Microsoft.Win32.SafeHandles; namespace Microsoft.Identity.Client.KeyAttestation @@ -13,7 +14,7 @@ namespace Microsoft.Identity.Client.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 + internal static class PopKeyAttestor { /// /// Asynchronously attests a KeyGuard/CNG key with the remote attestation service and returns a JWT. 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 c24fbe5105..1a5cf5f13c 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,24 +1,2 @@ -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! -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! +static Microsoft.Identity.Client.KeyAttestation.ManagedIdentityAttestationExtensions.WithAttestationSupport(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder 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 c24fbe5105..1a5cf5f13c 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,24 +1,2 @@ -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! -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! +static Microsoft.Identity.Client.KeyAttestation.ManagedIdentityAttestationExtensions.WithAttestationSupport(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder diff --git a/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs b/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs index cbbae36056..598f8d698d 100644 --- a/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs +++ b/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs @@ -29,6 +29,7 @@ the test exits early with Assert.Inconclusive instead of failing the overall bui */ using Microsoft.Identity.Client.KeyAttestation; +using Microsoft.Identity.Client.KeyAttestation.Attestation; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Security.Cryptography; @@ -131,7 +132,7 @@ public void Attest_KeyGuardKey_OnAzureArc_Succeeds() Assert.Inconclusive("Key was created but not KeyGuard-protected. Is KeyGuard/VBS enabled on this machine?"); } - // Use the new public AttestationClient from the MtlsPop package. :contentReference[oaicite:2]{index=2} + // Use the new public AttestationClient from the KeyAttestation package. :contentReference[oaicite:2]{index=2} using var client = new AttestationClient(); var result = client.Attest(endpoint, key.Handle, clientId); From 1e24c34ee0530700c106fd945392f6cef74b1187 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Tue, 16 Dec 2025 16:31:39 -0500 Subject: [PATCH 09/13] Removed WithAttestationProviderForTests and replaced it with .WithAttestationSupport() --- .../KeyGuardAttestationProvider.cs | 78 ++++++++++ .../ManagedIdentityAttestationExtensions.cs | 38 +---- .../ModuleInitializer.cs | 32 ++++ ...TokenForManagedIdentityParameterBuilder.cs | 18 +-- .../AcquireTokenCommonParameters.cs | 2 +- ...cquireTokenForManagedIdentityParameters.cs | 2 +- .../Internal/RequestContext.cs | 2 +- .../Requests/ManagedIdentityAuthRequest.cs | 6 +- .../AttestationProviderRegistry.cs | 36 +++++ .../ManagedIdentity/IAttestationProvider.cs | 51 +++++++ .../V2/ImdsV2ManagedIdentitySource.cs | 72 +++++---- .../ManagedIdentityTests/ImdsV2Tests.cs | 140 +++++++++--------- .../TestAttestationProviders.cs | 120 +++++++++++++++ .../Microsoft.Identity.Test.Unit.csproj | 1 + 14 files changed, 441 insertions(+), 157 deletions(-) create mode 100644 src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/KeyGuardAttestationProvider.cs create mode 100644 src/client/Microsoft.Identity.Client.KeyAttestation/ModuleInitializer.cs create mode 100644 src/client/Microsoft.Identity.Client/ManagedIdentity/AttestationProviderRegistry.cs create mode 100644 src/client/Microsoft.Identity.Client/ManagedIdentity/IAttestationProvider.cs create mode 100644 tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/TestAttestationProviders.cs diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/KeyGuardAttestationProvider.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/KeyGuardAttestationProvider.cs new file mode 100644 index 0000000000..b4c55f9e38 --- /dev/null +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/KeyGuardAttestationProvider.cs @@ -0,0 +1,78 @@ +// 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.ManagedIdentity; + +namespace Microsoft.Identity.Client.KeyAttestation.Attestation +{ + /// + /// Implementation of IAttestationProvider for KeyGuard attestation. + /// This provider is automatically registered when the KeyAttestation package is loaded. + /// + internal class KeyGuardAttestationProvider : IAttestationProvider + { + public async Task AttestKeyGuardAsync( + string attestationEndpoint, + SafeHandle keyHandle, + string clientId, + CancellationToken cancellationToken) + { + try + { + // Call the existing PopKeyAttestor implementation + var result = await PopKeyAttestor.AttestKeyGuardAsync( + attestationEndpoint, + keyHandle, + clientId, + cancellationToken).ConfigureAwait(false); + + // Map the result to the MSAL interface types + return new ManagedIdentity.AttestationResult + { + Status = result.Status == AttestationStatus.Success + ? ManagedIdentity.AttestationStatus.Success + : ManagedIdentity.AttestationStatus.Failed, + Jwt = result.Jwt, + ErrorMessage = result.ErrorMessage, + NativeErrorCode = result.NativeErrorCode + }; + } + catch (Exception ex) + { + return new ManagedIdentity.AttestationResult + { + Status = ManagedIdentity.AttestationStatus.Failed, + ErrorMessage = ex.Message, + NativeErrorCode = -1 + }; + } + } + } + + /// + /// Static initializer that registers the KeyGuard attestation provider + /// when the KeyAttestation assembly is loaded. + /// + internal static class AttestationProviderInitializer + { + static AttestationProviderInitializer() + { + // Register the provider when this type is first accessed + AttestationProviderRegistry.RegisterProvider(new KeyGuardAttestationProvider()); + } + + /// + /// Method to force static constructor execution. + /// Called from module initializer. + /// + internal static void Initialize() + { + // This method body is intentionally empty. + // Its purpose is to trigger the static constructor above. + } + } +} diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs index db07879f21..513605ddd0 100644 --- a/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs @@ -26,40 +26,12 @@ public static AcquireTokenForManagedIdentityParameterBuilder WithAttestationSupp 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 }; - } + // Ensure the provider is registered (triggers static constructor on older frameworks) + Attestation.AttestationProviderInitializer.Initialize(); - throw new MsalClientException( - "attestation_failure", - $"Key Attestation failed " + - $"(status={attestationResult?.Status}, " + - $"code={attestationResult?.NativeErrorCode}). {attestationResult?.ErrorMessage}"); - }); + // Set the flag to enable attestation + builder.CommonParameters.IsAttestationRequested = true; + return builder; } } } diff --git a/src/client/Microsoft.Identity.Client.KeyAttestation/ModuleInitializer.cs b/src/client/Microsoft.Identity.Client.KeyAttestation/ModuleInitializer.cs new file mode 100644 index 0000000000..55167a9339 --- /dev/null +++ b/src/client/Microsoft.Identity.Client.KeyAttestation/ModuleInitializer.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#if NET5_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif +using Microsoft.Identity.Client.KeyAttestation.Attestation; + +namespace Microsoft.Identity.Client.KeyAttestation +{ +#if NET5_0_OR_GREATER + /// + /// Module initializer that runs when the KeyAttestation assembly is loaded. + /// Automatically registers the KeyGuard attestation provider with MSAL. + /// + internal static class ModuleInitializer + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2255:The 'ModuleInitializer' attribute should not be used in libraries", Justification = "Required for auto-registration of attestation provider")] + [ModuleInitializer] + internal static void Initialize() + { + // Force the static constructor of AttestationProviderInitializer to run, + // which registers the KeyGuard attestation provider + AttestationProviderInitializer.Initialize(); + } + } +#else + // For .NET Standard 2.0 and .NET Framework, we rely on the static constructor + // being triggered when the extension method is first called. + // This ensures the provider is registered before it's needed. +#endif +} diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs index dd9600b1aa..98d6457fed 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs @@ -96,28 +96,12 @@ internal override ApiEvent.ApiIds CalculateApiEventId() return ApiEvent.ApiIds.AcquireTokenForUserAssignedManagedIdentity; } - /// - /// TEST HOOK ONLY: Allows unit tests to inject a fake attestation-token provider - /// so we don't hit the real attestation service. Not part of the public API. - /// - internal AcquireTokenForManagedIdentityParameterBuilder WithAttestationProviderForTests( - Func> provider) - { - if (provider is null) - { - throw new ArgumentNullException(nameof(provider)); - } - - CommonParameters.AttestationTokenProvider = provider; - return this; - } - private static void ApplyMtlsPopAndAttestation( AcquireTokenCommonParameters acquireTokenCommonParameters, AcquireTokenForManagedIdentityParameters acquireTokenForManagedIdentityParameters) { acquireTokenForManagedIdentityParameters.IsMtlsPopRequested = acquireTokenCommonParameters.IsMtlsPopRequested; - acquireTokenForManagedIdentityParameters.AttestationTokenProvider ??= acquireTokenCommonParameters.AttestationTokenProvider; + acquireTokenForManagedIdentityParameters.IsAttestationRequested = acquireTokenCommonParameters.IsAttestationRequested; } } } diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs index cbb52f4a88..da3c5533d0 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs @@ -40,7 +40,7 @@ internal class AcquireTokenCommonParameters public string FmiPathSuffix { get; internal set; } public string ClientAssertionFmiPath { get; internal set; } public bool IsMtlsPopRequested { get; set; } - internal Func> AttestationTokenProvider { get; set; } + public bool IsAttestationRequested { get; set; } internal async Task InitMtlsPopParametersAsync(IServiceBundle serviceBundle, CancellationToken ct) { diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs index 32646eaa62..2207cbc9d0 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForManagedIdentityParameters.cs @@ -25,7 +25,7 @@ internal class AcquireTokenForManagedIdentityParameters : IAcquireTokenParameter public bool IsMtlsPopRequested { get; set; } - internal Func> AttestationTokenProvider { get; set; } + public bool IsAttestationRequested { get; set; } internal X509Certificate2 MtlsCertificate { get; set; } diff --git a/src/client/Microsoft.Identity.Client/Internal/RequestContext.cs b/src/client/Microsoft.Identity.Client/Internal/RequestContext.cs index 2fb786a6a6..02b88de971 100644 --- a/src/client/Microsoft.Identity.Client/Internal/RequestContext.cs +++ b/src/client/Microsoft.Identity.Client/Internal/RequestContext.cs @@ -31,7 +31,7 @@ internal class RequestContext public X509Certificate2 MtlsCertificate { get; } - internal Func> AttestationTokenProvider { get; set; } + public bool IsAttestationRequested { get; set; } public RequestContext(IServiceBundle serviceBundle, Guid correlationId, X509Certificate2 mtlsCertificate, CancellationToken cancellationToken = default) { diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs index bd9df91090..d7cd957883 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs @@ -216,9 +216,9 @@ private async Task SendTokenRequestForManagedIdentityAsync _managedIdentityParameters.IsMtlsPopRequested = AuthenticationRequestParameters.IsMtlsPopRequested; - // Ensure the attestation provider reaches RequestContext for IMDSv2 - AuthenticationRequestParameters.RequestContext.AttestationTokenProvider ??= - _managedIdentityParameters.AttestationTokenProvider; + // Pass the attestation flag to the request context + AuthenticationRequestParameters.RequestContext.IsAttestationRequested = + _managedIdentityParameters.IsAttestationRequested; ManagedIdentityResponse managedIdentityResponse = await _managedIdentityClient diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AttestationProviderRegistry.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AttestationProviderRegistry.cs new file mode 100644 index 0000000000..602c49b123 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AttestationProviderRegistry.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Identity.Client.ManagedIdentity +{ + /// + /// Static registry for attestation providers. + /// The KeyAttestation package registers itself here via InternalsVisibleTo. + /// + internal static class AttestationProviderRegistry + { + private static IAttestationProvider s_provider; + + /// + /// Gets the current attestation provider, if one has been registered. + /// + internal static IAttestationProvider Provider => s_provider; + + /// + /// Registers an attestation provider. Called by the KeyAttestation package. + /// + /// The attestation provider to register. + internal static void RegisterProvider(IAttestationProvider provider) + { + s_provider = provider; + } + + /// + /// Clears the registered provider. Used for testing. + /// + internal static void ClearProvider() + { + s_provider = null; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/IAttestationProvider.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/IAttestationProvider.cs new file mode 100644 index 0000000000..2a5f10b6cd --- /dev/null +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/IAttestationProvider.cs @@ -0,0 +1,51 @@ +// 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; + +namespace Microsoft.Identity.Client.ManagedIdentity +{ + /// + /// Internal interface for attestation providers. + /// The KeyAttestation package implements this to provide attestation functionality. + /// + internal interface IAttestationProvider + { + /// + /// Attests a KeyGuard key and returns an attestation JWT. + /// + /// The attestation endpoint URL. + /// The KeyGuard key handle (must be SafeNCryptKeyHandle). + /// The client ID to include in the attestation. + /// Cancellation token. + /// An attestation result containing the JWT or error information. + Task AttestKeyGuardAsync( + string attestationEndpoint, + SafeHandle keyHandle, + string clientId, + CancellationToken cancellationToken); + } + + /// + /// Result of an attestation operation. + /// + internal class AttestationResult + { + public AttestationStatus Status { get; set; } + public string Jwt { get; set; } + public string ErrorMessage { get; set; } + public int NativeErrorCode { get; set; } + } + + /// + /// Status codes for attestation operations. + /// + internal enum AttestationStatus + { + Success = 0, + Failed = 1 + } +} diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs index 1fb18efaee..ef1c355607 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs @@ -26,6 +26,7 @@ internal class ImdsV2ManagedIdentitySource : AbstractManagedIdentity internal static readonly ICertificateCache s_mtlsCertificateCache = new InMemoryCertificateCache(); private readonly IMtlsCertificateCache _mtlsCache; + private bool _isAttestationRequested; // used in unit tests public const string ApiVersionQueryParam = "cred-api-version"; @@ -165,6 +166,15 @@ internal ImdsV2ManagedIdentitySource( _mtlsCache = mtlsCache ?? throw new ArgumentNullException(nameof(mtlsCache)); } + public override async Task AuthenticateAsync( + ApiConfig.Parameters.AcquireTokenForManagedIdentityParameters parameters, + CancellationToken cancellationToken) + { + // Capture the attestation flag before calling base + _isAttestationRequested = parameters.IsAttestationRequested; + return await base.AuthenticateAsync(parameters, cancellationToken).ConfigureAwait(false); + } + private async Task ExecuteCertificateRequestAsync( string clientId, string attestationEndpoint, @@ -371,49 +381,51 @@ private async Task GetAttestationJwtAsync( ManagedIdentityKeyInfo keyInfo, CancellationToken cancellationToken) { - // 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) + // Check if attestation was requested via WithAttestationSupport() + if (!_isAttestationRequested) { - _requestContext.Logger.Info("[ImdsV2] No attestation provider configured. Proceeding with non-attested flow."); - return null; // Null attestation token indicates non-attested flow (field will be omitted from JSON) + _requestContext.Logger.Info("[ImdsV2] Attestation not requested. Proceeding with non-attested flow."); + return null; // Null attestation token indicates non-attested flow + } + + // Check if an attestation provider has been registered + var attestationProvider = AttestationProviderRegistry.Provider; + if (attestationProvider == null) + { + throw new MsalClientException( + "attestation_not_configured", + "[ImdsV2] Attestation was requested but no attestation provider is registered. " + + "Ensure you reference the Microsoft.Identity.Client.KeyAttestation package."); } // KeyGuard requires RSACng on Windows - if (keyInfo.Type == ManagedIdentityKeyType.KeyGuard && - keyInfo.Key is not System.Security.Cryptography.RSACng rsaCng) + if (keyInfo.Key is not System.Security.Cryptography.RSACng rsaCng) { throw new MsalClientException( "keyguard_requires_cng", "[ImdsV2] KeyGuard attestation currently supports only RSA CNG keys on Windows."); } - // Attestation token input - var input = new AttestationTokenInput - { - ClientId = clientId, - AttestationEndpoint = attestationEndpoint, - KeyHandle = (keyInfo.Key as System.Security.Cryptography.RSACng)?.Key.Handle - }; - - // response from provider - var response = await provider(input, cancellationToken).ConfigureAwait(false); - - // Validate response - if (response == null || string.IsNullOrWhiteSpace(response.AttestationToken)) + // Call attestation via the registered provider + AttestationResult attestationResult = await attestationProvider.AttestKeyGuardAsync( + attestationEndpoint.AbsoluteUri, + rsaCng.Key.Handle, + clientId, + cancellationToken).ConfigureAwait(false); + + // Validate and return the attestation JWT + if (attestationResult != null && + attestationResult.Status == AttestationStatus.Success && + !string.IsNullOrWhiteSpace(attestationResult.Jwt)) { - throw new MsalClientException( - "attestation_failed", - "[ImdsV2] Attestation provider failed to return an attestation token."); + return attestationResult.Jwt; } - // Return the JWT - return response.AttestationToken; + throw new MsalClientException( + "attestation_failed", + $"[ImdsV2] Key Attestation failed " + + $"(status={attestationResult?.Status}, " + + $"code={attestationResult?.NativeErrorCode}). {attestationResult?.ErrorMessage}"); } private Task GetOrCreateMtlsBindingAsync( diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs index a3f6cc5b42..1b5e0a43a8 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs @@ -1,17 +1,17 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.IO; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using System.Threading; using System.Threading.Tasks; using System.Xml; using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Internal.Logger; +using Microsoft.Identity.Client.KeyAttestation; using Microsoft.Identity.Client.ManagedIdentity; using Microsoft.Identity.Client.ManagedIdentity.KeyProviders; using Microsoft.Identity.Client.ManagedIdentity.V2; @@ -40,14 +40,6 @@ public class ImdsV2Tests : TestBase enablePiiLogging: false ); - // Fake attestation provider used by mTLS PoP tests so we never hit the real service - private static readonly Func> - s_fakeAttestationProvider = - (input, ct) => Task.FromResult(new AttestationTokenResponse - { - AttestationToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.fake.attestation.sig" - }); - public const string Bearer = "Bearer"; public const string MTLSPoP = "mtls_pop"; @@ -60,6 +52,16 @@ public void ImdsV2Tests_Init() // A broad sweep is simplest and safe for our fake endpoints/certs ImdsV2TestStoreCleaner.RemoveAllTestArtifacts(); } + + // Register fake attestation provider for tests + AttestationProviderRegistry.RegisterProvider(TestAttestationProviders.CreateFakeProvider()); + } + + [TestCleanup] + public void ImdsV2Tests_Cleanup() + { + // Clear provider after each test + AttestationProviderRegistry.ClearProvider(); } private void AddMocksToGetEntraToken( @@ -179,7 +181,7 @@ public async Task mTLSPopTokenHappyPath( var result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(result); @@ -190,7 +192,7 @@ public async Task mTLSPopTokenHappyPath( result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(result); @@ -222,7 +224,7 @@ public async Task mTLSPopTokenIsPerIdentity( var result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(result); @@ -256,7 +258,7 @@ public async Task mTLSPopTokenIsPerIdentity( var result2 = await managedIdentityApp2.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(result2); @@ -267,7 +269,7 @@ public async Task mTLSPopTokenIsPerIdentity( result2 = await managedIdentityApp2.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(result2); @@ -301,7 +303,7 @@ public async Task mTLSPopTokenIsReAcquiredWhenCertificateIsExpired( var result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(result); @@ -316,7 +318,7 @@ public async Task mTLSPopTokenIsReAcquiredWhenCertificateIsExpired( result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(result); @@ -383,7 +385,7 @@ public async Task ApplicationsCannotSwitchBetweenImdsVersionsForPreview( var result = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) //.WithMtlsProofOfPossession() - excluding this will cause fallback to ImdsV1 - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(result); @@ -679,13 +681,13 @@ public async Task MtlsPop_AttestationProviderReturnsNull_ThrowsClientException() // CreateManagedIdentityAsync does a probe; Add one more CSR response for the actual acquire. httpManager.AddMockHandler(MockHelpers.MockCsrResponse()); - var nullProvider = new Func>( - (input, ct) => Task.FromResult(null)); + // Register null provider for this test + AttestationProviderRegistry.RegisterProvider(TestAttestationProviders.CreateNullProvider()); var ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(nullProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false) ).ConfigureAwait(false); @@ -706,13 +708,13 @@ public async Task MtlsPop_AttestationProviderReturnsEmptyToken_ThrowsClientExcep // CreateManagedIdentityAsync does a probe; Add one more CSR response for the actual acquire. httpManager.AddMockHandler(MockHelpers.MockCsrResponse()); - var emptyProvider = new Func>( - (input, ct) => Task.FromResult(new AttestationTokenResponse { AttestationToken = " " })); + // Register empty provider for this test + AttestationProviderRegistry.RegisterProvider(TestAttestationProviders.CreateEmptyProvider()); var ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(emptyProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false) ).ConfigureAwait(false); @@ -763,23 +765,19 @@ public async Task mTLSPop_ForceRefresh_UsesCachedCert_NoIssueCredential_PostsCan // First acquire: full flow (CSR + issuecredential + token) AddMocksToGetEntraToken(httpManager); - int attestationCalls = 0; - Func> countingProvider = - (input, ct) => - { - Interlocked.Increment(ref attestationCalls); - return Task.FromResult(new AttestationTokenResponse { AttestationToken = "header.payload.sig" }); - }; + // Register counting provider for this test + var countingProvider = TestAttestationProviders.CreateCountingProvider(); + AttestationProviderRegistry.RegisterProvider(countingProvider); var result1 = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(countingProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(ImdsV2Tests.MTLSPoP, result1.TokenType); Assert.IsNotNull(result1.BindingCertificate); Assert.AreEqual(TokenSource.IdentityProvider, result1.AuthenticationResultMetadata.TokenSource); - Assert.AreEqual(1, attestationCalls, "Attestation must be called exactly once on first mint."); + Assert.AreEqual(1, countingProvider.CallCount, "Attestation must be called exactly once on first mint."); // Second acquire: FORCE REFRESH to bypass token cache. // Expect: 1x getplatformmetadata + token request. NO /issuecredential. Attestation NOT called again. @@ -793,13 +791,13 @@ public async Task mTLSPop_ForceRefresh_UsesCachedCert_NoIssueCredential_PostsCan var result2 = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithForceRefresh(true) // if your API is parameterless, use .WithForceRefresh() .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(countingProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(ImdsV2Tests.MTLSPoP, result2.TokenType); Assert.IsNotNull(result2.BindingCertificate); Assert.AreEqual(TokenSource.IdentityProvider, result2.AuthenticationResultMetadata.TokenSource); - Assert.AreEqual(1, attestationCalls, "Attestation must NOT be invoked on refresh when cert is cached."); + Assert.AreEqual(1, countingProvider.CallCount, "Attestation must NOT be invoked on refresh when cert is cached."); } } @@ -823,11 +821,11 @@ public async Task mTLSPop_CachedCertIsPerIdentity_OnRefresh_Identity1UsesCache_I var result1 = await mi1.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TokenSource.IdentityProvider, result1.AuthenticationResultMetadata.TokenSource); - // Identity 1 – force refresh (should use cached cert → NO /issuecredential) + // Identity 1 – force refresh (should use cached cert ? NO /issuecredential) MockHelpers.AddMocksToGetEntraTokenUsingCachedCert( httpManager, _identityLoggerAdapter, @@ -841,19 +839,19 @@ public async Task mTLSPop_CachedCertIsPerIdentity_OnRefresh_Identity1UsesCache_I var result1Refresh = await mi1.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithForceRefresh(true) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync() .ConfigureAwait(false); Assert.AreEqual(TokenSource.IdentityProvider, result1Refresh.AuthenticationResultMetadata.TokenSource); - // Identity 2 – new identity (should MINT again → requires /issuecredential) + // Identity 2 – new identity (should MINT again ? requires /issuecredential) var mi2 = await CreateManagedIdentityAsync(httpManager, userAssignedIdentityId, userAssignedId2, addProbeMock: false, addSourceCheck: false, managedIdentityKeyType: ManagedIdentityKeyType.KeyGuard).ConfigureAwait(false); AddMocksToGetEntraToken(httpManager, userAssignedIdentityId, userAssignedId2); var result2 = await mi2.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TokenSource.IdentityProvider, result2.AuthenticationResultMetadata.TokenSource); } @@ -895,7 +893,7 @@ public async Task mTLSPopTokenHappyPath_LongLivedCert_IdentityMapping( var first = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(first); @@ -908,7 +906,7 @@ public async Task mTLSPopTokenHappyPath_LongLivedCert_IdentityMapping( var second = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(second); @@ -959,7 +957,7 @@ public async Task mTLSPop_LongLivedCerts_SamiVsUami_DistinctAndCached( managedIdentityKeyType: ManagedIdentityKeyType.KeyGuard).ConfigureAwait(false); // --- First acquire (MINT): return the identity-specific cert we want --- - // SAMI → use rawCertSami ; UAMI (any alias) → use rawCertUami + // SAMI ? use rawCertSami ; UAMI (any alias) ? use rawCertUami string selectedCert = (userAssignedIdentityId == UserAssignedIdentityId.None) ? rawCertSami : rawCertUami; AddMocksToGetEntraToken( @@ -970,7 +968,7 @@ public async Task mTLSPop_LongLivedCerts_SamiVsUami_DistinctAndCached( var first = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(first); @@ -985,7 +983,7 @@ public async Task mTLSPop_LongLivedCerts_SamiVsUami_DistinctAndCached( // --- Second acquire: cached; cert should be the SAME (cached binding cert) --- var second = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(second); @@ -1032,7 +1030,7 @@ public async Task mTLSPop_LongLivedCerts_SamiAndUami_ThumbprintsDiffer_AndEachCa var s1 = await sami.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(s1.BindingCertificate); @@ -1043,7 +1041,7 @@ public async Task mTLSPop_LongLivedCerts_SamiAndUami_ThumbprintsDiffer_AndEachCa // cached var s2 = await sami.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TokenSource.Cache, s2.AuthenticationResultMetadata.TokenSource); @@ -1067,7 +1065,7 @@ public async Task mTLSPop_LongLivedCerts_SamiAndUami_ThumbprintsDiffer_AndEachCa var u1 = await uami.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(u1.BindingCertificate); @@ -1077,7 +1075,7 @@ public async Task mTLSPop_LongLivedCerts_SamiAndUami_ThumbprintsDiffer_AndEachCa var u2 = await uami.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TokenSource.Cache, u2.AuthenticationResultMetadata.TokenSource); @@ -1090,8 +1088,8 @@ public async Task mTLSPop_LongLivedCerts_SamiAndUami_ThumbprintsDiffer_AndEachCa /// /// Subject mapping test that mirrors prod: CN=canonical client_id, DC=tenant id. - /// - SAMI → CN = Constants.ManagedIdentityDefaultClientId - /// - UAMI (client_id|object_id|resource_id) → CN = TestConstants.ClientId (canonical) + /// - SAMI ? CN = Constants.ManagedIdentityDefaultClientId + /// - UAMI (client_id|object_id|resource_id) ? CN = TestConstants.ClientId (canonical) /// Both assert DC = TestConstants.TenantId and cert cache reuse. /// [DataTestMethod] @@ -1124,7 +1122,7 @@ public async Task mTLSPop_SubjectCnDc_MatchesMetadata_AndCaches( var first = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(MTLSPoP, first.TokenType, $"[{label}]"); @@ -1132,7 +1130,7 @@ public async Task mTLSPop_SubjectCnDc_MatchesMetadata_AndCaches( var second = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TokenSource.Cache, second.AuthenticationResultMetadata.TokenSource, $"[{label}] cache"); @@ -1153,7 +1151,7 @@ public async Task mTLSPoP_Uami_ClientIdThenObjectId_MintsThenCaches_SubjectCNIsC string expectedDc = TestConstants.TenantId; string rawCert = CreateRawCertForCsrKeyWithCnDc(expectedCn, expectedDc, DateTimeOffset.UtcNow.AddYears(20)); - // (1) client_id → MINT (CSR + issuecredential + token) + // (1) client_id ? MINT (CSR + issuecredential + token) var miClientId = await CreateManagedIdentityAsync( httpManager, userAssignedIdentityId: UserAssignedIdentityId.ClientId, @@ -1168,14 +1166,14 @@ public async Task mTLSPoP_Uami_ClientIdThenObjectId_MintsThenCaches_SubjectCNIsC var c1 = await miClientId.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(MTLSPoP, c1.TokenType); Assert.AreEqual(TokenSource.IdentityProvider, c1.AuthenticationResultMetadata.TokenSource); AssertCertSubjectCnDc(c1.BindingCertificate, expectedCn, expectedDc, "[client_id]"); - // (2) object_id → MINT (new alias → its own cache key) + // (2) object_id ? MINT (new alias ? its own cache key) var miObjectId = await CreateManagedIdentityAsync( httpManager, userAssignedIdentityId: UserAssignedIdentityId.ObjectId, @@ -1192,7 +1190,7 @@ public async Task mTLSPoP_Uami_ClientIdThenObjectId_MintsThenCaches_SubjectCNIsC var o1 = await miObjectId.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(MTLSPoP, o1.TokenType); @@ -1200,7 +1198,7 @@ public async Task mTLSPoP_Uami_ClientIdThenObjectId_MintsThenCaches_SubjectCNIsC AssertCertSubjectCnDc(o1.BindingCertificate, expectedCn, expectedDc, "[object_id first]"); var objectIdThumb = o1.BindingCertificate.Thumbprint; - // (3) object_id again → CACHED + // (3) object_id again ? CACHED var o2 = await miObjectId.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() .ExecuteAsync().ConfigureAwait(false); @@ -1228,7 +1226,7 @@ public async Task mTLSPoP_Uami_ClientIdThenAlias_MintsThenCaches_SubjectCNIsClie string expectedDc = TestConstants.TenantId; string rawCert = CreateRawCertForCsrKeyWithCnDc(expectedCn, expectedDc, DateTimeOffset.UtcNow.AddYears(20)); - // (1) client_id → MINT (CSR + issuecredential + token) + // (1) client_id ? MINT (CSR + issuecredential + token) var miClientId = await CreateManagedIdentityAsync( httpManager, userAssignedIdentityId: UserAssignedIdentityId.ClientId, @@ -1243,14 +1241,14 @@ public async Task mTLSPoP_Uami_ClientIdThenAlias_MintsThenCaches_SubjectCNIsClie var c1 = await miClientId.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(MTLSPoP, c1.TokenType, "[client_id]"); Assert.AreEqual(TokenSource.IdentityProvider, c1.AuthenticationResultMetadata.TokenSource, "[client_id] should mint"); AssertCertSubjectCnDc(c1.BindingCertificate, expectedCn, expectedDc, "[client_id]"); - // (2) alias (object_id/resource_id) → MINT (new alias → new cache key) + // (2) alias (object_id/resource_id) ? MINT (new alias ? new cache key) var miAlias = await CreateManagedIdentityAsync( httpManager, userAssignedIdentityId: aliasKind, @@ -1267,7 +1265,7 @@ public async Task mTLSPoP_Uami_ClientIdThenAlias_MintsThenCaches_SubjectCNIsClie var a1 = await miAlias.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(MTLSPoP, a1.TokenType, $"[{label} first]"); @@ -1275,10 +1273,10 @@ public async Task mTLSPoP_Uami_ClientIdThenAlias_MintsThenCaches_SubjectCNIsClie AssertCertSubjectCnDc(a1.BindingCertificate, expectedCn, expectedDc, $"[{label} first]"); var aliasThumb = a1.BindingCertificate.Thumbprint; - // (3) alias again → CACHED (no /issuecredential; no extra mocks needed) + // (3) alias again ? CACHED (no /issuecredential; no extra mocks needed) var a2 = await miAlias.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TokenSource.Cache, a2.AuthenticationResultMetadata.TokenSource, $"[{label} second] should be cached"); @@ -1322,7 +1320,7 @@ public async Task mTLSPop_ShortLivedCert_LessThan24h_NotCached_ReMints( var first = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TokenSource.IdentityProvider, first.AuthenticationResultMetadata.TokenSource, $"[{label}] first must mint."); @@ -1333,7 +1331,7 @@ public async Task mTLSPop_ShortLivedCert_LessThan24h_NotCached_ReMints( var second = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithForceRefresh(true) // <-- key change .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TokenSource.IdentityProvider, second.AuthenticationResultMetadata.TokenSource, $"[{label}] second must mint (no cert cache for <24h)."); @@ -1357,7 +1355,7 @@ public async Task mTLSPop_CertAtLeast24h_IsCached_ReusedOnSecondAcquire( ManagedIdentityClient.ResetSourceForTest(); SetEnvironmentVariables(ManagedIdentitySource.ImdsV2, TestConstants.ImdsEndpoint); - // NotAfter >= 24h + 1min → should be cached and reused + // NotAfter >= 24h + 1min ? should be cached and reused var rawLong = CreateRawCertForCsrKeyWithCnDc( cn: (idKind == UserAssignedIdentityId.None ? Constants.ManagedIdentityDefaultClientId : TestConstants.ClientId), dc: TestConstants.TenantId, @@ -1366,20 +1364,20 @@ public async Task mTLSPop_CertAtLeast24h_IsCached_ReusedOnSecondAcquire( var mi = await CreateManagedIdentityAsync(httpManager, idKind, idValue, managedIdentityKeyType: ManagedIdentityKeyType.KeyGuard) .ConfigureAwait(false); - // First acquire → MINT + // First acquire ? MINT AddMocksToGetEntraToken(httpManager, idKind, idValue, certificateRequestCertificate: rawLong); var first = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TokenSource.IdentityProvider, first.AuthenticationResultMetadata.TokenSource, $"[{label}] first must mint long-lived cert."); - // Second acquire → CACHED (no /issuecredential mocks needed) + // Second acquire ? CACHED (no /issuecredential mocks needed) var second = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationProviderForTests(s_fakeAttestationProvider) + .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TokenSource.Cache, second.AuthenticationResultMetadata.TokenSource, $"[{label}] second should be cache."); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/TestAttestationProviders.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/TestAttestationProviders.cs new file mode 100644 index 0000000000..160026233e --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/TestAttestationProviders.cs @@ -0,0 +1,120 @@ +// 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.ManagedIdentity; + +namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests +{ + /// + /// Test attestation providers for unit testing. + /// + internal static class TestAttestationProviders + { + /// + /// Fake attestation provider that returns a mock JWT. + /// + public static IAttestationProvider CreateFakeProvider() + { + return new FakeAttestationProvider(); + } + + /// + /// Attestation provider that returns null (for error testing). + /// + public static IAttestationProvider CreateNullProvider() + { + return new NullAttestationProvider(); + } + + /// + /// Attestation provider that returns empty/whitespace token (for error testing). + /// + public static IAttestationProvider CreateEmptyProvider() + { + return new EmptyAttestationProvider(); + } + + /// + /// Attestation provider that counts calls. + /// + public static CountingAttestationProvider CreateCountingProvider() + { + return new CountingAttestationProvider(); + } + + private class FakeAttestationProvider : IAttestationProvider + { + public Task AttestKeyGuardAsync( + string attestationEndpoint, + SafeHandle keyHandle, + string clientId, + CancellationToken cancellationToken) + { + return Task.FromResult(new AttestationResult + { + Status = AttestationStatus.Success, + Jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.fake.attestation.sig", + ErrorMessage = null, + NativeErrorCode = 0 + }); + } + } + + private class NullAttestationProvider : IAttestationProvider + { + public Task AttestKeyGuardAsync( + string attestationEndpoint, + SafeHandle keyHandle, + string clientId, + CancellationToken cancellationToken) + { + return Task.FromResult(null); + } + } + + private class EmptyAttestationProvider : IAttestationProvider + { + public Task AttestKeyGuardAsync( + string attestationEndpoint, + SafeHandle keyHandle, + string clientId, + CancellationToken cancellationToken) + { + return Task.FromResult(new AttestationResult + { + Status = AttestationStatus.Success, + Jwt = " ", + ErrorMessage = null, + NativeErrorCode = 0 + }); + } + } + + public class CountingAttestationProvider : IAttestationProvider + { + private int _callCount; + + public int CallCount => _callCount; + + public Task AttestKeyGuardAsync( + string attestationEndpoint, + SafeHandle keyHandle, + string clientId, + CancellationToken cancellationToken) + { + Interlocked.Increment(ref _callCount); + return Task.FromResult(new AttestationResult + { + Status = AttestationStatus.Success, + Jwt = "header.payload.sig", + ErrorMessage = null, + NativeErrorCode = 0 + }); + } + } + } +} 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 eb6a842e0f..71989abaa8 100644 --- a/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj +++ b/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj @@ -16,6 +16,7 @@ + {3433eb33-114a-4db7-bc57-14f17f55da3c} Microsoft.Identity.Client From 90b5326a425b78565c2b11535b39080c9617853f Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Tue, 16 Dec 2025 16:48:22 -0500 Subject: [PATCH 10/13] fixed broken unit tests --- .../ManagedIdentityTests/ImdsV2Tests.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs index 1b5e0a43a8..b36321771b 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsV2Tests.cs @@ -893,7 +893,6 @@ public async Task mTLSPopTokenHappyPath_LongLivedCert_IdentityMapping( var first = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(first); @@ -906,7 +905,6 @@ public async Task mTLSPopTokenHappyPath_LongLivedCert_IdentityMapping( var second = await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.IsNotNull(second); @@ -1122,7 +1120,6 @@ public async Task mTLSPop_SubjectCnDc_MatchesMetadata_AndCaches( var first = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(MTLSPoP, first.TokenType, $"[{label}]"); @@ -1130,7 +1127,6 @@ public async Task mTLSPop_SubjectCnDc_MatchesMetadata_AndCaches( var second = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .WithMtlsProofOfPossession() - .WithAttestationSupport() .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TokenSource.Cache, second.AuthenticationResultMetadata.TokenSource, $"[{label}] cache"); From 2a50866bb70204135336d0d18fdd1e04494026e7 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Tue, 16 Dec 2025 17:12:18 -0500 Subject: [PATCH 11/13] updated msiv2 demo app --- prototype/MsiV2DemoApp/MsiV2DemoApp.csproj | 1 + prototype/MsiV2DemoApp/Program.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/prototype/MsiV2DemoApp/MsiV2DemoApp.csproj b/prototype/MsiV2DemoApp/MsiV2DemoApp.csproj index db688e9072..dda6a17198 100644 --- a/prototype/MsiV2DemoApp/MsiV2DemoApp.csproj +++ b/prototype/MsiV2DemoApp/MsiV2DemoApp.csproj @@ -9,6 +9,7 @@ + diff --git a/prototype/MsiV2DemoApp/Program.cs b/prototype/MsiV2DemoApp/Program.cs index 02cd5adce7..a9b71ed9ae 100644 --- a/prototype/MsiV2DemoApp/Program.cs +++ b/prototype/MsiV2DemoApp/Program.cs @@ -40,7 +40,7 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; -using Microsoft.Identity.Client.MtlsPop; +using Microsoft.Identity.Client.KeyAttestation; using Microsoft.IdentityModel.Abstractions; using System.Net.Http; using System.Net.Http.Headers; From 31cf11d72e4e1412e9d89d9fdc5e3dfb17610e12 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 17 Dec 2025 18:34:34 -0500 Subject: [PATCH 12/13] added .WithAttestationSupport to demo app --- prototype/MsiV2DemoApp/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prototype/MsiV2DemoApp/Program.cs b/prototype/MsiV2DemoApp/Program.cs index a9b71ed9ae..d60cf1f225 100644 --- a/prototype/MsiV2DemoApp/Program.cs +++ b/prototype/MsiV2DemoApp/Program.cs @@ -256,7 +256,7 @@ private static IManagedIdentityApplication BuildMiApp(ManagedIdentityId miId, II bool showFullToken) { var builder = app.AcquireTokenForManagedIdentity(scope); - if (useMtls) builder = builder.WithMtlsProofOfPossession(); + if (useMtls) builder = builder.WithMtlsProofOfPossession().WithAttestationSupport(); if (forceRefresh) builder = builder.WithForceRefresh(true); var result = await Ui.WithSpinnerAsync( From 7a19b8a31c82dc3ca18521c8740c76053c15dc3c Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 17 Dec 2025 19:23:18 -0500 Subject: [PATCH 13/13] fixed unshpped files --- .../PublicApi/net8.0-android/PublicAPI.Unshipped.txt | 2 +- .../PublicApi/net8.0-ios/PublicAPI.Unshipped.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 82f7291a54..12e767a98e 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 @@ -7,4 +7,4 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidation Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task Microsoft.Identity.Client.ManagedIdentityPopExtensions -static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! +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 82f7291a54..12e767a98e 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 @@ -7,4 +7,4 @@ Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidation Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task Microsoft.Identity.Client.ManagedIdentityPopExtensions -static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! +static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder