Skip to content

docs(msal-node): mTLS PoP cross-SDK implementation standard#8556

Open
Robbie-Microsoft wants to merge 2 commits into
rginsburg/mtls_popfrom
rginsburg/mtls_pop_msals_implementation_doc
Open

docs(msal-node): mTLS PoP cross-SDK implementation standard#8556
Robbie-Microsoft wants to merge 2 commits into
rginsburg/mtls_popfrom
rginsburg/mtls_pop_msals_implementation_doc

Conversation

@Robbie-Microsoft
Copy link
Copy Markdown
Collaborator

Summary

Adds mtls-pop-cross-sdk-implementation-standard.md — a reference document for the Azure SDK team and MSAL maintainers covering the mTLS Proof-of-Possession implementation standard across all MSAL SDKs.

Contents

  • Background — what mTLS PoP is, MSAL.NET as the reference implementation, the downstream calls challenge
  • Part 1 — per-SDK API changes: enabling mTLS PoP, attestation, HTTP client injection, token result fields, GetManagedIdentitySourceAsync()
  • Part 2AuthenticationResult binding certificate fields by SDK
  • Part 3 — downstream mTLS calls: why BYO HTTP clients fail with KeyGuard keys; per-SDK current state
  • Part 4 — software key pivot (roadmap; pending IMDS support)
  • Appendix A — options for non-exportable key scenarios in Node.js
  • Appendix B — SNI / Confidential Client path: certificate input formats and standard
  • Appendix CGetManagedIdentitySourceAsync() (msal-dotnet)
  • Appendix D — FIC / ClientAssertionCredential (stub; separate doc TBD)

Reviewers

This document has been reviewed by @bgavrilMS and @gladjohn.

Robbie-Microsoft and others added 2 commits April 23, 2026 14:23
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ation-standard

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Robbie-Microsoft Robbie-Microsoft requested review from a team as code owners April 23, 2026 18:26

### HTTP Client Injection

A key architectural change for mTLS PoP: MSAL must control (or be injected with) the HTTP client used for the token request, because it must present the client certificate during the TLS handshake with the STS. For MSI flows specifically, this applies only to the second leg (IMDS → ESTS token request) — a custom HTTP client can be used for the first leg (calls made within IMDS, which require no client certificate). MSAL uses its own internal transport for the cert-authenticated ESTS leg because the private key is non-exportable and cannot be surfaced to an external HTTP client.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What parts of this apply to IMDS users (3p) and which to IMDS itself (1p)?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Nothing in this paragraph applies to IMDS itself - IMDS is just the endpoint MSAL calls on leg 1. The entire description is about what a 3p MSAL consumer can and cannot inject.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ah, then I was confused by "IMDS → ESTS token request" and "calls made within IMDS"

Comment thread lib/msal-node/docs/mtls-pop-cross-sdk-implementation-standard.md

**MSAL.NET is the reference implementation.** It ships mTLS PoP for both the Managed Identity path (`ManagedIdentityApplication.AcquireTokenForManagedIdentity().WithMtlsProofOfPossession()`) and the Confidential Client / SNI path (`ConfidentialClientApplication.AcquireTokenForClient().WithMtlsProofOfPossession()`). The goal of this document is to standardize the same behavior across all MSAL SDKs.

**The downstream problem is the key challenge.** After acquiring an mTLS PoP token, the developer must make downstream resource calls *over mTLS using the same binding certificate* — the resource server checks that the TLS client certificate matches the public key embedded in the token. When the binding key is stored in Windows KeyGuard (VBS-protected), standard TLS stacks like OpenSSL cannot perform this handshake because CNG refuses to export the key. See [Part 3](#part-3--downstream-mtls-calls-the-core-problem) for the full analysis.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A definition of "CNG" and maybe a link to some docs for it would help

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.

https://learn.microsoft.com/en-us/windows/win32/seccng/cng-portal

CNG (Cryptography API: Next Generation) is Microsoft's modern cryptographic framework in Windows, replacing the legacy CryptoAPI. It provides:

  • Key storage providers (KSPs) — including the KeyGuard/VBS provider that keeps keys in virtualization-based security
  • NCryptSignHash() — signs data using a key handle without ever exposing raw key bytes
  • NCryptExportKey() — exports keys (blocked for non-exportable KeyGuard keys)

In the mTLS PoP context, CNG is the gatekeeper. TLS stacks that can talk to CNG via key handles (Schannel, WinHTTP, Go's crypto.Signer) work with KeyGuard keys, while stacks that need raw key bytes (OpenSSL, used by Node.js/Python/Java) cannot.

MSAL generates a key inside Windows Virtualization-Based Security (VBS) KeyGuard. The key material is **never** accessible outside CNG (`NCryptSignHash` only). Attestation via MAA is an integral part of this path — KeyGuard without attestation provides minimal security value. This is the complex scenario: attestation requires a native `AttestationClientLib.dll` distributed separately, and downstream BYO HTTP client is **not possible** for most SDKs (see [Part 3](#part-3--downstream-mtls-calls-the-core-problem)).

**Roadmap — software / exportable keys (simpler path):**
MSAL generates a software RSA key; key material can be exported as PEM bytes. No attestation is required. Downstream BYO HTTP client is **possible** for all SDKs because the raw key bytes can be passed to any TLS stack. This path is not yet end-to-end testable — Entra STS accepts software-key mTLS PoP tokens, but IMDS (MIRP) does not yet issue software-key binding certificates, so the full flow cannot be validated.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks like a much easier path for everyone; could we start here?

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.

Agree - software keys are the simpler path for everyone. We have an active ask to the IMDS team to light this up.

**Roadmap — software / exportable keys (simpler path):**
MSAL generates a software RSA key; key material can be exported as PEM bytes. No attestation is required. Downstream BYO HTTP client is **possible** for all SDKs because the raw key bytes can be passed to any TLS stack. This path is not yet end-to-end testable — Entra STS accepts software-key mTLS PoP tokens, but IMDS (MIRP) does not yet issue software-key binding certificates, so the full flow cannot be validated.

> **Critical**: Downstream transport feasibility depends on the **key model**. A KeyGuard key cannot be used with standard BYO HTTP clients regardless of whether attestation is enabled.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

My understanding is that Py, Java and JS rely on OpenSSL on Windows for crypto. And Open SSL doesn't KeyGuard crypto operations (namely opening mTLS channel with cert that has private key in KeyGuard). But Golang seems to work?

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.

msal-go wraps the CNG key handle in a crypto.Signer implementation that calls NCryptSignHash() under the hood, so the TLS CertificateVerify step is performed entirely through CNG without ever exporting key bytes. It's architecturally equivalent to what Schannel/.NET does.

Copy link
Copy Markdown
Collaborator Author

@Robbie-Microsoft Robbie-Microsoft Apr 23, 2026

Choose a reason for hiding this comment

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

You are correct.

Go works because its TLS stack is pluggable via crypto.Signer in a way that OpenSSL-backed runtimes are not.


All SDKs set `tokenType` / `token_type` to `"mtls_pop"`.

**Authorization header:** All SDKs use `Authorization: mtls_pop <access_token>` for downstream resource calls. msal-dotnet provides `result.CreateAuthorizationHeader()`; other SDKs require manual construction.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why the need for manual construction in non-.NET languages? Can an authZ header helper be provided in other languages too?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

CreateAuthorizationHeader() is a trivial one-liner ("mtls_pop " + accessToken) - there's no technical reason it can't be added to every MSAL SDK. It just hasn't been done yet since the feature is new. We should add equivalent helpers across all SDKs as a follow-up.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If it's as simple as token.kind + token.string() I'd leave it to callers


- **Current**: the developer must manually place `AttestationClientLib.dll` alongside the MSAL native addon (e.g., in `bin/win-x64/`). Each MSAL team tracks a follow-up to provide a setup script that downloads the DLL from the appropriate NuGet package, unzips it, and places it correctly.
- **Ownership**: each MSAL SDK team owns the setup script for their respective package.
- **Long-term goal**: the MAA team will produce first-class native packages (npm, pip, Maven, etc.) so that `AttestationClientLib.dll` can be declared as a standard package dependency — eliminating manual bin-placement entirely.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This won't eliminate manual bin placement for all languages (e.g. Go)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch - that's accurate. Go's module system has no mechanism for distributing native binaries, so there's no Go equivalent of an npm/pip/Maven package that could carry AttestationClientLib.dll. Even if MAA publishes native packages for every other ecosystem, Go developers would still need to manually obtain and place the DLL.

|---|---|---|
| **msal-dotnet** | `.WithAttestationSupport()` — extension method in the separate `Microsoft.Identity.Client.KeyAttestation` NuGet package | `Microsoft.Identity.Client.KeyAttestation` NuGet (wraps native `Microsoft.Azure.Security.KeyGuardAttestation.dll`) |
| **msal-go** | Automatic — key type is selected at runtime (KeyGuard → Hardware → InMemory fallback); KeyGuard path triggers attestation when `AttestationClientLib.dll` is present; no explicit API flag | `AttestationClientLib.dll` |
| **msal-java** | `withAttestation: true` boolean parameter in `MtlsMsiClient.acquireToken()` | `AttestationClientLib.dll` on system `PATH` |
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The fact that we don't have a maven package for the attestation package is not great. We need the MAA or KeyGuard team to produce one. If there are security issues, CVEs cannot alert bin-placed DLLs.


After acquiring an mTLS PoP token, the developer must make downstream resource calls **over mTLS using the same binding certificate**. This is where the key model critically matters.

Any TLS stack performing a client certificate handshake must have signing access to the private key for the `CertificateVerify` step in the TLS exchange — either through raw key material (as OpenSSL-style stacks require) or through a platform/provider-backed key handle (as Schannel, WinHTTP, and Go's `crypto.Signer` provide). When the private key is stored in Windows KeyGuard (VBS-protected), the CNG key storage provider rejects all export operations, so only stacks with a CNG-handle path can work:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It would help to clarify who's responsible for ensuring the resource request handshake succeeds i.e., that the resource client is able to use the certificate, and outline how that should be done

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.

agreed we need a devex story here -

  • KeyGuard path (current): For msal-dotnet and msal-go, the Azure SDK story is clear , you can configure your own HTTP client with the binding certificate from AuthenticationResult.
  • For msal-java, msal-node, and msal-python, MSAL m,ay need to own the downstream transport via
    helpers (e.g., MtlsMsiClient.httpRequest(), sendGetRequestAsync()) because the developer's HTTP client can't use the non-exportable key.
  • Software key path (roadmap): The Azure SDK / developer is universally responsible — MSAL returns the exportable key + cert in AuthenticationResult


#### Developer experience for `AttestationClientLib.dll` bin-placement

- **Current**: the developer must manually place `AttestationClientLib.dll` alongside the MSAL native addon (e.g., in `bin/win-x64/`). Each MSAL team tracks a follow-up to provide a setup script that downloads the DLL from the appropriate NuGet package, unzips it, and places it correctly.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Some more details would help here. Is the destination path global? Does it vary across MSALs? How will users find it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good point - the path is not global and varies by SDK. For msal-dotnet, NuGet handles placement automatically. For Go and Node.js, Windows' standard DLL search order applies (same directory as the executable/addon, or PATH). For Java, PATH is required. We'll add per-SDK specifics to the subsection. The setup scripts we're tracking will handle the correct destination per package.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remember to threat model DLL preloading attacks


- **Current**: the developer must manually place `AttestationClientLib.dll` alongside the MSAL native addon (e.g., in `bin/win-x64/`). Each MSAL team tracks a follow-up to provide a setup script that downloads the DLL from the appropriate NuGet package, unzips it, and places it correctly.
- **Ownership**: each MSAL SDK team owns the setup script for their respective package.
- **Long-term goal**: the MAA team will produce first-class native packages (npm, pip, Maven, etc.) so that `AttestationClientLib.dll` can be declared as a standard package dependency — eliminating manual bin-placement entirely.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Realistically, how far out would this long-term goal be accomplished? The manual bin placement approach feels archaic.

- MSAL's built-in transport helpers become optional convenience wrappers rather than mandatory requirements
- `AttestationClientLib.dll` and native attestation dependencies are not required

**Current status**: IMDS (MIRP) does not yet issue software-key binding certificates, making this path untestable end-to-end. Entra STS accepts software-key mTLS PoP tokens, but without IMDS support the full MSI flow cannot be exercised. This is on the roadmap — there is an active ask to the IMDS team to enable software-key issuance. Until that pivot lands, **use MSAL's SDK-provided transport for downstream calls on msal-java, msal-node, and msal-python**, and BYO HTTP client on msal-dotnet and msal-go.
Copy link
Copy Markdown

@chlowell chlowell Apr 23, 2026

Choose a reason for hiding this comment

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

"Untestable" implies this path is possible but the rest of the text presents it as impossible because IMDS is missing a feature. Do we have any idea when that feature will be implemented?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch - "untestable" is misleading. The path is not yet possible (not merely hard to test): IMDS does not issue software-key binding certs, so the flow cannot complete. Will change to "this path is not yet functional end-to-end."

And the sentence would read:
Current status: IMDS (MIRP) does not yet issue software-key binding certificates, so this path is not yet functional end-to-end. Entra STS accepts software-key mTLS PoP tokens, but without IMDS support the full MSI flow cannot complete. This is on the roadmap - there is an active ask to the IMDS team to enable software-key issuance.

| **msal-go** | Path 1 (SNI): `confidential.WithMtlsProofOfPossession()` on `AcquireTokenByCredential()`<br/>Path 2 (MSI): `managedidentity.WithMtlsProofOfPossession()` on `AcquireToken()` |
| **msal-java** | Path 1 (SNI): `ClientCredentialParameters.builder(scopes).withMtlsProofOfPossession().build()` on `app.acquireToken()`<br/>Path 2 (MSI): `new MtlsMsiClient().acquireToken(resource, identityType, identityId, withAttestation, correlationId)` |
| **msal-node** | Path 1 (SNI): `app.acquireTokenByClientCredential({ scopes, authenticationScheme: AuthenticationScheme.MTLS_POP })` — `@azure/msal-node`<br/>Path 2 (MSI): `new MtlsManagedIdentityApplication().acquireToken({ resource })` — `@azure/msal-node-mtls-extensions` |
| **msal-python** | TBD |
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is there no API yet for python, or just the doc hasn't been updated for Python yet?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The Python implementation is not finished yet. CC @gladjohn

| **msal-node** | WinHTTP + Schannel (N-API addon) | ❌ **Not possible** — Node.js/OpenSSL needs raw key bytes; CNG refuses export for KeyGuard keys | `MtlsManagedIdentityApplication.sendGetRequestAsync()` / `sendPostRequestAsync()` — `@azure/msal-node-mtls-extensions` |
| **msal-python** | WinHTTP + Schannel (ctypes) | ❌ **Not possible** — same CNG constraint | ❌ **Not yet implemented** |

> **Default stance for msal-java, msal-node, msal-python**: With the current KeyGuard architecture, downstream BYO HTTP client use is **not possible**. This is a fundamental architectural constraint — the key cannot be extracted from CNG. SDK-provided transports are the only workaround where available; for msal-python, no downstream helper exists yet.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

for msal-python, no downstream helper exists yet

Is the work to create this downstream helper already in progress?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's just that we haven't finished the msal-python implementation yet. CC @gladjohn

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.

The msal-python mTLS PoP implementation currently covers token acquisition via WinHTTP/Schannel (ctypes). We have a POC for the downstream API helper. I can share the POC on this one.

@microsoft-github-policy-service
Copy link
Copy Markdown
Contributor

Reminder: This PR appears to be stale. If this PR is still a work in progress please mark as draft.

@microsoft-github-policy-service microsoft-github-policy-service Bot added the Needs: Attention 👋 Awaiting response from the MSAL.js team label May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs: Attention 👋 Awaiting response from the MSAL.js team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants