Skip to content

POC 2: MSI v2 mTLS PoP — WindowsCertificate + SchannelSession#931

Open
gladjohn wants to merge 3 commits into
AzureAD:devfrom
gladjohn:gladjohn/msiv2-poc2-mtls-pop
Open

POC 2: MSI v2 mTLS PoP — WindowsCertificate + SchannelSession#931
gladjohn wants to merge 3 commits into
AzureAD:devfrom
gladjohn:gladjohn/msiv2-poc2-mtls-pop

Conversation

@gladjohn

Copy link
Copy Markdown
Contributor

Summary

End-to-end mTLS Proof-of-Possession for Managed Identity v2 on Windows Azure VMs with Credential Guard / KeyGuard.

E2E proven on MSIV2 VM (June 2026): Token acquired → cert binding verified → downstream mTLS call to tokenbinding.vault.azure.net returned HTTP 200 with secret value.

What's included

Core (msal package)

  • msal/windows_certificate.py — Python equivalent of .NET X509Certificate2. Wraps NCRYPT_KEY_HANDLE, thread-safe CERT_CONTEXT creation, lifecycle management.
  • msal/msi_v2.py — Full MSI v2 flow: KeyGuard key mgmt, CSR, MAA attestation, issuecredential, WinHTTP/SChannel mTLS token acquisition. Returns binding_certificate in result.
  • msal/managed_identity.py — Public API: acquire_token_for_client(mtls_proof_of_possession=True, with_attestation_support=True)

Separate packages

  • msal-schannel-transport/ — WinHTTP/SChannel HTTP client for downstream mTLS (bypasses OpenSSL which cannot use non-exportable keys). Self-contained Win32 bindings.
  • msal-key-attestation/ — Python wrapper for MAA attestation. Native DLL sourced from NuGet: Microsoft.Azure.Security.KeyGuardAttestation v1.1.5 (not bundled in this PR).

Dev app + docs

  • sample/devapp_msi_v2_mtls/app.py — Uses ONLY public MSAL API
  • docs/mTLS-PoP-Architecture-Decision.md — Why WinHTTP/SChannel (OpenSSL CNG provider gap research)

App developer usage

import msal
from msal_schannel_transport import SchannelSession

client = msal.ManagedIdentityClient(msal.SystemAssignedManagedIdentity(), http_client=requests.Session())
result = client.acquire_token_for_client(
    resource="https://vault.azure.net",
    mtls_proof_of_possession=True, with_attestation_support=True)

with SchannelSession(client_certificate=result["binding_certificate"]) as session:
    resp = session.get(url, headers={
        "Authorization": f"{result['token_type']} {result['access_token']}",
        "x-ms-tokenboundauth": "true"})

E2E Results (MSIV2 VM)

Step Result
Token acquisition mtls_pop, 86399s
Cert binding (cnf.x5t#S256) ✓ MATCH
Downstream mTLS (tokenbinding vault) ✓ HTTP 200, secret returned

Notes

  • AttestationClientLib.dll (5.3MB) not included — sourced from NuGet at build time
  • Cert cache threshold: 1h remaining lifetime (IMDS certs are ~8h)
  • 52/52 unit tests pass locally

@gladjohn gladjohn requested a review from a team as a code owner June 20, 2026 23:55
Copilot AI review requested due to automatic review settings June 20, 2026 23:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a Windows-only end-to-end MSI v2 flow for Managed Identity mTLS Proof-of-Possession (PoP), including a handle-backed certificate wrapper (WindowsCertificate), the MSI v2 implementation (msal.msi_v2), and companion packages for KeyGuard attestation and WinHTTP/SChannel downstream mTLS calls.

Changes:

  • Add MSI v2 (IMDSv2) token acquisition over WinHTTP/SChannel with KeyGuard key management, CSR issuance, optional MAA attestation, and in-memory certificate caching.
  • Add WindowsCertificate (platform key-handle backed) and expose new ManagedIdentityClient.acquire_token_for_client() opt-in flags for MSI v2 gating.
  • Add new tests/docs/samples and two new sub-packages: msal-key-attestation and msal-schannel-transport.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
tests/test_msi_v2.py Unit coverage for cnf binding, cache behavior, and MI client gating.
tests/test_e2e_mtls_pop.py Windows-only E2E test wiring MSAL MSI v2 token + downstream Schannel mTLS call.
sample/msi_v2_sample.py Sample script demonstrating MSI v2 token acquisition and (non-mTLS) request header usage.
sample/MSI_V2_GUIDE.md Setup/usage guide for MSI v2 + attestation.
sample/devapp_msi_v2_mtls/app.py Dev app showing public-API-only flow + downstream SchannelSession call.
msal/windows_certificate.py New handle-backed certificate wrapper with store lookup and CERT_CONTEXT creation.
msal/msi_v2.py New MSI v2 implementation: KeyGuard key, CSR, IMDS calls, cert binding, WinHTTP mTLS token acquisition, cert cache.
msal/managed_identity.py Adds MsiV2Error and new opt-in flags to route MSI v2 flow from public API.
msal/__init__.py Exposes WindowsCertificate at package root (public API surface).
msal-schannel-transport/pyproject.toml Defines new transport package metadata/build config.
msal-schannel-transport/msal_schannel_transport/session.py Implements WinHTTP/SChannel session for downstream mTLS calls.
msal-schannel-transport/msal_schannel_transport/__init__.py Transport package exports + usage docstring.
msal-key-attestation/pyproject.toml Defines new attestation package metadata/build config and dependency on msal.
msal-key-attestation/msal_key_attestation/attestation.py Implements AttestationClientLib.dll bindings + JWT caching and provider factory.
msal-key-attestation/MANIFEST.in Packaging include/exclude rules for the attestation package.
msal-key-attestation/LICENSE License file for the attestation package.
docs/mTLS-PoP-Architecture-Decision.md Architecture decision record explaining WinHTTP/SChannel approach and separation of packages.
docs/MSI_V2_API.md API reference documentation for MSI v2 public API usage and behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread msal/__init__.py
Comment thread tests/test_e2e_mtls_pop.py
Comment thread tests/test_e2e_mtls_pop.py
Comment thread tests/test_e2e_mtls_pop.py
Comment thread sample/devapp_msi_v2_mtls/app.py
Comment thread docs/MSI_V2_API.md
Comment thread msal-key-attestation/pyproject.toml
Comment thread msal-schannel-transport/pyproject.toml
Comment thread docs/MSI_V2_API.md
Comment thread docs/MSI_V2_API.md
@gladjohn gladjohn force-pushed the gladjohn/msiv2-poc2-mtls-pop branch from 0c3db4d to 00539ca Compare June 21, 2026 00:04
Copilot AI review requested due to automatic review settings June 21, 2026 13:41
@gladjohn gladjohn force-pushed the gladjohn/msiv2-poc2-mtls-pop branch from 00539ca to 3df2f04 Compare June 21, 2026 13:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 38 out of 40 changed files in this pull request and generated 8 comments.

Comment thread msal/msi_v2.py Outdated
Comment thread tests/test_msi_v2.py
Comment thread sample/MSI_V2_GUIDE.md Outdated
Comment thread docs/MSI_V2_API.md
Comment thread tests/test_e2e_mtls_pop.py
Comment thread msal-schannel-transport/msal_schannel_transport/__init__.py
Comment thread msal-key-attestation/pyproject.toml
Comment thread msal-schannel-transport/msal_schannel_transport.egg-info/PKG-INFO Outdated
gladjohn and others added 3 commits June 21, 2026 06:52
…roof

This POC implements end-to-end mTLS Proof-of-Possession for Managed Identity v2
on Windows Azure VMs with Credential Guard / KeyGuard.

- msal/windows_certificate.py: Python equivalent of .NET X509Certificate2
  - Wraps NCRYPT_KEY_HANDLE (non-exportable private key)
  - Thread-safe CERT_CONTEXT creation for mTLS transports
  - Lifecycle management (close/context-manager)
- msal/msi_v2.py: obtain_token() now returns 'binding_certificate' in result
- msal/managed_identity.py: Public API wired (acquire_token_for_client)
- msal/__init__.py: WindowsCertificate exported

- msal-schannel-transport/: WinHTTP/SChannel HTTP client for downstream mTLS
  - Bypasses OpenSSL (which can't use non-exportable keys)
  - Uses NCRYPT_KEY_HANDLE via SChannel for TLS CertificateVerify
  - Self-contained Win32 bindings (no MSAL internal imports)
- msal-key-attestation/: MAA attestation wrapper with in-memory JWT caching

- sample/devapp_msi_v2_mtls/app.py: Uses ONLY public MSAL API
- docs/mTLS-PoP-Architecture-Decision.md: Why WinHTTP (OpenSSL CNG gap)
- docs/MSI_V2_API.md: API reference
- tests/: Unit tests (52 pass) + E2E test framework

- Token: mtls_pop, 86399s ✓
- Binding: cnf.x5t#S256 MATCH ✓
- Downstream: tokenbinding.vault.azure.net HTTP 200, secret returned ✓

- MSAL acquires token + returns cert handle (never makes downstream calls)
- App developer uses SchannelSession for downstream mTLS
- Same pattern as .NET: MSAL → HttpClient becomes MSAL → SchannelSession

- AttestationClientLib.dll (5.3MB) not included in this PR
  (sourced from NuGet: Microsoft.Azure.Security.KeyGuardAttestation v1.1.5)
- Production packaging TBD (platform wheel vs bundled)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- mtls_proof_of_possession=True now ALWAYS routes to MSI v2
  (no silent fallback to Bearer/v1 — matches .NET's
  MtlsPopTokenNotSupportedinImdsV1 hard-fail behavior)
- with_attestation_support controls binding strength tier:
  True  = KeyGuard (attested)
  False = Software (non-attested mTLS, still v2)
- Fix architecture doc: acknowledge third-party OpenSSL 3 CNG
  provider exists, clarify why it's insufficient for Python
- Add x-ms-tokenboundauth header to E2E Key Vault test
- Update unit test to verify non-attested v2 path

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add MsiV2Error to msal/__init__.py exports
- Add MsiV2Error class definition in managed_identity.py
- Add mtls_proof_of_possession and with_attestation_support params
- Fix base64url padding: (4 - len % 4) -> ((4 - len % 4) % 4)
- Fix cert cache threshold: 24h -> 1h (matches IMDS ~8h certs)
- Fix authorization_header references -> token_type + access_token
- Fix --run-e2e docs -> RUN_E2E_TESTS env var
- Fix Python version: >=3.8 -> >=3.9 (matches msal setup.cfg)
- Fix license format: 'MIT' -> {text = 'MIT'} (PEP 621)
- Add msal-key-attestation/__init__.py (fixes import + __version__)
- Add README.md to both sub-packages (fixes pyproject.toml refs)
- Add binding_certificate to MSI_V2_API.md return value docs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@gladjohn gladjohn force-pushed the gladjohn/msiv2-poc2-mtls-pop branch from 3df2f04 to f1ab3e0 Compare June 21, 2026 13:59
gladjohn added a commit that referenced this pull request Jun 21, 2026
- Add prerequisite note: APIs come from PR #931, not yet on dev
- Note this is standalone docs (not Sphinx-rendered)
- Fix rstrip('/.default') -> removesuffix('/.default')
- Fix AccessToken usage: store token_type on credential (not AccessToken)
- Fix auth policy: read token_type from credential, not token object

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants