Skip to content

Add Managed Identity v2 KeyGuard mTLS PoP support - #1059

Open
Gladwin Johnson VR (gladjohn) wants to merge 6 commits into
devfrom
gladjohn/java-msi-v2-mtls-binding-context-local
Open

Add Managed Identity v2 KeyGuard mTLS PoP support#1059
Gladwin Johnson VR (gladjohn) wants to merge 6 commits into
devfrom
gladjohn/java-msi-v2-mtls-binding-context-local

Conversation

@gladjohn

@gladjohn Gladwin Johnson VR (gladjohn) commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds production-shaped Managed Identity v2 mTLS support for Java using Windows
VBS KeyGuard and optional Microsoft Azure Attestation.

Java retains native interop only for KeyGuard/CNG signing and attestation.
JCA/JSSE owns TLS, and mTLS PoP callers receive a reusable
IMtlsBindingContext for independent downstream Java HTTP calls.

Reviewer entry point: managed-identity-v2-mtls-pop-review-guide.md

Architecture

ManagedIdentityApplication
  -> IMDS v2 platform metadata
  -> KeyGuard non-exportable RSA key
  -> optional MAA attestation
  -> IMDS v2 binding certificate
  -> normal MSAL OAuth token pipeline over JSSE mTLS
  -> mtls_pop token + reusable IMtlsBindingContext

Native signing remains limited to:

JSSE
  -> X509ExtendedKeyManager
  -> CngRsaPrivateKey
  -> CngSignatureSpi
  -> NCryptSignHash
  -> VBS KeyGuard

Changes

  • Adds managed-identity mTLS PoP and binding-aware token caching keyed by token
    type, complete leaf-certificate DER SHA-256 identity, and attestation mode.
  • Routes the ESTS mTLS exchange through the normal MSAL token pipeline for
    claims, client capabilities, telemetry, correlation IDs, retries, error
    handling, and response parsing.
  • Adds an optional Windows extension for CNG, KeyGuard, CSR generation,
    attestation, certificate rotation, and reusable JSSE binding contexts.
  • Bundles and verifies the Microsoft-signed
    Microsoft.Azure.Security.KeyGuardAttestation 1.1.5
    AttestationClientLib.dll.
  • Requires exact token_type=mtls_pop, HTTPS token endpoints, IMDS-specific
    retry behavior, validated IMDS response origin, and disabled redirects for
    credential-bound requests.
  • Defines IMtlsCapableHttpClient for custom transports and fails fast when a
    custom client does not consume request-specific mTLS configuration.
  • Detects and recreates stale per-boot KeyGuard keys using a private signing
    liveness probe.
  • Keeps the manual Key Vault E2E application in a separate profile-only module.

.NET parity follow-up

The following parity changes are included in this PR branch:

  • Moves the public attestation opt-in out of core and into the optional
    com.microsoft.azure:msal4j-key-attestation artifact through
    ManagedIdentityAttestationExtensions.withAttestationSupport(...), matching
    the MSAL.NET package boundary.
  • Adds bearer-over-mTLS through withRequestOverMtls(). It authenticates the
    ESTS connection with KeyGuard, requests token_type=bearer, uses a distinct
    cache partition, and intentionally returns no downstream binding context.
  • Adds SLF4J-backed native attestation logging with correlation IDs, safe native
    callback lifetime, and suppression of raw native debug payloads.
  • Adds MSAL_MI_DISABLE_IMDS_V2; true or 1 disables v2, capability
    discovery reports no mTLS binding, and explicit PoP or bearer-over-mTLS
    requests fail before metadata, key, CSR, certificate, or attestation work.
  • Keeps the KeyGuard key container persistent but the issued certificate and
    binding context process-memory only. No Windows certificate-store persistence
    is required.
  • Does not add token-revocation reminting, claims-triggered certificate
    replacement, or bypass_cache.

The optional bounded capability-discovery timeout proposed in MSAL.NET issue
#6180 remains a separate follow-up. It is useful for credential-chain failover,
but is not currently shipped by MSAL.NET and requires a Java cancellation and
transport-budget contract rather than a superficial future timeout.

Validation

  • Full msal4j-sdk tests pass.
  • Targeted optional-package API and native logger tests pass.
  • Production classes remain Java 8 compatible.
  • The production extension and shaded E2E JAR contain the verified native DLL;
    the production extension excludes the manual E2E app.
  • Live validation passed on a Windows Server 2025 Trusted Launch Azure VM:
    • acquired an attested mtls_pop token;
    • returned the binding certificate and reusable JSSE SSLContext;
    • verified JWT cnf.x5t#S256 against SHA-256 of the complete leaf certificate
      DER;
    • independently called a token-binding-enforced Azure Key Vault with
      HttpsURLConnection and received HTTP 200;
    • retried the same token without its binding certificate and received HTTP
      401 Unauthorized;
    • reacquired through MSAL and received TokenSource.CACHE with the matching
      live binding context;
    • force-refreshed and received TokenSource.IDENTITY_PROVIDER, then completed
      another HTTP 200 Key Vault call;
    • acquired an ordinary Bearer token over attested mTLS, exposed no binding
      context, and verified the second acquisition was a cache hit;
    • enabled MSAL_MI_DISABLE_IMDS_V2 and verified mTLS PoP failed before native
      attestation work.

Security properties

  • Private-key bytes are never exported into Java memory.
  • TLS is performed by Java JCA/JSSE, not WinHTTP or Schannel as the HTTP stack.
  • Attestation failures fail closed when attestation is requested.
  • Ordinary bearer, bearer-over-mTLS, mTLS PoP, attested, and unattested cache
    entries cannot cross-hit.
  • Same-key certificate renewal creates a new token-cache partition because the
    complete certificate DER changes.
  • Credential-bound redirects are disabled.
  • Custom HTTP clients fail fast unless they support request-specific mTLS.
  • Retired native key handles are cleaned after certificate expiry.

MSAL.NET parity gap analysis

A feature-by-feature comparison, intentional platform differences, validation evidence, and prioritized remaining release gaps are documented in managed-identity-v2-dotnet-java-parity-gap-analysis.md.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds optional Windows KeyGuard-backed Managed Identity v2 mTLS PoP support, including core SDK integration, native CNG/attestation extensions, caching, tests, documentation, and manual validation.

Changes:

  • Extends token acquisition, HTTP, result, and cache pipelines for mTLS-bound tokens.
  • Adds the Windows CNG/KeyGuard provider and IMDS v2 credential flow.
  • Adds unit tests, documentation, Maven modules, and an e2e validation app.

Reviewed changes

Copilot reviewed 61 out of 61 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
run-java-msi-v2-mtls-devapp.ps1 Builds and runs manual validation.
README.md Introduces the mTLS extension.
pom.xml Registers extension and e2e modules.
msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java Tests mTLS OAuth request construction.
msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityMtlsProviderLoaderTest.java Tests missing-provider failure.
msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityMtlsParametersTest.java Tests parameters, cache partitioning, and token validation.
msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityMtlsBindingTest.java Tests token endpoint validation.
msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/DefaultHttpClientMtlsTest.java Tests redirect prevention.
msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AuthenticationResultMtlsTest.java Tests binding serialization and equality.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java Parses OAuth token type.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java Sends request-specific mTLS token requests.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java Supports explicit extended cache hashes.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java Propagates request socket factories.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalRequest.java Stores extended cache-key hashes.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalError.java Defines mTLS error codes.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityResponse.java Parses relative expiration values.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityParameters.java Adds mTLS and attestation options.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityMtlsRequest.java Defines the provider request contract.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityMtlsProviderLoader.java Discovers optional providers.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityMtlsHttpResponse.java Defines callback HTTP responses.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityMtlsHttpRequest.java Defines callback HTTP requests.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ManagedIdentityMtlsBinding.java Represents validated binding generations.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IMtlsCapableHttpClient.java Marks mTLS-capable custom clients.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IMtlsBindingContext.java Exposes process-local JSSE bindings.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IManagedIdentityMtlsProvider.java Defines the optional provider SPI.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IManagedIdentityMtlsHttpClient.java Defines the MSAL HTTP callback.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IAuthenticationResult.java Exposes token and binding metadata.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpRequest.java Carries request-specific socket factories.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/DefaultHttpClient.java Applies mTLS factories and disables redirects.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java Stores token type and live bindings.
msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByManagedIdentitySupplier.java Integrates binding, cache, IMDS, and OAuth flows.
msal4j-sdk/docs/managed-identity-v2-mtls-pop.md Documents architecture and validation.
msal4j-mtls-extensions/src/test/java/com/microsoft/aad/msal4j/mtls/Pkcs10BuilderTest.java Tests CSR DER generation.
msal4j-mtls-extensions/src/test/java/com/microsoft/aad/msal4j/mtls/KeyGuardMtlsBindingContextTest.java Tests binding key IDs and contexts.
msal4j-mtls-extensions/src/test/java/com/microsoft/aad/msal4j/mtls/KeyGuardManagedIdentityMtlsProviderTest.java Tests certificate rotation timing.
msal4j-mtls-extensions/src/test/java/com/microsoft/aad/msal4j/mtls/ImdsV2ClientTest.java Tests IMDS v2 contracts.
msal4j-mtls-extensions/src/test/java/com/microsoft/aad/msal4j/mtls/CngX509ExtendedKeyManagerTest.java Tests RSA alias selection.
msal4j-mtls-extensions/src/test/java/com/microsoft/aad/msal4j/mtls/CngSignatureParametersTest.java Tests PSS parameter validation.
msal4j-mtls-extensions/src/test/java/com/microsoft/aad/msal4j/mtls/CngRsaPrivateKeyTest.java Tests non-exportability and cleanup.
msal4j-mtls-extensions/src/test/java/com/microsoft/aad/msal4j/mtls/CngProviderTest.java Tests provider registration and delegation.
msal4j-mtls-extensions/src/test/java/com/microsoft/aad/msal4j/mtls/CngKeyGuardTest.java Tests stale native-key deletion.
msal4j-mtls-extensions/src/test/java/com/microsoft/aad/msal4j/mtls/AttestationTokenCacheTest.java Tests attestation caching and concurrency.
msal4j-mtls-extensions/src/main/resources/META-INF/services/com.microsoft.aad.msal4j.IManagedIdentityMtlsProvider Registers the KeyGuard provider.
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/Pkcs10Builder.java Builds PKCS#10 CSRs.
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/NCryptLibrary.java Defines Windows NCrypt bindings.
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/MtlsMsiException.java Defines extension failures.
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/KeyGuardMtlsBindingContext.java Creates JSSE binding contexts.
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/KeyGuardManagedIdentityMtlsProvider.java Manages binding creation and rotation.
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/ImdsV2Client.java Implements IMDS v2 requests and parsing.
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/CngX509ExtendedKeyManager.java Supplies KeyGuard credentials to JSSE.
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/CngSignatureSpi.java Implements CNG-backed RSA signing.
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/CngRsaPrivateKey.java Wraps non-exportable CNG keys.
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/CngProvider.java Registers CNG signature services.
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/CngKeyGuard.java Implements KeyGuard and attestation interop.
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/AttestationTokenCache.java Caches fresh attestation JWTs.
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/AttestationLibrary.java Defines attestation DLL bindings.
msal4j-mtls-extensions/README.md Documents extension usage.
msal4j-mtls-extensions/pom.xml Configures the extension artifact.
msal4j-mtls-extensions-e2e/src/main/java/com/microsoft/aad/msal4j/mtls/e2e/ManagedIdentityMtlsPopKeyVaultDevApp.java Implements manual Key Vault validation.
msal4j-mtls-extensions-e2e/pom.xml Builds the shaded validation app.
.github/copilot-instructions.md Documents the new architecture and modules.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/copilot-instructions.md Outdated
@gladjohn Gladwin Johnson VR (gladjohn) changed the title Gladjohn/java msi v2 mtls binding context local Add Managed Identity v2 KeyGuard mTLS PoP support Aug 16, 2026
@gladjohn
Gladwin Johnson VR (gladjohn) force-pushed the gladjohn/java-msi-v2-mtls-binding-context-local branch 2 times, most recently from a5eec14 to e2b03ec Compare August 16, 2026 02:29
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 30714a3c-b1e6-4578-8223-e07611e44e1d
*/
public interface IMtlsBindingContext {

SSLContext sslContext();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider also exposing the underlying KeyManager (or a KeyManagerFactory / X509ExtendedKeyManager) here, not just a fully built SSLContext. Most Azure SDK data-plane clients on Java run on Netty (reactor-netty), Vert.x, or the JDK HttpClient, and those stacks want to build their own SslContext from a KeyManager rather than consume a prebuilt JSSE SSLContext. KeyGuardMtlsBindingContext already constructs a CngX509ExtendedKeyManager internally, so surfacing it would let non-JSSE transports wire the binding without reflection or rebuilding. Keeping sslContext() is still useful for the JSSE / HttpsURLConnection path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exposed the CNG-backed X509ExtendedKeyManager alongside SSLContext so Netty, JDK HttpClient, and other transports can build their own TLS context.

Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpRequest.java
Add binding capability discovery and strength enforcement, expose transport-neutral TLS material, harden native loading, and expand local mTLS coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 30714a3c-b1e6-4578-8223-e07611e44e1d

@g2vinay Vinay Gera (g2vinay) left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after the latest fix commit. The mTLS PoP surface looks solid. Most of my remaining notes are consumer-side integration questions from the Azure SDK for Java side (azure-identity / azure-core), where these bindings will be wired into the HTTP pipeline across pluggable transports.

The higher-priority items are the SSLEngine alias path and issuer-independent alias selection in CngX509ExtendedKeyManager (our default reactor-netty and JDK HttpClient transports are engine-based and MI certs are self-issued), the resource-facing contract (whether the same cert must ride the data-plane call and what Authorization scheme to use for mtls_pop), and the transient-binding cache-reload path. The rest are clarifications on thread-safety, binding identity/rotation lifecycle, and the keyManager() vs sslContext() trust scope. None are blocking; mostly confirmations and a couple of test/javadoc asks. Thanks.

}

@Override
public String chooseEngineClientAlias(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to see chooseEngineClientAlias overridden here. That is the path our default HTTP transport (reactor-netty) and the JDK HttpClient actually use, since both drive TLS through an SSLEngine rather than SSLSocket. Could we add (or point me to) a test that exercises alias selection through a real SSLEngine handshake, not just SSLSocket? That is the path most Azure SDK consumers will hit, and it is easy for the engine path to regress without a test that catches it.

}

@Override
public String chooseClientAlias(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question on alias selection: do chooseClientAlias and chooseEngineClientAlias return the binding alias regardless of the issuers argument? The MI leaf is attested/self-issued and will not chain to a public CA, so during the handshake the server often advertises an empty or non-matching CA list. If selection is filtered by issuers, these methods would return null and no client cert gets sent, which fails silently as an empty client Certificate message. Want to confirm the selection is unconditional here.

private final AuthenticationResultMetadata metadata;
private final Boolean isPopAuthorization;
private final String tokenType;
private final transient IMtlsBindingContext mtlsBindingContext;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking the binding transient is right for not serializing key material, but it means a result restored from the persistent token cache comes back with mtlsBindingContext == null. I see the constructor guard just below throws when tokenType is mtls_pop and the binding is null, which is good, but does deserialization actually go through that constructor, or could a cached mtls_pop token resurface without its binding? Want to make sure we cannot silently end up with an mtls_pop token and no cert on the wire after a cache reload.

import java.security.PrivateKey;
import java.security.cert.X509Certificate;

final class CngX509ExtendedKeyManager extends X509ExtendedKeyManager {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread-safety check: these KeyManager callbacks get invoked concurrently when a connection pool opens several handshakes in parallel. Is this class, and the underlying CNG / native signing path it delegates to, safe for concurrent use from multiple threads? If there is shared native state per key handle, it would be good to confirm the signing calls are either stateless or synchronized.

/**
* Base64URL-without-padding SHA-256 digest of the complete leaf certificate DER.
*/
String keyId();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For connection pooling on our side we need a stable identity for the binding so we can key/segment pools and detect rotation. A few things to confirm about keyId(): is it stable for the life of a token and unique per binding (it looks derived from the full leaf cert DER, so renewal changes it, which is what we want)? On rotation, is the previous native key handle disposed/closed so we do not leak CNG handles? And does keyManager() return a stable instance for a given token rather than a new one per call?

* Returns a ready-to-use JSSE context. The context uses the JVM's default trust
* managers.
*/
SSLContext sslContext();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming the intended split: sslContext() is the ready-to-use context for MSAL's own token call (and for simple JSSE consumers), while for resource calls that need custom trust or a different transport we should take keyManager() and build our own context, pairing it with the platform default trust managers. If that is right, a line in the javadoc steering consumers away from reusing the token-acquisition SSLContext for data-plane traffic would help, since reusing it would also apply MSAL's trust configuration to resource calls.

Resolve the dev merge conflict and add SSLEngine coverage, cache reload validation, native key lifecycle synchronization, rotation tests, and downstream transport guidance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 30714a3c-b1e6-4578-8223-e07611e44e1d
Re-check completed capability discovery synchronously so Java 8 cannot return a transiently unavailable result before the asynchronous cache-reset callback runs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 30714a3c-b1e6-4578-8223-e07611e44e1d
Move attestation opt-in to the optional package, add bearer-over-mTLS, native logging, the IMDS v2 kill switch, DevEx documentation, and real-VM validation coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 30714a3c-b1e6-4578-8223-e07611e44e1d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 30714a3c-b1e6-4578-8223-e07611e44e1d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants