Implement mTLS Proof-of-Possession (mTLS PoP) token acquisition#1021
Implement mTLS Proof-of-Possession (mTLS PoP) token acquisition#1021Robbie-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>
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>
|
|
||
| ## 3. Certificate Caching | ||
|
|
||
| The binding certificate (issued by `managedidentitysnissuer.login.microsoft.com`) is cached in-memory with a 5-minute pre-expiry buffer: |
There was a problem hiding this comment.
Minor issue, but 5 minutes seems tight.
Unless this is for something else, .NET seems to have a 24 hour lifetime (but that might be too much) and stores it in the CurrentUser\My keystore (which might not be needed): https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/main/src/client/Microsoft.Identity.Client/ManagedIdentity/V2/WindowsPersistentCertificateCache.cs#L13
|
|
||
| **Java cannot natively create or use KeyGuard keys on Windows.** The root cause is the same as for Node.js/OpenSSL: Java's Windows TLS/crypto integration (`SunMSCAPI`) uses the legacy **CryptoAPI (CAPI)** rather than the modern **CNG (Cryptography API: Next Generation)**. KeyGuard keys are CNG-only. | ||
|
|
||
| The only viable architecture for Managed Identity mTLS PoP in Java is the same subprocess approach used by msal-node: spawning `MsalMtlsMsiHelper.exe` (a .NET 8 binary) which uses Schannel and CNG natively. |
There was a problem hiding this comment.
Is this doc still needed/up-to-date? Here you say that the subprocess process is the only viable approach and some method comments still say that, but newer versions of the docs/code seem to focus on a JNI/JNA approach: https://github.com/AzureAD/microsoft-authentication-library-for-java/pull/1021/changes#diff-b33fca40515fa2d8afea3213b775ae51d79c59a5e5cecd40677934d77478d9daR3
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]