Skip to content

Commit 59cd104

Browse files
feat: implement mTLS Proof-of-Possession (mTLS PoP) token acquisition
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>
1 parent eb4d2d4 commit 59cd104

31 files changed

Lines changed: 2402 additions & 11 deletions

msal4j-mtls-extensions/README.md

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# msal4j-mtls-extensions
2+
3+
Provides Managed Identity mTLS Proof-of-Possession (mTLS PoP) support for `msal4j`. This module handles the acquisition of `mtls_pop` tokens using IMDS-issued, KeyGuard-backed certificates on Azure VMs.
4+
5+
> **Platform**: Windows only (requires .NET 8 runtime for the bundled `MsalMtlsMsiHelper.exe`)
6+
7+
---
8+
9+
## Why a Separate Module?
10+
11+
KeyGuard keys — hardware-isolated private keys used by Managed Identity mTLS PoP — are created via Windows CNG (`NCryptCreatePersistedKey` with `NCRYPT_VBS_KEYISOLATION_FLAG`). Java's Windows TLS integration (`SunMSCAPI`) uses the legacy CAPI subsystem and has no path to CNG. This is the same fundamental limitation as Node.js/OpenSSL.
12+
13+
The solution is a subprocess model: this module bundles `MsalMtlsMsiHelper.exe`, a .NET 8 binary that uses Schannel and CNG natively to handle the full KeyGuard certificate lifecycle. `msal4j-sdk` delegates to this binary when `withMtlsProofOfPossession(true)` is set on `ManagedIdentityParameters`.
14+
15+
For a detailed technical analysis, see [keyguard-jvm-analysis.md](../msal4j-sdk/docs/keyguard-jvm-analysis.md).
16+
17+
---
18+
19+
## Installation
20+
21+
Add to your `pom.xml`:
22+
23+
```xml
24+
<dependency>
25+
<groupId>com.microsoft.azure</groupId>
26+
<artifactId>msal4j-mtls-extensions</artifactId>
27+
<version>1.24.0</version>
28+
</dependency>
29+
```
30+
31+
The extension is a standalone artifact — you do **not** need to depend on `msal4j` separately; `msal4j-mtls-extensions` already depends on it transitively.
32+
33+
---
34+
35+
## Requirements
36+
37+
| Requirement | Details |
38+
|-------------|---------|
39+
| OS | Windows (the bundled binary is Windows x64) |
40+
| .NET runtime | .NET 8.x (`dotnet --version` must print `8.x.x`) |
41+
| Azure environment | VM, App Service, Azure Functions, or any managed-identity-enabled resource |
42+
| Managed identity | System-assigned or user-assigned identity must be enabled |
43+
44+
---
45+
46+
## Usage
47+
48+
### System-Assigned Managed Identity
49+
50+
```java
51+
import com.microsoft.aad.msal4j.*;
52+
53+
ManagedIdentityApplication app = ManagedIdentityApplication
54+
.builder(ManagedIdentityId.systemAssigned())
55+
.build();
56+
57+
ManagedIdentityParameters params = ManagedIdentityParameters
58+
.builder("https://management.azure.com/")
59+
.withMtlsProofOfPossession(true)
60+
.build();
61+
62+
IAuthenticationResult result = app.acquireTokenForManagedIdentity(params).get();
63+
String token = result.accessToken(); // mtls_pop token
64+
```
65+
66+
### User-Assigned Managed Identity
67+
68+
```java
69+
// By client ID
70+
ManagedIdentityApplication app = ManagedIdentityApplication
71+
.builder(ManagedIdentityId.userAssignedClientId("your-client-id"))
72+
.build();
73+
74+
// By object ID
75+
ManagedIdentityApplication app2 = ManagedIdentityApplication
76+
.builder(ManagedIdentityId.userAssignedObjectId("your-object-id"))
77+
.build();
78+
```
79+
80+
### With MAA Attestation
81+
82+
MAA (Microsoft Azure Attestation) attestation provides cryptographic proof that the key was created in a VBS-isolated enclave. Requires Trusted Launch or Confidential VM.
83+
84+
```java
85+
ManagedIdentityParameters params = ManagedIdentityParameters
86+
.builder("https://graph.microsoft.com/")
87+
.withMtlsProofOfPossession(true)
88+
.withAttestation(true)
89+
.build();
90+
```
91+
92+
---
93+
94+
## API Reference
95+
96+
### `MtlsMsiClient`
97+
98+
Main entry point for the subprocess wrapper (used internally by `msal4j-sdk` via reflection).
99+
100+
```java
101+
package com.microsoft.aad.msal4j.mtls;
102+
103+
public class MtlsMsiClient {
104+
// Acquire an mtls_pop token via MsalMtlsMsiHelper.exe
105+
public MtlsMsiHelperResult acquireToken(
106+
String resource,
107+
String identityType, // "SystemAssigned" | "UserAssignedClientId" | "UserAssignedObjectId" | "UserAssignedResourceId"
108+
String identityId, // null for SystemAssigned
109+
boolean withAttestation,
110+
String correlationId
111+
) throws MtlsMsiException;
112+
113+
// Make an mTLS-authenticated HTTP request via the helper
114+
public MtlsMsiHttpResponse httpRequest(
115+
String url,
116+
String method,
117+
String token,
118+
Map<String, String> headers,
119+
String body
120+
) throws MtlsMsiException;
121+
}
122+
```
123+
124+
### `MtlsMsiHelperResult`
125+
126+
Result of a successful `acquireToken` call.
127+
128+
| Field | Type | Description |
129+
|-------|------|-------------|
130+
| `accessToken` | `String` | The `mtls_pop` access token |
131+
| `tokenType` | `String` | Always `"mtls_pop"` |
132+
| `expiresOn` | `long` | Expiry as Unix timestamp (seconds) |
133+
| `thumbprint` | `String` | x5t#S256 Base64URL thumbprint of binding cert |
134+
135+
### `MtlsMsiException`
136+
137+
Thrown when the helper subprocess fails. Wraps the error JSON from the helper's stderr:
138+
139+
```json
140+
{ "error": "...", "error_description": "..." }
141+
```
142+
143+
---
144+
145+
## How It Works
146+
147+
```
148+
Java App
149+
150+
▼ acquireTokenForManagedIdentity(params) [withMtlsProofOfPossession=true]
151+
ManagedIdentityApplication (msal4j-sdk)
152+
153+
▼ reflection → MtlsMsiClient (msal4j-mtls-extensions)
154+
MtlsMsiClient
155+
156+
▼ spawn subprocess
157+
MsalMtlsMsiHelper.exe (.NET 8)
158+
├── IMDS getplatformmetadata
159+
├── NCryptCreatePersistedKey (CNG + NCRYPT_VBS_KEYISOLATION_FLAG)
160+
├── Generate CSR
161+
├── [optional] MAA attestation
162+
├── IMDS /issuecredential → receives KeyGuard-backed X509 cert
163+
├── mTLS token request to mtlsauth.microsoft.com (Schannel + NCRYPT_KEY_HANDLE)
164+
└── stdout: JSON {access_token, token_type, expires_on, thumbprint}
165+
166+
▼ parse JSON, build AuthenticationResult
167+
Java App receives IAuthenticationResult
168+
```
169+
170+
---
171+
172+
## Helper Binary Location
173+
174+
The `MsalMtlsMsiHelper.exe` binary is bundled in the JAR at `resources/MsalMtlsMsiHelper.exe`. At runtime, `MtlsMsiHelperLocator` extracts it to a temp directory on first use.
175+
176+
**Override**: Set the `MSAL_MTLS_HELPER_PATH` environment variable to an absolute path to use a custom or pre-extracted binary:
177+
178+
```bash
179+
set MSAL_MTLS_HELPER_PATH=C:\custom\path\MsalMtlsMsiHelper.exe
180+
```
181+
182+
This is useful for:
183+
- Air-gapped environments where JAR extraction is restricted
184+
- Testing with a debug build of the helper
185+
- Pre-extracting the binary to a known location as part of VM provisioning
186+
187+
---
188+
189+
## Building `MsalMtlsMsiHelper.exe` from Source
190+
191+
The binary is built from the msaljs `msal-node-mtls-extensions` project:
192+
193+
```
194+
C:\Projects\msaljs\extensions\msal-node-mtls-extensions\native\MsalMtlsMsiHelper\
195+
```
196+
197+
Build (framework-dependent, requires .NET 8 SDK):
198+
199+
```bash
200+
cd MsalMtlsMsiHelper
201+
dotnet publish -r win-x64 --self-contained false -o publish/
202+
# Output: publish/MsalMtlsMsiHelper.exe (≈1.4 MB)
203+
```
204+
205+
For self-contained (no .NET runtime required on target):
206+
207+
```bash
208+
dotnet publish -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish/
209+
# Output: publish/MsalMtlsMsiHelper.exe (≈65 MB)
210+
```
211+
212+
---
213+
214+
## Token Caching
215+
216+
mTLS PoP tokens are cached in the in-memory token cache with credential type `AccessToken_With_AuthScheme` and a `keyId` segment (the x5t#S256 thumbprint). Cache entries do not conflict with Bearer tokens for the same scope.
217+
218+
Cache TTL matches the `expires_in` returned by AAD (typically 1 hour). Expired tokens trigger a new subprocess invocation.
219+
220+
---
221+
222+
## Support and Servicing
223+
224+
This module follows the same support lifecycle as `msal4j`. File issues at [GitHub Issues](https://github.com/AzureAD/microsoft-authentication-library-for-java/issues).
225+
226+
**Windows only**: The bundled `MsalMtlsMsiHelper.exe` is a Windows x64 binary. Linux/macOS Azure environments that support Managed Identity can use the standard Bearer token flow via `ManagedIdentityApplication` without this extension.

msal4j-mtls-extensions/pom.xml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>com.microsoft.azure</groupId>
8+
<artifactId>msal4j-mtls-extensions</artifactId>
9+
<version>1.0.0</version>
10+
<packaging>jar</packaging>
11+
12+
<name>Microsoft Authentication Library for Java - mTLS Extensions</name>
13+
<description>
14+
Extension package that enables mTLS Proof-of-Possession (mTLS PoP) token acquisition
15+
for Azure Managed Identity scenarios requiring KeyGuard-bound certificates. Delegates
16+
key creation, CSR generation, MAA attestation, and IMDS credential issuance to a
17+
.NET 8 subprocess (MsalMtlsMsiHelper.exe) because Java's JSSE/SunMSCAPI stack uses
18+
the legacy Windows CryptoAPI (CAPI), not CNG, and therefore cannot create or use
19+
KeyGuard keys (which require NCryptCreatePersistedKey with CNG-only VBS isolation flags).
20+
</description>
21+
22+
<properties>
23+
<maven.compiler.source>8</maven.compiler.source>
24+
<maven.compiler.target>8</maven.compiler.target>
25+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
26+
</properties>
27+
28+
<dependencies>
29+
<dependency>
30+
<groupId>com.microsoft.azure</groupId>
31+
<artifactId>msal4j</artifactId>
32+
<version>1.23.1</version>
33+
</dependency>
34+
35+
<!-- Test dependencies -->
36+
<dependency>
37+
<groupId>org.junit.jupiter</groupId>
38+
<artifactId>junit-jupiter-api</artifactId>
39+
<version>5.10.0</version>
40+
<scope>test</scope>
41+
</dependency>
42+
<dependency>
43+
<groupId>org.junit.jupiter</groupId>
44+
<artifactId>junit-jupiter-engine</artifactId>
45+
<version>5.10.0</version>
46+
<scope>test</scope>
47+
</dependency>
48+
<dependency>
49+
<groupId>org.mockito</groupId>
50+
<artifactId>mockito-core</artifactId>
51+
<version>5.4.0</version>
52+
<scope>test</scope>
53+
</dependency>
54+
<dependency>
55+
<groupId>org.mockito</groupId>
56+
<artifactId>mockito-junit-jupiter</artifactId>
57+
<version>5.4.0</version>
58+
<scope>test</scope>
59+
</dependency>
60+
</dependencies>
61+
</project>

0 commit comments

Comments
 (0)