Add opt-in KERB_CERTIFICATE_LOGON smart card logon workaround#172
Conversation
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>
There was a problem hiding this comment.
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
CMsRdpExtendedSettingson the async CredSSP worker thread, plus aKERB_CERTIFICATE_LOGONbuilder 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,crypt32linkage 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.
| bool CMsRdpExtendedSettings::GetPasswordContainsSCardPin() | ||
| { | ||
| return m_PasswordContainsSCardPin; | ||
| } |
There was a problem hiding this comment.
It just mirrors the setter, this is harmless and allows existing patterns. Let's leave it.
Pavlo Myroniuk (TheBestTvarynka)
left a comment
There was a problem hiding this comment.
I have no comments. The solution is impressive.
LGTM
Summary
Client-side workaround for smart card certificate logon failures over RDP caused by the
tspkgregression, implemented entirely by extending MsRdpEx's SSPI API hooking — no LSASS patching and no internalmstscaxoffset hooks. Supersedes #171: same approach, extended so it also works for the in-process RDP ActiveX control (not just out-of-processmstsc.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.tspkgthen callsCryptAcquireCertificatePrivateKeywithoutCRYPT_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 packedKERB_CERTIFICATE_LOGON. Certificate-only (no PIN) logons were never broken.Solution
When
KerbCertificateLogon:i:1is set on a session, theAcquireCredentialsHandleWdetour hands LSASS aCredsspCertificateCreds/KERB_CERTIFICATE_LOGONpacked buffer, obtained either by:mstsc.execase (preservingCREDSSP_CRED_EXwrapping).Key design points
CMsRdpExtendedSettingson 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.CryptProtectMemory), decrypted transiently while the credential is built, and zeroed before release.mstscaxreplaces it with the resolved account name after the first logon; without the snapshot a reconnect could not rebuild the credential.PasswordContainsSCardPin— the stock RDP setting the caller sets to have a smart card credential delegated to the remote.MSRDPEX_SSPI_SMARTCARD_DEBUG=1.Files changed
dll/Sspi.cpp— session registry + resolution, diagnostics,KERB_CERTIFICATE_LOGONbuilder, synthesis + rewrite paths, wired-in detourdll/RdpSettings.cpp,include/MsRdpEx/RdpSettings.h—KerbCertificateLogonopt-in, per-session DPAPI-protected PIN capture, marshaled-username snapshotdll/RdpInstance.cpp,include/MsRdpEx/RdpInstance.h— resolve extended settings by core property setdll/MsRdpClient.cpp— SSPI session scope lifetime; discard captured PIN on non-certificate logonsdll/CMakeLists.txt— linkcrypt32(DPAPI)include/MsRdpEx/Sspi.h— begin/end session declarationsREADME.md— documents the RDP option, behavior, and debug env varValidation
MsRdpEx.dllRelease/x64 — links cleanly, no new warnings.mstsc.exe) modes; certificate-only (no PIN) still prompts and connects; connect and reconnect both work.KERB_SMARTCARD_CSP_INFO(CspData) for multi-reader / multi-card disambiguation (tracked with a TODO).