Skip to content

Add opt-in KERB_CERTIFICATE_LOGON SSPI rewrite for smart card logon#171

Draft
Marc-André Moreau (mamoreau-devolutions) wants to merge 1 commit into
masterfrom
kerb-cert-logon-hooking
Draft

Add opt-in KERB_CERTIFICATE_LOGON SSPI rewrite for smart card logon#171
Marc-André Moreau (mamoreau-devolutions) wants to merge 1 commit into
masterfrom
kerb-cert-logon-hooking

Conversation

@mamoreau-devolutions

Copy link
Copy Markdown
Contributor

Summary

Implements the client-side workaround for smart card certificate logon failures over RDP, entirely by extending MsRdpEx's existing SSPI API hooking — no LSASS patching and no internal mstscax offset hooks.

Problem

When a smart card certificate thumbprint and PIN are both pre-supplied (e.g. by a connection manager that stores them and sets PasswordContainsSCardPin:i:1), 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). The interactive Windows prompt avoids this because it builds a packed KERB_CERTIFICATE_LOGON.

Solution

When KerbCertificateLogon:i:1 is set on a session, the AcquireCredentialsHandleW detour rewrites a marshaled smart card certificate + PIN CredSSP credential into the equivalent CredsspCertificateCreds / KERB_CERTIFICATE_LOGON packed buffer before it is handed to LSASS. CREDSSP_CRED_EX wrapping is preserved when present.

Key design points

  • Per-session isolation reuses the existing instance/session machinery. A new thread-local SSPI session scope is bracketed around CMsRdpClient::raw_Connect() (with a guarded, non-ambiguous global fallback), so the hook resolves the owning CMsRdpExtendedSettings without affecting other concurrent sessions.
  • Builder: CredUnPackAuthenticationBufferW → validate marshaled CertCredentialCredPackAuthenticationBufferW, verifying the first DWORD is KerbCertificateLogon.
  • Explicit opt-in only — never auto-triggered by PasswordContainsSCardPin. Unknown / already-correct credential shapes pass through unchanged.
  • No secret leakage — no PINs, passwords, or certificate bytes are logged; PIN-bearing buffers are zeroed before free. Optional secret-free diagnostics gated behind MSRDPEX_SSPI_SMARTCARD_DEBUG=1.

Files changed

  • dll/Sspi.cpp — session-scope infra, diagnostics, KERB_CERTIFICATE_LOGON builder, wired-in rewrite
  • dll/MsRdpClient.cpp — SSPI session begin/end around raw_Connect()
  • dll/RdpSettings.cpp, include/MsRdpEx/RdpSettings.hKerbCertificateLogon opt-in + PasswordContainsSCardPin state
  • include/MsRdpEx/Sspi.h — begin/end session declarations
  • README.md — documents the new RDP option, behavior, and debug env var

Validation

  • Built MsRdpEx.dll Release/x64 — links cleanly, no new warnings from the changes.
  • Not yet done (requires hardware): runtime validation against a real smart card + PIN + KDC scenario. The rewrite is fully guarded and passes through unchanged on any unrecognized shape, so it is safe to ship for live testing with MSRDPEX_SSPI_SMARTCARD_DEBUG=1.

Draft pending smart card hardware validation.

Smart card logon via RDP fails when a certificate thumbprint and PIN are
both pre-supplied: 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-backed keys.

Implement the client-side workaround by extending the existing SSPI API
hooking instead of patching LSASS. The AcquireCredentialsHandleW detour now,
strictly per session and only when KerbCertificateLogon:i:1 is set, rewrites
a marshaled smart card certificate credential into the equivalent
CredsspCertificateCreds / KERB_CERTIFICATE_LOGON packed buffer before it is
handed to LSASS. CREDSSP_CRED_EX wrapping is preserved when present.

Per-session isolation reuses the existing instance/session machinery: a new
thread-local SSPI session scope is bracketed around CMsRdpClient::raw_Connect()
(with a guarded, non-ambiguous global fallback) so the hook can resolve the
owning CMsRdpExtendedSettings without affecting other concurrent sessions.

Safety: rewrite is opt-in only, never triggered automatically by
PasswordContainsSCardPin; unknown credential shapes pass through unchanged;
no PINs, passwords, or certificate bytes are logged; PIN-bearing buffers are
zeroed before free. Optional secret-free diagnostics are gated behind
MSRDPEX_SSPI_SMARTCARD_DEBUG.

Co-authored-by: Copilot App <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

Development

Successfully merging this pull request may close these issues.

3 participants