Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Registers the mTLS PoP attestation runtime (interop) by installing a provider
/// function into MSAL's internal config.
/// </summary>
public static class ManagedIdentityPopExtensions
{
/// <summary>
/// App-level registration: tells MSAL how to obtain a KeyGuard/CNG handle
/// and perform attestation to get the JWT needed for mTLS PoP.
/// </summary>
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;
}

/// <summary>
/// Adds the runtime support by registering the attestation function.
/// </summary>
/// <param name="builder"></param>
/// <exception cref="MsalClientException"></exception>
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()
Original file line number Diff line number Diff line change
@@ -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

Original file line number Diff line number Diff line change
@@ -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

Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Extension methods for enabling mTLS Proof-of-Possession in managed identity flows.
/// </summary>
public static class ManagedIdentityPopExtensions
{
/// <summary>
/// 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.
/// </summary>
/// <param name="builder">The AcquireTokenForManagedIdentityParameterBuilder instance.</param>
/// <returns>The builder to chain .With methods.</returns>
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,8 @@ private async Task<CertificateRequestResponse> 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);

Expand All @@ -200,6 +194,10 @@ private async Task<CertificateRequestResponse> 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()
{
Expand Down Expand Up @@ -261,6 +259,22 @@ protected override async Task<ManagedIdentityRequest> CreateRequestAsync(string
{
CsrMetadata csrMetadata = await GetCsrMetadataAsync(_requestContext).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(
Expand Down Expand Up @@ -357,9 +371,19 @@ private async Task<string> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource>
Microsoft.Identity.Client.ManagedIdentityPopExtensions
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource>
Microsoft.Identity.Client.ManagedIdentityPopExtensions
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource>
Microsoft.Identity.Client.ManagedIdentityPopExtensions
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder!
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource>
Microsoft.Identity.Client.ManagedIdentityPopExtensions
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder! builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder!
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource>
Microsoft.Identity.Client.ManagedIdentityPopExtensions
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource>
Microsoft.Identity.Client.ManagedIdentityPopExtensions
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
111 changes: 111 additions & 0 deletions src/client/Msal.KeyAttestation/AttestationClient.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Managed façade for <c>AttestationClientLib.dll</c>. Holds initialization state,
/// does ref-count hygiene on <see cref="SafeNCryptKeyHandle"/>, and returns a JWT.
/// </summary>
internal sealed class AttestationClient : IDisposable
{
private bool _initialized;

/// <summary>
/// AttestationClient constructor. Relies on the default OS loader to locate the native DLL.
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
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.");
}

/// <summary>
/// Calls the native <c>AttestKeyGuardImportKey</c> and returns a structured result.
/// </summary>
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();
}
}

/// <summary>
/// Disposes the client, releasing any resources and un-initializing the native library.
/// </summary>
public void Dispose()
{
if (_initialized)
{
AttestationClientLib.UninitAttestationLib();
_initialized = false;
}
GC.SuppressFinalize(this);
}
}
}
Loading
Loading