Skip to content

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

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

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>
@thenextman

Copy link
Copy Markdown
Member

Superseded by #172, which squashes this work plus the follow-up changes into a single commit: extends the workaround to the in-process RDP ActiveX control (synthesis from the session's marshaled certificate user name + PIN), hardens session correlation and PIN handling, fixes reconnect, keeps it inert for certificate-only logons, and decouples it from PasswordContainsSCardPin. Validated end-to-end against smart card hardware.

Richard Markiewicz (thenextman) added a commit that referenced this pull request Jul 3, 2026
## 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.h` —
`KerbCertificateLogon` 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).

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>
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