Skip to content

Commit d4f58ec

Browse files
authored
Add documentation for MSI v2 mTLS in Python (#904)
Document the MSI v2 mTLS flow in Python, detailing the problem, solution, and technical findings related to using KeyGuard-protected certificates.
1 parent 5866feb commit d4f58ec

1 file changed

Lines changed: 97 additions & 0 deletions

File tree

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)