Add Managed Identity v2 KeyGuard mTLS PoP support - #1059
Add Managed Identity v2 KeyGuard mTLS PoP support#1059Gladwin Johnson VR (gladjohn) wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
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.
a5eec14 to
e2b03ec
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30714a3c-b1e6-4578-8223-e07611e44e1d
e2b03ec to
a0b0785
Compare
| */ | ||
| public interface IMtlsBindingContext { | ||
|
|
||
| SSLContext sslContext(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Exposed the CNG-backed X509ExtendedKeyManager alongside SSLContext so Netty, JDK HttpClient, and other transports can build their own TLS context.
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
Vinay Gera (g2vinay)
left a comment
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
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
IMtlsBindingContextfor independent downstream Java HTTP calls.Reviewer entry point:
managed-identity-v2-mtls-pop-review-guide.mdArchitecture
Native signing remains limited to:
Changes
type, complete leaf-certificate DER SHA-256 identity, and attestation mode.
claims, client capabilities, telemetry, correlation IDs, retries, error
handling, and response parsing.
attestation, certificate rotation, and reusable JSSE binding contexts.
Microsoft.Azure.Security.KeyGuardAttestation1.1.5AttestationClientLib.dll.token_type=mtls_pop, HTTPS token endpoints, IMDS-specificretry behavior, validated IMDS response origin, and disabled redirects for
credential-bound requests.
IMtlsCapableHttpClientfor custom transports and fails fast when acustom client does not consume request-specific mTLS configuration.
liveness probe.
.NET parity follow-up
The following parity changes are included in this PR branch:
com.microsoft.azure:msal4j-key-attestationartifact throughManagedIdentityAttestationExtensions.withAttestationSupport(...), matchingthe MSAL.NET package boundary.
withRequestOverMtls(). It authenticates theESTS connection with KeyGuard, requests
token_type=bearer, uses a distinctcache partition, and intentionally returns no downstream binding context.
callback lifetime, and suppression of raw native debug payloads.
MSAL_MI_DISABLE_IMDS_V2;trueor1disables v2, capabilitydiscovery reports no mTLS binding, and explicit PoP or bearer-over-mTLS
requests fail before metadata, key, CSR, certificate, or attestation work.
binding context process-memory only. No Windows certificate-store persistence
is required.
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
msal4j-sdktests pass.the production extension excludes the manual E2E app.
mtls_poptoken;SSLContext;cnf.x5t#S256against SHA-256 of the complete leaf certificateDER;
HttpsURLConnectionand received HTTP 200;401
Unauthorized;TokenSource.CACHEwith the matchinglive binding context;
TokenSource.IDENTITY_PROVIDER, then completedanother HTTP 200 Key Vault call;
Bearertoken over attested mTLS, exposed no bindingcontext, and verified the second acquisition was a cache hit;
MSAL_MI_DISABLE_IMDS_V2and verified mTLS PoP failed before nativeattestation work.
Security properties
entries cannot cross-hit.
complete certificate DER changes.
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.