Implement mTLS Proof-of-Possession (mTLS PoP) token acquisition#1021
Open
Robbie-Microsoft wants to merge 9 commits intodevfrom
Open
Implement mTLS Proof-of-Possession (mTLS PoP) token acquisition#1021Robbie-Microsoft wants to merge 9 commits intodevfrom
Robbie-Microsoft wants to merge 9 commits intodevfrom
Conversation
Adds mTLS PoP support for both ConfidentialClientApplication (SNI path) and ManagedIdentityApplication (subprocess path via MsalMtlsMsiHelper.exe). SNI path (native Java JSSE): - MtlsPopAuthenticationScheme: TOKEN_TYPE_MTLS_POP constant, computeX5tS256() thumbprint computation, buildMtlsTokenEndpoint() with public/sovereign cloud handling (US Gov + China unsupported) - MtlsSslContextHelper: creates SSLSocketFactory from PrivateKey + X509Certificate[] via in-memory PKCS12 KeyStore - TokenRequestExecutor: isMtlsPopRequest(), executeTokenRequestWithMtls(), getMtlsClientCertificate(); skips client_assertion, adds token_type=mtls_pop - ConfidentialClientApplication: validateMtlsPopParameters() pre-flight (cert required, tenanted authority, AAD only, region required) Managed Identity path (subprocess delegation): - New msal4j-mtls-extensions Maven module bundles MsalMtlsMsiHelper.exe (.NET 8 binary using CNG/Schannel; same approach as msal-node since Java SunMSCAPI uses legacy CAPI and cannot access KeyGuard keys) - MtlsMsiClient: subprocess wrapper with concurrent stdout/stderr threads to prevent deadlock; supports acquire-token and http-request modes - MtlsMsiHelperLocator: resolves binary via MSAL_MTLS_HELPER_PATH env var or bundled JAR resource (extracted to temp on first use) - ManagedIdentityApplication: validateMtlsPopParameters() with classpath check - AcquireTokenByManagedIdentitySupplier: executeMtlsPop() delegates to MtlsMsiClient via reflection (avoids compile-time dependency) Token cache isolation: - CredentialTypeEnum.ACCESS_TOKEN_WITH_AUTH_SCHEME (AccessToken_With_AuthScheme) - AccessTokenCacheEntity: keyId field (x5t#S256 thumbprint); getKey() appends as 7th segment only when non-blank (Bearer tokens keep 6-segment keys) - TokenCache.createAccessTokenCacheEntity: sets auth scheme type + keyId for mtls_pop token responses API additions: - ClientCredentialParameters.withMtlsProofOfPossession(boolean) - ManagedIdentityParameters.withMtlsProofOfPossession(boolean) - IAuthenticationResult.tokenType() / bindingCertificate() (default methods) - AuthenticationResult.tokenType / bindingCertificate fields + builder methods - TokenResponse: parses token_type from JSON response - AuthenticationErrorCode.INVALID_REQUEST Tests: 22 new unit tests in MtlsPopTest covering all new/modified code. All 324 tests pass (6 pre-existing lab cert errors unchanged). Docs: - msal4j-sdk/docs/mtls-pop.md (developer guide) - msal4j-sdk/docs/mtls-pop-manual-testing.md (testing guide) - msal4j-sdk/docs/keyguard-jvm-analysis.md (CNG/CAPI/JNI analysis) - msal4j-mtls-extensions/README.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace subprocess approach with JNA-backed java.security.Provider - NCryptLibrary.java, AttestationLibrary.java: JNA interfaces to ncrypt.dll and AttestationClientLib.dll - CngKeyGuard.java: CNG key ops (KeyGuard > Hardware fallback), matches msal-go cng_windows.go - CngRsaPrivateKey.java: RSAPrivateKey backed by NCRYPT_KEY_HANDLE (non-exportable) - CngSignatureSpi.java: SignatureSpi routing NCryptSignHash; delegates to next provider for regular keys - CngProvider.java: java.security.Provider registering SHA256withRSA/SHA1withRSA/RSASSA-PSS - ImdsV2Client.java: IMDS /getplatformmetadata and /issuecredential HTTP client - Pkcs10Builder.java: pure-Java PKCS#10 CSR DER encoding matching msal-go generateCSR() - MtlsBindingCertManager.java: orchestrates IMDS flow with in-process cache - MtlsMsiClient.java: rewritten to use JNA+JSSE, no subprocess or .NET runtime required - Fix extractString() in ImdsV2Client to use sequential escape processing - Fix CngSignatureSpi.engineSetParameter to forward PSSParameterSpec to delegate - Remove MtlsMsiHelperLocator.java and MsalMtlsMsiHelper.exe (subprocess artifacts) - Rewrite msal4j-mtls-extensions README.md to match msal-go README style - Add unit tests (56 passing): - ImdsV2ClientTest: extractString() edge cases, PlatformMetadata.cuIdString(), CredentialResponse - Pkcs10BuilderTest: DER primitives, full CSR generation (CngKeyGuard.signPss mocked) - MtlsMsiClientTest: null resource validation, buildTokenUrl, buildTokenRequestBody - MtlsBindingInfoTest: 5-minute early-expiry logic - CngProviderTest: provider registration, installIfAbsent() idempotency - CngSignatureSpiTest: SHA256withRSA/SHA1withRSA/RSASSA-PSS delegation to SunRsaSign - Add maven-surefire-plugin 3.1.2 with JVM args for Mockito inline mocking on Java 21 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
msal4j-mtls-extensions/src/main/java/com/microsoft/aad/msal4j/mtls/MtlsMsiClient.java
Fixed
Show fixed
Hide fixed
Bugs fixed during manual path 2 (Managed Identity mTLS PoP) testing:
1. AttestationLibrary.java: DLL requires non-null log function pointer
- Added LogCallback JNA Callback interface with NOOP_LOG static instance
- Changed logFunc field type from Pointer to LogCallback
- Without this fix: InitAttestationLib returns 0xFFFFFFF8 (invalid args)
2. Pkcs10Builder.java: CSR attributes tag must be 0xA0 (constructed)
- Changed contextImplicit(0, attrSeq) -> contextExplicit(0, attrSeq)
- PKCS#10 [0] IMPLICIT Attributes uses 0xA0 (context + constructed), not 0x80
- Without this fix: IMDS returns HTTP 400 'CSR is invalid'
3. CngSignatureSpi.java: null guard and provider interaction fixes
- engineInitVerify throws InvalidKeyException so chooseProvider() skips
our SPI for verification (server cert, signature checks); SunRsaSign
handles those correctly without CNG involvement
- Added null guards to engineUpdate overloads
- engineInitSign converts IllegalStateException -> InvalidKeyException
4. pom.xml: Added build-helper-maven-plugin (e2e sources) and
maven-shade-plugin (fat JAR with signature file stripping)
5. e2e test driver: Path2ManagedIdentity.java added
- Mirrors msal-go path2_managedidentity/main.go
- Tests first call (full IMDS flow), second call (cert cache), downstream
End-to-end test results on Azure VM (centraluseuap):
- AttestationClientLib.dll: initialized and attestation token obtained
- IMDS /issuecredential: binding cert issued (HTTP 200)
- mTLS handshake to centraluseuap.mtlsauth.microsoft.com: SUCCESS
- AAD token endpoint: AADSTS392196 (tenant config, same as MSAL.NET)
The AADSTS392196 error is environment-specific (resource not configured for
certificate-bound tokens in this tenant), not a code bug. Behavior matches
MSAL.NET reference implementation exactly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ntless check - Add Path1ConfidentialClient.java: mirrors msal-go path1_confidential/main.go - 4 error cases: missing region, /common, /organizations, secret credential - Happy path: acquire mTLS PoP token, print binding cert, cache check, downstream call - PEM loading with PKCS#1/PKCS#8 auto-detect + bundled test cert fallback - Add E2ETestRunner.java: dispatcher; routes path1/path2 args to test drivers - Update Path2ManagedIdentity.java: add static run() method for dispatcher - Update pom.xml: mainClass → E2ETestRunner, add e2e resources dir - Fix AADAuthority.isTenantless: now true for both 'common' and 'organizations' (was only 'common') so validateMtlsPopParameters catches /organizations correctly - Add mtls-test-cert.p12 as bundled e2e resource (no PEM files required for error tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
management.azure.com is not enrolled for mTLS PoP in this tenant; graph.microsoft.com is. Mirrors msal-go's path2_managedidentity/main.go which uses https://graph.microsoft.com as the resource. Verified: full flow succeeds — binding cert received, mTLS PoP token issued, cert cache working on second call, downstream TLS handshake OK. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add appid, app_displayname, idtyp, appidacr, aud, xms_tbflags claims to summary output. Print raw JWT after summary for verification. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- msal4j-sdk/docs/mtls-pop.md: - Path 2: replace subprocess/.NET description with JNA/CNG description - Remove 'Why Java Cannot Use CNG' section (no longer true) - Remove .NET 8 runtime from requirements - Update ManagedIdentityParameters API table description - msal4j-sdk/docs/mtls-pop-manual-testing.md: - Path 2: remove .NET helper exe smoke-test, .NET runtime prereq - Replace with fat JAR e2e runner (path2 --attest) - Show expected output including JWT claims and downstream 401 - Change resource from management.azure.com to graph.microsoft.com - Add resource enrollment note (AADSTS392196) - Update troubleshooting table - msal4j-mtls-extensions/README.md: - Fix broken quick links (docs are in msal4j-sdk/docs/, not here) - Change resource examples from management.azure.com to graph.microsoft.com - Add resource enrollment note - Add Path 1 (Confidential Client) quick start section - Add e2e test driver section with path1/path2 commands Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…omparison table - Fix Path 1 API calls: ClientCredentialFactory.createFromCertificate + withMtlsProofOfPossession() (no-arg) - Add cross-SDK comparison table (msal-java vs dotnet vs go vs node) to mtls-pop.md - Update mtls-pop-manual-testing.md: fat JAR e2e runner replaces .NET helper smoke-test - Add mtls-pop-architecture.md: JNA/CNG design, sequence diagrams, key level fallback, cert cache - Link architecture doc from mtls-pop.md references and mtls-extensions README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove TRUST_ALL X509TrustManager that accepted any server certificate - buildSslSocketFactory now throws MtlsMsiException if insecure=true is passed - Pass null TrustManagers to SSLContext.init() so JVM default trust store is used - Remove unused TrustManager and X509TrustManager imports Resolves GitHub Advanced Security CodeQL alert: 'TrustManager that accepts all certificates' (High) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
mTLS Proof of Possession (mTLS PoP)
Implements mTLS Proof of Possession token acquisition for two scenarios, matching MSAL.NET's mTLS PoP implementation:
Path 1 — Confidential Client
New option:
ClientCredentialParameters.withMtlsProofOfPossession()onacquireToken.The caller provides a certificate (PKCS12, PEM, or hardware-backed PKCS11) as the client credential. MSAL builds a custom
SSLSocketFactoryand presents the certificate during the TLS handshake to the regional mTLS endpoint — noclient_assertionJWT is included in the request body. The token is cached and discriminated by the certificate'sx5t#S256thumbprint so different certificates never share cache entries.Path 2 — Managed Identity (IMDSv2)
New class:
MtlsMsiClientin themsal4j-mtls-extensionsmodule.Fully automated — no certificates or keys to manage. MSAL handles the complete flow via JNA (
ncrypt.dll) directly from the JVM:GET /metadata/identity/getplatformmetadata→clientId,tenantId,cuId,attestationEndpointNCryptCreatePersistedKey— 3-level fallback: KeyGuard (VBS) → Hardware (Software KSP) → InMemoryCsr.Generate():CN={clientId} DC={tenantId}, RSASSA-PSS SHA-256,CuIDOID1.3.6.1.4.1.311.90.2.10inattributes [0]AttestationClientLib.dll→ MAA JWT proving KeyGuard key protection (Trusted Launch VMs only)POST /metadata/identity/issuecredential→ binding cert frommanagedidentitysnissuer.login.microsoft.comPOST {mtlsEndpoint}/{tenantId}/oauth2/v2.0/token— TLS handshake viaCngSignatureSpi(key never leaves CNG)Caching
token_type=mtls_pop+ certx5t#S256token_type=mtls_pop+ certx5t#S256MSALMtlsKey_{cuId}(Software KSP, USER scope)Requirements
Note
https://management.azure.commay returnAADSTS392196(resource not enrolled for mTLS PoP). Usehttps://graph.microsoft.comfor testing.Verified on
Important
Tested on MSIV2 (CentralUSEUAP, System-Assigned MI, Trusted Launch, Credential Guard active) — VBS KeyGuard-protected RSA-2048 — resource:
https://graph.microsoft.comDocumentation
msal4j-sdk/docs/mtls-pop.mdmsal4j-sdk/docs/mtls-pop-architecture.mdCngSignatureSpidesign, 3-level key fallback,AttestationClientLib.dllusage, pure-Java PKCS#10 ASN.1, cert cache, cross-SDK architecture comparisonmsal4j-sdk/docs/mtls-pop-manual-testing.mdPath1ConfidentialClient.java--errors-onlyworks with zero setup)Path2ManagedIdentity.javaE2ETestRunner.javajava -jar mtls-extensions.jar path1|path2 [--errors-only]