|
| 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/) |
0 commit comments