Skip to content

Add opt-in KERB_CERTIFICATE_LOGON smart card logon workaround#172

Merged
Richard Markiewicz (thenextman) merged 1 commit into
masterfrom
rdmw/kerb-cert-logon-inproc
Jul 3, 2026
Merged

Add opt-in KERB_CERTIFICATE_LOGON smart card logon workaround#172
Richard Markiewicz (thenextman) merged 1 commit into
masterfrom
rdmw/kerb-cert-logon-inproc

Conversation

@thenextman

Copy link
Copy Markdown
Member

Summary

Client-side workaround for smart card certificate logon failures over RDP caused by the tspkg regression, implemented entirely by extending MsRdpEx's SSPI API hooking — no LSASS patching and no internal mstscax offset hooks. Supersedes #171: same approach, extended so it also works for the in-process RDP ActiveX control (not just out-of-process mstsc.exe), hardened, and validated end-to-end against smart card hardware.

Problem

When a smart card certificate and PIN are both pre-supplied (an unattended logon, no interactive prompt), the CredSSP credential reaching LSASS is not a KERB_CERTIFICATE_LOGON. tspkg then calls CryptAcquireCertificatePrivateKey without CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG, which fails for CNG/NCrypt-backed keys (the common smart card case) and surfaces as "The Local Security Authority cannot be contacted". The interactive Windows prompt is unaffected because it builds a packed KERB_CERTIFICATE_LOGON. Certificate-only (no PIN) logons were never broken.

Solution

When KerbCertificateLogon:i:1 is set on a session, the AcquireCredentialsHandleW detour hands LSASS a CredsspCertificateCreds / KERB_CERTIFICATE_LOGON packed buffer, obtained either by:

  • synthesizing it from the session's marshaled certificate user name + PIN — for hosts that do not pass the credential in-band (the in-process RDP ActiveX control), or
  • rewriting a marshaled smart card credential already present in the auth data — the out-of-process mstsc.exe case (preserving CREDSSP_CRED_EX wrapping).

Key design points

  • Session correlation: an active-session registry with per-thread bindings resolves the owning CMsRdpExtendedSettings on the SSPI worker thread — where the asynchronous CredSSP handshake actually runs — and fails closed when ambiguous. The SSPI session scope is idempotent and ends on connect failure, disconnect and teardown.
  • PIN handling: the PIN cannot be read back from the protected secure-string property, so it is captured when set, kept only for the connection, DPAPI-encrypted in memory (CryptProtectMemory), decrypted transiently while the credential is built, and zeroed before release.
  • Reconnect: the marshaled certificate user name is snapshotted, because mstscax replaces it with the resolved account name after the first logon; without the snapshot a reconnect could not rebuild the credential.
  • Opt-in and inert without a PIN: certificate-only logons are left untouched and use the normal Windows prompt. Independent of PasswordContainsSCardPin — the stock RDP setting the caller sets to have a smart card credential delegated to the remote.
  • No secret leakage — no PINs, passwords, or certificate bytes are logged; secret-free diagnostics gated behind MSRDPEX_SSPI_SMARTCARD_DEBUG=1.

Files changed

  • dll/Sspi.cpp — session registry + resolution, diagnostics, KERB_CERTIFICATE_LOGON builder, synthesis + rewrite paths, wired-in detour
  • dll/RdpSettings.cpp, include/MsRdpEx/RdpSettings.hKerbCertificateLogon opt-in, per-session DPAPI-protected PIN capture, marshaled-username snapshot
  • dll/RdpInstance.cpp, include/MsRdpEx/RdpInstance.h — resolve extended settings by core property set
  • dll/MsRdpClient.cpp — SSPI session scope lifetime; discard captured PIN on non-certificate logons
  • dll/CMakeLists.txt — link crypt32 (DPAPI)
  • include/MsRdpEx/Sspi.h — begin/end session declarations
  • README.md — documents the RDP option, behavior, and debug env var

Validation

  • Built MsRdpEx.dll Release/x64 — links cleanly, no new warnings.
  • Validated end-to-end against smart card hardware in Remote Desktop Manager: certificate + PIN logon succeeds in both embedded (in-process ActiveX) and external (mstsc.exe) modes; certificate-only (no PIN) still prompts and connects; connect and reconnect both work.
  • Not yet done: x86 / arm64 builds (x64 only so far — CI), and KERB_SMARTCARD_CSP_INFO (CspData) for multi-reader / multi-card disambiguation (tracked with a TODO).

A recent Windows update regressed tspkg: when a smart card certificate and PIN
are both pre-supplied (an unattended logon, no interactive prompt), the CredSSP
credential reaching LSASS is not a KERB_CERTIFICATE_LOGON, so tspkg calls
CryptAcquireCertificatePrivateKey without CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG and
fails for CNG/NCrypt-backed smart card keys ("The Local Security Authority cannot
be contacted"). The interactive prompt is unaffected because it packs a proper
KERB_CERTIFICATE_LOGON.

Add an opt-in client-side workaround, enabled per session with
KerbCertificateLogon:i:1. MsRdpEx hooks AcquireCredentialsHandleW and hands LSASS
a KERB_CERTIFICATE_LOGON (CredsspCertificateCreds), built either by:

- synthesizing it from the session's marshaled certificate user name and PIN,
  for hosts that do not pass the credential in-band (the in-process ActiveX
  control), or
- rewriting a marshaled smart card credential already present in the auth data
  (the out-of-process mstsc.exe case).

Details:
- Session correlation resolves the connection on the SSPI worker thread via an
  active-session registry with per-thread bindings, failing closed when
  ambiguous. The SSPI session scope is idempotent and ends on connect failure,
  disconnect and teardown.
- The PIN is captured when set (it cannot be read back), kept only for the
  connection, DPAPI-encrypted in memory, decrypted transiently while building the
  credential, and zeroed before release.
- The marshaled certificate user name is snapshotted so reconnect still works
  after mstscax replaces it with the resolved account name.
- Strictly opt-in and inert without a PIN, so certificate-only logons keep using
  the normal Windows prompt. Independent of PasswordContainsSCardPin, which the
  caller sets to have the smart card credential delegated to the remote.
- MSRDPEX_SSPI_SMARTCARD_DEBUG=1 logs secret-free CredSSP credential metadata.

Co-authored-by: Marc-André Moreau <marcandre.moreau@gmail.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

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 adds an opt-in, client-side workaround (KerbCertificateLogon:i:1) for smart card certificate logon failures over RDP caused by the tspkg regression. It is implemented entirely by extending MsRdpEx's existing SSPI API hooking: when enabled for a session, the AcquireCredentialsHandleW detour hands LSASS a CredsspCertificateCreds / KERB_CERTIFICATE_LOGON packed buffer — either synthesized from the session's marshaled certificate user name + captured PIN (in-process ActiveX control) or by rewriting an in-band marshaled smart card credential (mstsc.exe). It supersedes #171 by additionally covering the in-process case and hardening session correlation. The change fits into the DLL's hooking layer alongside the existing mstscax/CredSSP interception machinery.

Changes:

  • New SSPI session registry (per-thread TLS binding + global active-session table under a critical section) to resolve the owning CMsRdpExtendedSettings on the async CredSSP worker thread, plus a KERB_CERTIFICATE_LOGON builder with synthesis and rewrite paths.
  • Per-session PIN capture from SetSecureStringProperty, held DPAPI-encrypted (CryptProtectMemory) and discarded for non-certificate logons, plus a marshaled-username snapshot for reconnect.
  • SSPI session scope lifetime bracketed around raw_Connect()/Disconnect()/teardown, crypt32 linkage for DPAPI, and README documentation.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
dll/Sspi.cpp Session registry/resolution, diagnostics, KERB_CERTIFICATE_LOGON builder, synthesis + rewrite paths, wired into the AcquireCredentialsHandleW detour
dll/RdpSettings.cpp KerbCertificateLogon/PasswordContainsSCardPin opt-in, DPAPI-protected PIN capture/read/clear, marshaled-username snapshot
include/MsRdpEx/RdpSettings.h Declarations for the new accessors and PIN/state members
dll/RdpInstance.cpp MsRdpEx_FindExtendedSettingsByCoreProps to route captured PIN to the right instance
include/MsRdpEx/RdpInstance.h Declaration of the core-props lookup helper
dll/MsRdpClient.cpp Begin/end SSPI session scope around connect/disconnect/destroy; discard captured PIN on non-cert logons
include/MsRdpEx/Sspi.h Begin/end session declarations
dll/CMakeLists.txt Link crypt32.lib for DPAPI
README.md Documents the RDP option, behavior, and debug env var

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

Comment thread dll/RdpSettings.cpp
Comment thread dll/RdpSettings.cpp
Comment on lines +1688 to +1691
bool CMsRdpExtendedSettings::GetPasswordContainsSCardPin()
{
return m_PasswordContainsSCardPin;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It just mirrors the setter, this is harmless and allows existing patterns. Let's leave it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I have no comments. The solution is impressive.

LGTM

@thenextman Richard Markiewicz (thenextman) merged commit cb72ba2 into master Jul 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants