Add Managed Identity v2 KeyGuard mTLS PoP support - #1059
Add Managed Identity v2 KeyGuard mTLS PoP support#1059Gladwin Johnson (gladjohn) wants to merge 2 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
| /** | ||
| * Manual Java 8 validation for attested managed identity v2 mTLS PoP with Key Vault. | ||
| */ | ||
| public final class ManagedIdentityMtlsPopKeyVaultDevApp { |
There was a problem hiding this comment.
The unit coverage on the signing and PKCS10 pieces is solid, but the end-to-end path of binding to SSLContext to a real mTLS handshake is only exercised by this manual dev app, which cannot run in CI because it needs KeyGuard hardware. Consider an automated test that stands up a local TLS server requiring client auth and drives it with a software-backed KeyManager double implementing the same contract, so regressions in the SSLContext wiring (protocol, key manager, cert chain) are caught in CI even though the native KeyGuard path cannot run there.
There was a problem hiding this comment.
Added a software-backed local mTLS server/client handshake test for CI coverage; the real KeyGuard flow was also validated manually on the VM because the Java pipeline cannot attach that specialized VM.
| /** | ||
| * Requests the KeyGuard managed identity v2 mTLS PoP flow. | ||
| */ | ||
| public ManagedIdentityParametersBuilder withMtlsProofOfPossession() { |
There was a problem hiding this comment.
There is currently no way to ask whether mTLS PoP is actually available before calling. A credential chain like Azure Identity's ManagedIdentityCredential needs to decide up front whether to attempt the mTLS path or fall back, and today the only signal is to call and catch. Consider a lightweight capability probe (for example a static isMtlsPopSupported() or a getManagedIdentityCapabilities() view) that reflects platform, provider presence via ServiceLoader, and the IMDS v2 source, so consumers can branch without triggering a full token attempt.
There was a problem hiding this comment.
Added getManagedIdentityCapabilities() with IMDS v2/provider/KeyGuard discovery, NONE/SOFTWARE/KEY_GUARD strength reporting, retryable failed probes, and a minimum-strength request floor for credential chains.
| */ | ||
| 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.
| 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 context transient is correct since it is process-local and non-serializable. Worth confirming the persisted-cache path explicitly: when an mtls_pop entry is written to a persistent cache and later deserialized, this field comes back null. Please confirm that path fails closed, that is, the SDK never hands back an mtls_pop token without a live binding context (which would look like a usable token but cannot complete the mTLS handshake). Including key_id in the cache key hash is a good guard; a short test around deserialize-then-use would lock the behavior in.
There was a problem hiding this comment.
Added fail-closed deserialization so an mtls_pop result cannot be restored or returned without its live process-local binding context, with coverage for the persisted-result path.
|
|
||
| static Path extractBundledLibrary() throws MtlsMsiException { | ||
| String architecture = System.getProperty("os.arch", ""); | ||
| if (!"amd64".equalsIgnoreCase(architecture) |
There was a problem hiding this comment.
The clear typed failure for non-x64 is good and much better than a confusing link error. Since Windows on ARM is increasingly common for Azure VMs, it would help to note the arm64 roadmap here (bundle a second native or document the intended timeline) so consumers know whether they need to design a fallback for that platform today.
There was a problem hiding this comment.
Documented Windows ARM64 as unsupported in this release; adding it requires an upstream ARM64 AttestationClientLib native artifact, so callers should retain a platform fallback for now.
| } | ||
|
|
||
| String actualHash = toHex(digest.digest()); | ||
| if (!SHA256.equals(actualHash)) { |
There was a problem hiding this comment.
Good that the extracted DLL is integrity-checked. One gap: a pinned SHA-256 constant that ships in the same JAR as the DLL only proves the bytes match what this build embedded. If the JAR is repackaged, both the DLL and this constant move together, so the check still passes for a substituted binary. For a native library that performs attestation, consider also verifying the Authenticode signature (WinVerifyTrust) so the loaded DLL is proven Microsoft-signed independent of JAR integrity.
There was a problem hiding this comment.
Added fail-closed WinVerifyTrust Authenticode validation before Native.load in addition to the pinned SHA-256 check, and locally confirmed the bundled DLL signer is Microsoft Corporation.
| " native library is missing from the extension JAR."); | ||
| } | ||
|
|
||
| directory = Files.createTempDirectory("msal4j-keyguard-attestation-"); |
There was a problem hiding this comment.
Extracting the native library to a temp directory and loading it by absolute path is the right approach and avoids PATH-based planting of the DLL itself. The residual Windows risk is dependency sideloading: when AttestationClientLib.dll loads, its own dependency DLLs can still be resolved from the load directory (this temp folder) under the default search order. Consider restricting the extraction directory ACLs to the current user and/or using the safe DLL search directory APIs, so a co-located planted dependency cannot be picked up.
There was a problem hiding this comment.
Restricted the extraction directory ACL to the current Windows user and now fail closed when Windows ACL support is unavailable, while continuing to load the verified DLL by absolute path.
| } catch (RuntimeException e) { | ||
| MsalClientException wrapped = new MsalClientException( | ||
| "The managed identity mTLS provider failed.", | ||
| MsalError.MANAGED_IDENTITY_MTLS_REQUEST_FAILED); |
There was a problem hiding this comment.
Several very different conditions all collapse into MsalClientException with MANAGED_IDENTITY_MTLS_REQUEST_FAILED: permanent setup or config errors (wrong MI source, unsupported selector, a custom client missing the marker) and potentially transient runtime failures (the provider or attestation call failing here). A consumer that wants to fall back in a credential chain cannot distinguish "this will never work, stop" from "this failed once, maybe retry or move on" without string matching. Consider mapping permanent errors to a distinct code from transient ones (you already have MANAGED_IDENTITY_MTLS_PROVIDER_UNAVAILABLE, so extending that taxonomy would help) and preserving the underlying MtlsMsiException as the cause so the specific reason is not lost.
There was a problem hiding this comment.
Added distinct permanent error codes for unsupported source/selector and custom HTTP clients, a dedicated minimum-strength error, retryable capability discovery, and preserved provider failures as the cause.
| * <p>Implementations must use {@link HttpRequest#sslSocketFactory()} when it is present | ||
| * and must not automatically follow redirects for that credential-bound request.</p> | ||
| */ | ||
| public interface IMtlsCapableHttpClient extends IHttpClient { |
There was a problem hiding this comment.
This marker means any existing custom IHttpClient fails closed for mTLS PoP until it is changed to implement IMtlsCapableHttpClient. That is a reasonable safety default, but it is a real friction point for consumers like Azure Identity, whose HttpPipelineAdapter implements IHttpClient and would otherwise silently lose mTLS support. Two asks: (1) call this out clearly in the changelog and migration notes as a required opt-in for custom clients, and (2) consider shipping a small default decorator that wraps a plain IHttpClient and applies HttpRequest.sslSocketFactory(), so integrators do not each reimplement the same socket-factory plumbing.
There was a problem hiding this comment.
Documented the required IMtlsCapableHttpClient opt-in and migration path; I did not add a universal decorator because it cannot safely retrofit request-specific TLS into arbitrary Netty, OkHttp, or JDK transports.
| /** | ||
| * Returns a request-specific JSSE socket factory, when the request requires mTLS. | ||
| */ | ||
| public SSLSocketFactory sslSocketFactory() { |
There was a problem hiding this comment.
SSLSocketFactory is a blocking, java.net.Socket-shaped abstraction. Custom IHttpClient implementations built on async or SSLEngine-based stacks (Netty, the JDK HttpClient, OkHttp) cannot consume an SSLSocketFactory directly, so this contract effectively limits request-scoped mTLS to blocking JSSE clients. Consider carrying the binding as an SSLContext (or the KeyManager) on the request so an async client can derive its own engine, or clearly document that mTLS PoP requires a JSSE-based IHttpClient.
There was a problem hiding this comment.
HttpRequest now carries the request-specific SSLContext as well as the legacy SSLSocketFactory, and the binding exposes its X509ExtendedKeyManager for SSLEngine-based transports.
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
Summary
Adds production-shaped Managed Identity v2 mTLS Proof-of-Possession 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 callers receive a reusable
IMtlsBindingContextcontaining the binding certificate and standard JavaSSLContextfor independent downstream HTTP calls.Reviewer entry point:
managed-identity-v2-mtls-pop-review-guide.mdArchitecture
Native signing remains limited to:
Changes
Microsoft.Azure.Security.KeyGuardAttestation1.1.5AttestationClientLib.dllin the optional extension, verifies its SHA-256 before loading, and includes its license and notice.token_type=mtls_popand HTTPS token endpoints.IMtlsCapableHttpClientfor custom transports.Validation
msal4j-sdkandmsal4j-mtls-extensionstest suites pass.mtls_poptoken;SSLContext;cnf.x5t#S256against SHA-256 of the full leaf certificate DER;HttpsURLConnectionand received HTTP 200;Unauthorized;TokenSource.CACHEwith the matching live binding context.Security properties