Skip to content

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

Closed
gladjohn wants to merge 1 commit into
AzureAD:devfrom
gladjohn:gladjohn/msiv2-poc2-mtls-pop
Closed

POC 2: MSI v2 mTLS PoP — WindowsCertificate + SchannelSession#930
gladjohn wants to merge 1 commit 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 (non-exportable private key), thread-safe CERT_CONTEXT creation, lifecycle management.
  • msal/msi_v2.pyobtain_token() now returns binding_certificate (WindowsCertificate) in auth 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 AttestationClientLib.dll with in-memory MAA JWT caching. Native DLL sourced from NuGet: Microsoft.Azure.Security.KeyGuardAttestation v1.1.5

Dev app + docs

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

Architecture

MSAL (token acquisition)          msal-schannel-transport (downstream)
┌──────────────────────┐          ┌─────────────────────────┐
│ acquire_token_for_   │          │ SchannelSession          │
│ client() →           │          │ • WinHTTP/SChannel       │
│   access_token       │─────────▶│ • Uses NCRYPT_KEY_HANDLE │
│   binding_certificate│          │ • mTLS with non-export   │
└──────────────────────┘          └─────────────────────────┘
  • MSAL only acquires tokens — never makes downstream calls
  • App developer owns downstream HTTP (same as .NET pattern)
  • msal-schannel-transport exists because Python's OpenSSL cannot use non-exportable keys

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:
    response = session.get(url, headers={"Authorization": f"{result['token_type']} {result['access_token']}"})

E2E Results

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

Notes

  • AttestationClientLib.dll (5.3MB) not included — sourced from NuGet at build time
  • Production packaging (platform wheel) TBD
  • 52/52 unit tests pass locally

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

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 an end-to-end Windows-only MSI v2 flow that acquires mTLS Proof-of-Possession (mtls_pop) tokens bound to non-exportable KeyGuard-backed private keys, and adds companion packages to support attestation and downstream mTLS calls via WinHTTP/SChannel.

Changes:

  • Add msal.msi_v2 (ctypes-based KeyGuard + IMDSv2 + WinHTTP/SChannel) and WindowsCertificate for platform-backed cert/key handle lifecycle.
  • Extend ManagedIdentityClient.acquire_token_for_client() with opt-in flags for MSI v2 (mTLS PoP + attestation) and expose MsiV2Error.
  • Add new companion packages (msal-key-attestation, msal-schannel-transport), tests/docs, and update Azure Pipelines to run SDL + test matrix.

Reviewed changes

Copilot reviewed 32 out of 33 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/test_msi_v2.py New unit tests for MSI v2 helpers (cnf binding, cache, IMDS helpers, gating).
tests/test_e2e.py E2E gating + lab-config improvements for CI friendliness (esp. Azure DevOps).
tests/test_e2e_mtls_pop.py New Windows-only E2E test for MSI v2 token + downstream mTLS call.
tests/lab_config.py Add LAB_APP_CLIENT_ID constant and clean_env() to avoid ADO $(VAR) literal pitfalls.
setup.cfg Configure pytest testpaths = tests.
sample/msi_v2_sample.py New sample demonstrating MSI v2 token acquisition and header usage.
sample/MSI_V2_GUIDE.md New guide for setup/usage of MSI v2 + attestation package.
sample/devapp_msi_v2_mtls/app.py Dev app demonstrating public-API-only MSI v2 + downstream Schannel transport usage.
msal/windows_certificate.py New WindowsCertificate wrapper for CERT_CONTEXT creation + NCrypt handle lifecycle.
msal/sku.py Fix docstring typo and bump version string.
msal/msi_v2.py New MSI v2 implementation (KeyGuard key, CSR, IMDS, cert binding, WinHTTP mTLS token acquisition).
msal/managed_identity.py Public API gating for MSI v2 via new flags + MsiV2Error.
msal/init.py Export MsiV2Error and WindowsCertificate in package namespace.
msal-schannel-transport/pyproject.toml New package metadata for downstream WinHTTP/SChannel transport.
msal-schannel-transport/msal_schannel_transport/session.py SchannelSession implementation for downstream mTLS with non-exportable keys.
msal-schannel-transport/msal_schannel_transport/init.py Public exports for msal-schannel-transport.
msal-key-attestation/tests/test_attestation.py Unit tests for in-memory MAA JWT caching and provider wrapper.
msal-key-attestation/setup.py Minimal setup entrypoint.
msal-key-attestation/setup.cfg Package metadata + dependency on msal.
msal-key-attestation/README.md Usage + architecture docs for attestation package.
msal-key-attestation/pyproject.toml Build metadata + packaging notes for native DLL sourcing.
msal-key-attestation/msal_key_attestation/attestation.py ctypes bindings to AttestationClientLib.dll + JWT caching + provider factory.
msal-key-attestation/msal_key_attestation/init.py Package exports/version.
msal-key-attestation/MANIFEST.in Include license/readme, exclude tests.
msal-key-attestation/LICENSE MIT license file.
docs/mTLS-PoP-Architecture-Decision.md ADR documenting why WinHTTP/SChannel is required for non-exportable keys in Python.
docs/MSI_V2_API.md Public API reference for MSI v2 and attestation package integration.
azure-pipelines.yml Replace ad-hoc pipeline with template-driven PR gate + scheduled CI.
.Pipelines/template-pipeline-stages.yml New shared template: SDL scans + test matrix + KV cert retrieval logic.
.Pipelines/pipeline-publish.yml New manual release pipeline with version validation + build + publish stages.
.Pipelines/credscan-exclusion.json CredScan suppressions for known test fixtures.
.Pipelines/CI-AND-RELEASE-PIPELINES.md Documentation for CI/release pipeline structure and release process.
.gitignore Ignore egg-info for msal-key-attestation editable installs.
Comments suppressed due to low confidence (1)

msal/managed_identity.py:274

  • This docstring claims the result is always cached, but the new MSI v2 branch returns before the token-cache lookup/store logic, so MSI v2 results are not cached the same way as MSI v1. Either cache MSI v2 results (if feasible) or document the difference to avoid misleading API consumers.
        """Acquire token for the managed identity.

        The result will be automatically cached.
        Subsequent calls will automatically search from cache first.


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

Comment on lines +92 to +96
try:
from msal_key_attestation import get_attestation_token
attestation_provider = get_attestation_token
except ImportError:
pass
Comment on lines +85 to +88
from msal.managed_identity import UserAssignedManagedIdentity
mi = UserAssignedManagedIdentity(
ManagedIdentity={"ManagedIdentityIdType": "ClientId",
"Id": E2E_CLIENT_ID})
Comment on lines +180 to +184
try:
from msal_key_attestation import get_attestation_token
attestation_provider = get_attestation_token
except ImportError:
pass
Comment thread msal/msi_v2.py
Comment on lines +101 to +103
# Minimum remaining cert lifetime to cache (24 hours)
MIN_REMAINING_LIFETIME_SEC = 24 * 3600

Comment thread tests/test_msi_v2.py
Comment on lines +242 to +244
# Not enough remaining lifetime (< 24h)
entry = self._make_entry(not_after=time.time() + 3600)
_cert_cache_set("k3", entry)
…roof

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

## Core (msal package)
- msal/windows_certificate.py: Python X509Certificate2 (NCRYPT_KEY_HANDLE wrapper)
- msal/msi_v2.py: Full MSI v2 flow (KeyGuard key, CSR, attestation, issuecredential, mTLS token)
- msal/managed_identity.py: Public API (acquire_token_for_client + mtls_proof_of_possession)

## Separate packages
- msal-schannel-transport/: WinHTTP/SChannel for downstream mTLS (bypasses OpenSSL)
- msal-key-attestation/: MAA attestation wrapper (DLL from NuGet: Microsoft.Azure.Security.KeyGuardAttestation v1.1.5)

## Dev app + docs
- sample/devapp_msi_v2_mtls/app.py: Uses ONLY public MSAL API
- docs/mTLS-PoP-Architecture-Decision.md: Why WinHTTP (OpenSSL CNG gap)

## E2E Results (MSIV2 VM, June 2026)
- Token: mtls_pop ✓ | Binding: cnf.x5t#S256 MATCH ✓ | Downstream: HTTP 200 ✓

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@gladjohn gladjohn force-pushed the gladjohn/msiv2-poc2-mtls-pop branch from f611587 to f92d975 Compare June 20, 2026 23:45
@gladjohn gladjohn closed this Jun 20, 2026
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