Skip to content

Commit b62934a

Browse files
Add mTLS PoP architecture doc, fix API calls in docs, add cross-SDK comparison 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>
1 parent a7cb5bd commit b62934a

4 files changed

Lines changed: 190 additions & 20 deletions

File tree

msal4j-mtls-extensions/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ The latest code resides in the `dev` branch.
66

77
Quick links:
88

9-
| [Docs](../../msal4j-sdk/docs/mtls-pop.md) | [Manual Testing](../../msal4j-sdk/docs/mtls-pop-manual-testing.md) | [Support](README.md#community-help-and-support) |
10-
| --- | --- | --- |
9+
| [Docs](../../msal4j-sdk/docs/mtls-pop.md) | [Manual Testing](../../msal4j-sdk/docs/mtls-pop-manual-testing.md) | [Architecture](../../msal4j-sdk/docs/mtls-pop-architecture.md) | [Support](README.md#community-help-and-support) |
10+
| --- | --- | --- | --- |
1111

1212
## Installation
1313

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# mTLS PoP Architecture — Deep Dive
2+
3+
This document describes the internal architecture of the mTLS Proof of Possession implementation in MSAL4J. For the user-facing API guide, see [mtls-pop.md](mtls-pop.md).
4+
5+
---
6+
7+
## Flow Diagrams
8+
9+
### Path 1 — Confidential Client (SNI Certificate)
10+
11+
```mermaid
12+
sequenceDiagram
13+
participant App
14+
participant MSAL as MSAL4J
15+
participant mtlsauth as {region}.mtlsauth.microsoft.com
16+
17+
App->>MSAL: acquireToken(withMtlsProofOfPossession())
18+
MSAL->>MSAL: Resolve region → build mTLS endpoint URL
19+
MSAL->>MSAL: MtlsSslContextHelper.createSslSocketFactory(key, cert)
20+
MSAL->>mtlsauth: POST /{tenant}/oauth2/v2.0/token<br/>(TLS handshake with caller cert — no client_assertion)
21+
mtlsauth-->>MSAL: token_type=mtls_pop, access_token
22+
MSAL-->>App: IAuthenticationResult{accessToken, bindingCertificate}
23+
Note over App: Subsequent calls → TokenSource=Cache
24+
```
25+
26+
### Path 2 — Managed Identity (IMDSv2)
27+
28+
```mermaid
29+
sequenceDiagram
30+
participant App
31+
participant Ext as MtlsMsiClient (msal4j-mtls-extensions)
32+
participant IMDS as IMDS (169.254.169.254)
33+
participant CNG as Windows CNG via JNA (ncrypt.dll)
34+
participant Attest as AttestationClientLib.dll → MAA
35+
participant Token as mTLS Token Endpoint
36+
37+
App->>Ext: acquireToken(resource, "SystemAssigned", withAttestation)
38+
Ext->>IMDS: GET /metadata/identity/getplatformmetadata
39+
IMDS-->>Ext: clientID, tenantID, cuID, attestationEndpoint
40+
Ext->>CNG: GetOrCreateManagedIdentityKey(MSALMtlsKey_{cuID})
41+
Note over CNG: KeyGuard (VBS) → Hardware → InMemory
42+
CNG-->>Ext: RSA-2048 key handle (CngKey)
43+
Ext->>Ext: Build PKCS#10 CSR (Pkcs10Builder via JNA)
44+
Ext->>Attest: AttestKeyGuardImportKey(attestationEndpoint, keyHandle)
45+
Attest-->>Ext: MAA JWT (proves VBS KeyGuard protection)
46+
Ext->>IMDS: POST /metadata/identity/issuecredential {csr, attestation_token}
47+
IMDS-->>Ext: binding_certificate + mtls_authentication_endpoint
48+
Ext->>Ext: Cache binding cert (expires 5 min before NotAfter)
49+
Ext->>Token: POST /{tenant}/oauth2/v2.0/token<br/>(TLS handshake with binding cert via CngSignatureSpi)
50+
Token-->>Ext: token_type=mtls_pop, access_token
51+
Ext-->>App: MtlsMsiHelperResult{accessToken, bindingCertificate}
52+
Note over App: Subsequent calls → cert cache hit, then token cache hit
53+
```
54+
55+
---
56+
57+
## 1. How Java Uses Windows CNG Without JNI Headers
58+
59+
Java has no built-in C FFI, but [JNA (Java Native Access)](https://github.com/java-native-access/jna) provides dynamic binding to native DLLs using pure Java interfaces — no C headers, no `javah`, no native compilation step beyond the DLL itself.
60+
61+
```java
62+
// JNA interface — maps directly to ncrypt.dll exports
63+
interface NCrypt extends Library {
64+
int NCryptOpenStorageProvider(PointerByReference phProvider, String pszProviderName, int dwFlags);
65+
int NCryptCreatePersistedKey(Pointer hProvider, PointerByReference phKey,
66+
String pszAlgId, String pszKeyName, int dwLegacyKeySpec, int dwFlags);
67+
int NCryptSetProperty(Pointer hObject, String pszProperty, byte[] pbInput, int cbInput, int dwFlags);
68+
int NCryptFinalizeKey(Pointer hKey, int dwFlags);
69+
int NCryptSignHash(Pointer hKey, Pointer pPaddingInfo, byte[] pbHashValue, int cbHashValue,
70+
byte[] pbSignature, int cbSignature, PointerByReference pcbResult, int dwFlags);
71+
}
72+
```
73+
74+
The key flag that enables KeyGuard VBS isolation:
75+
```java
76+
private static final int NCRYPT_VBS_KEYISOLATION_FLAG = 0x00010000;
77+
NCrypt.INSTANCE.NCryptFinalizeKey(hKey, NCRYPT_VBS_KEYISOLATION_FLAG);
78+
```
79+
80+
This is the same flag used by msal-dotnet (via `CngKey`) and msal-go (via `syscall.NewLazyDLL`).
81+
82+
---
83+
84+
## 2. Custom `java.security.Provider` for CNG-Backed TLS
85+
86+
Java's JSSE TLS stack calls `java.security.Signature` for the TLS `CertificateVerify` handshake message. A standard Java `PrivateKey` from `SunMSCAPI` cannot wrap a CNG KeyGuard key handle.
87+
88+
The solution: a custom `java.security.Provider` (`CngProvider`) that registers `CngSignatureSpi` — a `Signature` implementation that delegates signing to `NCryptSignHash` via JNA, keeping the key handle inside the VBS enclave.
89+
90+
```
91+
JSSE TLS handshake
92+
└─► KeyManager.getPrivateKey() → returns CngPrivateKey (opaque handle)
93+
└─► Signature.getInstance("SHA256withRSA", CngProvider)
94+
└─► CngSignatureSpi.engineInitSign(CngPrivateKey)
95+
└─► CngSignatureSpi.engineSign()
96+
└─► NCryptSignHash(hKey, BCRYPT_PKCS1_PADDING, hash, ...) via JNA
97+
└─► ncrypt.dll (in-process, VBS KeyGuard boundary)
98+
```
99+
100+
`engineInitVerify` throws `InvalidKeyException` intentionally — this causes JSSE's provider selection to fall through to `SunRsaSign`, which handles server certificate verification correctly. `CngSignatureSpi` only intercepts signing operations with the KeyGuard key.
101+
102+
---
103+
104+
## 3. Certificate Caching
105+
106+
The binding certificate (issued by `managedidentitysnissuer.login.microsoft.com`) is cached in-memory with a 5-minute pre-expiry buffer:
107+
108+
```
109+
certCache key: cuID (compute unit ID from IMDS platform metadata)
110+
certCache value: {bindingCert, expiry = cert.NotAfter - 5min}
111+
```
112+
113+
The CNG key is persisted in the Microsoft Software Key Storage Provider under the name `MSALMtlsKey_{cuID}` (user scope). On subsequent calls, the key is opened with `NCryptOpenKey` rather than re-created, ensuring the same public key is presented in the CSR and that the cached binding certificate remains valid.
114+
115+
---
116+
117+
## 4. Cross-SDK Architecture Comparison
118+
119+
| Concern | msal-java | msal-dotnet | msal-go | msal-node |
120+
|---------|-----------|-------------|---------|-----------|
121+
| CNG key creation | JNA → `ncrypt.dll` | `CngKey` (.NET) | `syscall.NewLazyDLL` | Subprocess (exe) |
122+
| TLS with CNG key | `CngSignatureSpi` + JSSE | Schannel (`NCRYPT_KEY_HANDLE`) | `crypto.Signer` interface | .NET subprocess |
123+
| CSR generation | `Pkcs10Builder` (pure Java ASN.1) | `CertificateRequest` (.NET) | `encoding/asn1` (Go stdlib) | Subprocess |
124+
| Attestation | JNA → `AttestationClientLib.dll` | Native NuGet package | `syscall` → DLL | Subprocess |
125+
| In-process |||||
126+
| .NET required || ✅ (runtime) || ✅ (subprocess) |
127+
128+
---
129+
130+
## 5. Why Path 1 Does Not Need JNA
131+
132+
Path 1 (SNI / Confidential Client) uses a certificate the caller already owns — typically loaded from a PKCS12 file or PKCS11 hardware token. Java's standard `KeyManagerFactory` and JSSE handle this transparently. The custom `SSLSocketFactory` built by `MtlsSslContextHelper` sets up the client certificate for the TLS handshake — no CNG involved.
133+
134+
---
135+
136+
## 6. Key Source Names
137+
138+
| Key Source | Description |
139+
|------------|-------------|
140+
| `KeyGuard` | Full VBS isolation — requires Credential Guard running |
141+
| `Hardware` | TPM-backed but not VBS-isolated |
142+
| `InMemory` | Software key — no hardware protection |
143+
144+
For production use, `KeyGuard` is required (`xms_tbflags: 2` in the token). `Hardware` or `InMemory` keys will result in `AADSTS392196` or similar errors from AAD.
145+
146+
---
147+
148+
## References
149+
150+
- [mTLS PoP API Guide](mtls-pop.md)
151+
- [mTLS PoP Manual Testing](mtls-pop-manual-testing.md)
152+
- [RFC 8705 — OAuth 2.0 Mutual-TLS Client Authentication](https://www.rfc-editor.org/rfc/rfc8705)
153+
- [JNA (Java Native Access)](https://github.com/java-native-access/jna)
154+
- [NCrypt API (MSDN)](https://docs.microsoft.com/en-us/windows/win32/api/ncrypt/)

msal4j-sdk/docs/mtls-pop-manual-testing.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ import java.util.*;
6464

6565
public class TestMtlsPop {
6666
public static void main(String[] args) throws Exception {
67-
// Load certificate
68-
InputStream certStream = new FileInputStream("test-cert.p12");
69-
ClientCertificate cert = ClientCertificate.create(certStream, "changeit");
67+
// Load certificate (PKCS12)
68+
IClientCertificate cert = ClientCredentialFactory.createFromCertificate(
69+
new FileInputStream("test-cert.p12"), "changeit");
7070

71-
// Build app
71+
// Build app — tenanted authority and region required
7272
ConfidentialClientApplication app = ConfidentialClientApplication
7373
.builder("your-client-id", cert)
7474
.authority("https://login.microsoftonline.com/your-tenant-id")
@@ -79,7 +79,7 @@ public class TestMtlsPop {
7979
Set<String> scopes = Collections.singleton("https://graph.microsoft.com/.default");
8080
ClientCredentialParameters params = ClientCredentialParameters
8181
.builder(scopes)
82-
.withMtlsProofOfPossession(true)
82+
.withMtlsProofOfPossession()
8383
.build();
8484

8585
IAuthenticationResult result = app.acquireToken(params).get();
@@ -108,8 +108,8 @@ Access token: eyJ0eXAiOiJKV1QiLCJub25jZSI6...
108108
Call `acquireToken` again immediately — the second call should not make a network request:
109109

110110
```java
111-
long t0 = System.currentTimeMillis();
112111
IAuthenticationResult r1 = app.acquireToken(params).get();
112+
long t0 = System.currentTimeMillis();
113113
IAuthenticationResult r2 = app.acquireToken(params).get();
114114
System.out.println("Same token: " + r1.accessToken().equals(r2.accessToken())); // true
115115
System.out.println("Elapsed: " + (System.currentTimeMillis() - t0) + "ms"); // should be <50ms
@@ -277,7 +277,7 @@ IAuthenticationResult bearerResult = app.acquireToken(bearerParams).get();
277277
// Acquire mTLS PoP token
278278
ClientCredentialParameters mtlsParams = ClientCredentialParameters
279279
.builder(scopes)
280-
.withMtlsProofOfPossession(true)
280+
.withMtlsProofOfPossession()
281281
.build();
282282
IAuthenticationResult mtlsResult = app.acquireToken(mtlsParams).get();
283283

msal4j-sdk/docs/mtls-pop.md

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,20 @@ MSAL4J supports two mTLS PoP acquisition paths:
1111
| Path | Application Type | Certificate Source | Attestation |
1212
|------|-----------------|-------------------|-------------|
1313
| **SNI (Subject Name Indication)** | `ConfidentialClientApplication` | Any PKCS12/PEM cert or hardware token (PKCS11) | Not required |
14-
| **Managed Identity** | `ManagedIdentityApplication` | IMDS-issued KeyGuard-backed certificate | Optional (IMDS-attested) |
14+
| **Managed Identity** | `MtlsMsiClient` (via `msal4j-mtls-extensions`) | IMDS-issued KeyGuard-backed certificate | Optional (Trusted Launch VMs) |
15+
16+
---
17+
18+
## Cross-SDK Implementation Comparison
19+
20+
| Library | TLS Stack | CNG Support | Approach |
21+
|---------|-----------|-------------|----------|
22+
| **msal-java** | JSSE + custom `SSLSocketFactory` (Path 1); JNA → `ncrypt.dll` (Path 2) | ✅ Via JNA | In-process |
23+
| **msal-dotnet** | Schannel (.NET) | ✅ Native | In-process |
24+
| **msal-go** | `crypto/tls` (pure Go) | ✅ Via `crypto.Signer` | In-process |
25+
| **msal-node** | OpenSSL (Node.js) | ❌ None | .NET subprocess (`MsalMtlsMsiHelper.exe`) |
26+
27+
No subprocess is needed in msal-java.
1528

1629
---
1730

@@ -52,22 +65,26 @@ This makes mTLS PoP suitable for high-value API access from server-side applicat
5265
### Quick Start
5366

5467
```java
55-
// 1. Load your certificate
56-
InputStream certStream = new FileInputStream("/path/to/cert.p12");
57-
ClientCertificate cert = ClientCertificate.create(certStream, "password");
68+
import com.microsoft.aad.msal4j.*;
69+
import java.io.FileInputStream;
70+
import java.util.*;
71+
72+
// 1. Load your certificate (PKCS12)
73+
IClientCertificate cert = ClientCredentialFactory.createFromCertificate(
74+
new FileInputStream("/path/to/cert.p12"), "password");
5875

5976
// 2. Build the app (tenanted authority + region required)
6077
ConfidentialClientApplication app = ConfidentialClientApplication
6178
.builder("your-client-id", cert)
6279
.authority("https://login.microsoftonline.com/your-tenant-id")
63-
.azureRegion("eastus") // or AzureRegion.AUTO_DISCOVER_REGION
80+
.azureRegion("eastus") // or autoDetectRegion(true)
6481
.build();
6582

6683
// 3. Request an mTLS PoP token
6784
Set<String> scopes = Collections.singleton("https://graph.microsoft.com/.default");
6885
ClientCredentialParameters params = ClientCredentialParameters
6986
.builder(scopes)
70-
.withMtlsProofOfPossession(true)
87+
.withMtlsProofOfPossession()
7188
.build();
7289

7390
IAuthenticationResult result = app.acquireToken(params).get();
@@ -87,13 +104,11 @@ KeyStore ks = KeyStore.getInstance("PKCS11", pkcs11Provider);
87104
ks.load(null, "pin".toCharArray());
88105

89106
PrivateKey privateKey = (PrivateKey) ks.getKey("my-key-alias", null);
90-
X509Certificate[] certChain = ...; // from ks.getCertificateChain()
107+
X509Certificate cert = (X509Certificate) ks.getCertificate("my-key-alias");
91108

92-
ClientCertificate cert = ClientCertificate.create(privateKey, certChain);
109+
IClientCertificate clientCert = ClientCredentialFactory.createFromCertificate(privateKey, cert);
93110
```
94111

95-
The `MtlsSslContextHelper` handles the PKCS12 in-memory KeyStore setup transparently.
96-
97112
### Token Endpoint
98113

99114
For public cloud, MSAL4J constructs:
@@ -183,7 +198,7 @@ ManagedIdentityApplication app = ManagedIdentityApplication
183198

184199
| Method | Description |
185200
|--------|-------------|
186-
| `.withMtlsProofOfPossession(boolean)` | When `true`, acquires an `mtls_pop` token instead of Bearer |
201+
| `.withMtlsProofOfPossession()` | Acquires an `mtls_pop` token instead of Bearer |
187202

188203
### `ManagedIdentityParameters`
189204

@@ -236,6 +251,7 @@ Standard Bearer tokens use a 6-segment key (no `keyId`). The two token types nev
236251
## References
237252

238253
- [mTLS PoP Manual Testing Guide](mtls-pop-manual-testing.md)
254+
- [mTLS PoP Architecture](mtls-pop-architecture.md)
239255
- [msal4j-mtls-extensions README](../../msal4j-mtls-extensions/README.md)
240256
- [MSAL.js mTLS PoP](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8476)
241257
- [MSAL.NET mTLS PoP](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/tree/main/docs)

0 commit comments

Comments
 (0)