-
Notifications
You must be signed in to change notification settings - Fork 213
KeyGuard POC - Add documentation for MSI v2 mTLS in Python #904
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
gladjohn
wants to merge
1
commit into
dev
Choose a base branch
from
gladjohn-patch-1
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+97
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| # MSI v2 mTLS End-to-End in Python — Summary | ||
|
|
||
| ## Problem | ||
|
|
||
| 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). | ||
|
|
||
| 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: | ||
|
|
||
| ```csharp | ||
| var handler = new HttpClientHandler { | ||
| ClientCertificates = { result.BindingCertificate } | ||
| }; | ||
| var client = new HttpClient(handler); | ||
| ``` | ||
|
|
||
| **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. | ||
|
|
||
| ## Solution | ||
|
|
||
| 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. | ||
|
|
||
| ```python | ||
| from msal.msi_v2 import mtls_http_request | ||
|
gladjohn marked this conversation as resolved.
|
||
|
|
||
| result = client.acquire_token_for_client( | ||
| resource="https://vault.azure.net", | ||
| mtls_proof_of_possession=True, | ||
| with_attestation_support=True, | ||
|
gladjohn marked this conversation as resolved.
|
||
| ) | ||
|
|
||
| cert_der = base64.b64decode(result["cert_der_b64"]) | ||
| resp = mtls_http_request( | ||
|
gladjohn marked this conversation as resolved.
|
||
| "GET", | ||
| "https://tokenbinding.vault.azure.net/secrets/boundsecret/?api-version=2015-06-01", | ||
| cert_der, | ||
| headers={ | ||
| "Authorization": f"{result['token_type']} {result['access_token']}", | ||
| "Accept": "application/json", | ||
| "x-ms-tokenboundauth": "true", | ||
| }, | ||
| ) | ||
| ``` | ||
|
|
||
| ## Why Python Needs a Helper (and .NET Doesn't) | ||
|
|
||
| | | .NET | Python | | ||
| |---|---|---| | ||
| | TLS stack | SChannel (Windows native) | OpenSSL | | ||
| | Can access non-exportable CNG keys | ✅ via `X509Certificate2` | ❌ Not possible | | ||
| | Standard HTTP client works for mTLS | ✅ `HttpClient` + `ClientCertificates` | ❌ `requests`/`httpx` cannot use KeyGuard keys | | ||
| | Solution | Platform gives it for free | `mtls_http_request()` bridges the gap via WinHTTP/SChannel | | ||
|
|
||
| ## Key Technical Findings | ||
|
|
||
| During development, three requirements were discovered for mTLS resource calls: | ||
|
|
||
| 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). | ||
|
|
||
| 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. | ||
|
|
||
| 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. | ||
|
|
||
| ## Auth Result Fields | ||
|
|
||
| ```python | ||
| { | ||
| "access_token": "eyJ...", | ||
| "token_type": "mtls_pop", | ||
| "expires_in": 86399, | ||
| "cert_pem": "-----BEGIN CERTIFICATE-----\n...", | ||
| "cert_der_b64": "MIID...", | ||
| "cert_thumbprint_sha256": "abc123...", | ||
| "key_name": "MsalMsiV2Key_caf87a12-..." | ||
| } | ||
|
gladjohn marked this conversation as resolved.
|
||
| ``` | ||
|
|
||
| ## Architecture Comparison | ||
|
|
||
| ``` | ||
| .NET E2E Flow: | ||
| MSAL.NET → mtls_pop token + X509Certificate2 (wraps CNG key) | ||
| ↓ | ||
| HttpClient + SChannel → mTLS to Key Vault ← platform-native, no helper needed | ||
|
|
||
| Python E2E Flow: | ||
| MSAL Python → mtls_pop token + cert PEM/DER (no private key access) | ||
| ↓ | ||
| mtls_http_request() → WinHTTP/SChannel via ctypes → mTLS to Key Vault | ||
| (helper needed because OpenSSL cannot access non-exportable KeyGuard keys) | ||
| ``` | ||
|
|
||
| ## Status | ||
|
|
||
| - ✅ Token acquisition works (mtls_pop token with KeyGuard attestation) | ||
| - ✅ Certificate binding verified (cnf.x5t#S256 matches) | ||
| - ✅ mTLS resource call presents certificate (confirmed by matching .NET's error) | ||
| - ⏳ AKV end-to-end blocked on service-side issue ("Certificate import or verification failed" — same error in both .NET and Python) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.