Skip to content

Commit 5c6747d

Browse files
authored
Add documentation for MSI v2 mTLS in Python
Document the MSI v2 mTLS flow in Python, detailing the problem, solution, and technical findings related to using KeyGuard-protected certificates.
1 parent 016092e commit 5c6747d

File tree

1 file changed

+97
-0
lines changed

1 file changed

+97
-0
lines changed

docs/msiv2-keyguard-poc.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# MSI v2 mTLS End-to-End in Python — Summary
2+
3+
## Problem
4+
5+
MSAL Python's MSI v2 flow acquires an `mtls_pop` token bound to a KeyGuard-protected certificate. After token acquisition, the calling app needs to present the **same certificate** over mTLS when calling the resource (e.g., Azure Key Vault).
6+
7+
In .NET, this is transparent — `X509Certificate2` wraps both the cert and the CNG key reference, and `HttpClient` + SChannel handles mTLS natively. The caller just does:
8+
9+
```csharp
10+
var handler = new HttpClientHandler {
11+
ClientCertificates = { result.BindingCertificate }
12+
};
13+
var client = new HttpClient(handler);
14+
```
15+
16+
**Python cannot do this.** Python's HTTP libraries (`requests`, `httpx`, `urllib3`) all use **OpenSSL** for TLS, which requires the private key as exportable bytes. A KeyGuard key is **non-exportable by design** — OpenSSL cannot access it.
17+
18+
## Solution
19+
20+
MSAL Python provides `mtls_http_request()` — a helper that uses **WinHTTP/SChannel** (the Windows-native TLS stack) via ctypes to make mTLS resource calls. SChannel can access the non-exportable KeyGuard key natively, just like .NET does.
21+
22+
```python
23+
from msal.msi_v2 import mtls_http_request
24+
25+
result = client.acquire_token_for_client(
26+
resource="https://vault.azure.net",
27+
mtls_proof_of_possession=True,
28+
with_attestation_support=True,
29+
)
30+
31+
cert_der = base64.b64decode(result["cert_der_b64"])
32+
resp = mtls_http_request(
33+
"GET",
34+
"https://tokenbinding.vault.azure.net/secrets/boundsecret/?api-version=2015-06-01",
35+
cert_der,
36+
headers={
37+
"Authorization": f"{result['token_type']} {result['access_token']}",
38+
"Accept": "application/json",
39+
"x-ms-tokenboundauth": "true",
40+
},
41+
)
42+
```
43+
44+
## Why Python Needs a Helper (and .NET Doesn't)
45+
46+
| | .NET | Python |
47+
|---|---|---|
48+
| TLS stack | SChannel (Windows native) | OpenSSL |
49+
| Can access non-exportable CNG keys | ✅ via `X509Certificate2` | ❌ Not possible |
50+
| Standard HTTP client works for mTLS |`HttpClient` + `ClientCertificates` |`requests`/`httpx` cannot use KeyGuard keys |
51+
| Solution | Platform gives it for free | `mtls_http_request()` bridges the gap via WinHTTP/SChannel |
52+
53+
## Key Technical Findings
54+
55+
During development, three requirements were discovered for mTLS resource calls:
56+
57+
1. **`x-ms-tokenboundauth: true` header** — Required by Azure Key Vault. This header triggers the server to request the client certificate via TLS renegotiation. Without it, the server never asks for the cert. Discovered from the [.NET E2E test](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/main/tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsV2Tests.cs).
58+
59+
2. **HTTP/1.1 (not HTTP/2)** — TLS renegotiation (needed for the server to request the client cert after seeing the header) is forbidden in HTTP/2. HTTP/1.1 must be used for the resource call.
60+
61+
3. **TLS 1.2** — TLS renegotiation for client certificates works reliably in TLS 1.2. TLS 1.3 uses post-handshake authentication which WinHTTP may not fully support.
62+
63+
## Auth Result Fields
64+
65+
```python
66+
{
67+
"access_token": "eyJ...",
68+
"token_type": "mtls_pop",
69+
"expires_in": 86399,
70+
"cert_pem": "-----BEGIN CERTIFICATE-----\n...",
71+
"cert_der_b64": "MIID...",
72+
"cert_thumbprint_sha256": "abc123...",
73+
"key_name": "MsalMsiV2Key_caf87a12-..."
74+
}
75+
```
76+
77+
## Architecture Comparison
78+
79+
```
80+
.NET E2E Flow:
81+
MSAL.NET → mtls_pop token + X509Certificate2 (wraps CNG key)
82+
83+
HttpClient + SChannel → mTLS to Key Vault ← platform-native, no helper needed
84+
85+
Python E2E Flow:
86+
MSAL Python → mtls_pop token + cert PEM/DER (no private key access)
87+
88+
mtls_http_request() → WinHTTP/SChannel via ctypes → mTLS to Key Vault
89+
(helper needed because OpenSSL cannot access non-exportable KeyGuard keys)
90+
```
91+
92+
## Status
93+
94+
- ✅ Token acquisition works (mtls_pop token with KeyGuard attestation)
95+
- ✅ Certificate binding verified (cnf.x5t#S256 matches)
96+
- ✅ mTLS resource call presents certificate (confirmed by matching .NET's error)
97+
- ⏳ AKV end-to-end blocked on service-side issue ("Certificate import or verification failed" — same error in both .NET and Python)

0 commit comments

Comments
 (0)