ee/wcp: migrate to rust - #1374
Merged
Merged
Conversation
Documents the crate layout and behavior parity requirements for the Rust/CEF credential provider replacing ak_cred_provider.
Sets up the credprovider/cef-host/e2e member crates and implements the shared IPC protocol (protobuf-framed AuthResult/CancelSignal messages) and tile/window appearance constants used by both.
Ports the credential-provider COM object model (ClassFactory, Provider, Credential) to Rust on the windows crate: the real 4-field tile layout with embedded icon, one credential per enumerated Windows user, local/domain GetSerialization branching, and Connect() driving the CEF host process over the wire IPC protocol with QueryContinue-based cancellation. Windows syscalls with side effects sit behind narrow traits for unit testing. Widens ak_ffi's sys_caps/sys_auth_start_async/sys_auth_url to plain pub Rust functions so callers outside the cxx bridge can use them directly, and drops DllRegisterServer/DllUnregisterServer since the MSI installer owns all registry setup.
Builds ak_cef.exe on the cef crate: branches on CEF's own --type= subprocess re-exec vs. the top-level host launch from credprovider, opens a CEF Views window matching the current 560x670 centered/framed/ fixed-size/icon appearance, injects the auth header on every request, and intercepts the goauthentik.io:// redirect to complete the sign-in flow over the wire protocol. Also adds ee/wcp-rs build tooling (Makefile/build.ps1) that builds the workspace, pulls the CEF runtime via export-cef-dir, and assembles the same bin/wcp/ output shape Package.wxs already expects.
Adds a mock of ak-sysd's Ping/SystemAuthInteractive/SystemAuthToken gRPC endpoints on the real named pipe path, and a COM-driving test that loads the built ak_cred_provider.dll directly and checks the tile's field descriptors against wire::TILE_FIELDS under CPUS_CREDUI.
Windows CI already runs make from a bash-compatible shell for the rest of the repo, so a separate PowerShell script isn't needed here.
Discovered on this machine's real ak_sysd install: freeing the pipe for the mock server needs admin rights a normal shell doesn't have.
ak_cred_provider/cefexe/cefsimple/libcef_dll and the vendored CEF SDK are superseded by ee/wcp-rs. ak_lsa and ak_common are also removed: ak_lsa was never invoked by the credential provider's logon path, and ak_common existed only to serve ak_cred_provider and ak_lsa. With no C++ consumer left, ak-ffi's cxx bridge (added only to expose sys_caps/sys_auth_start_async/sys_auth_url to C++) is removed too; those become plain pub fns used directly by credprovider/cef-host. CI: drop the wcp-specific _build-ee-wcp.yml workflow and the now-empty ee/wcp C++ lint job; ee/wcp-rs builds as a windows-only target in the standard _build-rs.yml matrix instead, keeping the "wcp" artifact name package-windows.yml already expects. Adds a vpkg/windows/local target (mirroring vpkg/macos's) to build the Windows installer entirely from local sources for test-machine installs.
Now that the credential provider is the only implementation at this path, the -rs suffix that used to distinguish it from the C++ version no longer serves a purpose.
Contributor
|
🚀 Deployed on https://pr-1374--authentik-pkg.netlify.app |
Without this the binary links as a console subsystem app, so Windows allocates a console window for it -- once for the host and again for every CEF renderer/GPU/utility re-exec. Logging already goes to the platform log rather than stdout, so nothing needs a console. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ak-ffi existed to expose sys_caps/sys_auth_start_async/sys_auth_url to the old C++ provider over a cxx bridge. The bridge went when the C++ tree did, leaving a root-workspace crate whose only consumers were credprovider and cef-host. Delete it and move the three functions into a new ee/wcp workspace member, sysd-client. Nothing outside ee/wcp referenced it. The redirect token query parameter moves to wire alongside REDIRECT_PREFIX and AUTH_HEADER_NAME so the client and the test harness share one definition. Fill in the remaining testing item from RUST_CEF_PLAN.md: a process-level e2e driving the real DLL through Connect()/ak_cef.exe down to GetSerialization, covering a completed sign-in and a QueryContinue cancellation, and asserting the auth header is injected on every request. It uses a synthetic ICredentialProviderUserArray with a non-local account, so it needs no throwaway Windows account; the local-account password-reset path remains manual-checklist only. Two things had to give for that to be drivable: - Connect() could only ever spawn ak_cef.exe as SYSTEM, since acquire_interactive_token needs SE_TCB_NAME. Allow a fallback to a plain CreateProcessW in the caller's own session under CPUS_CREDUI only -- the debug-gated scenario that already runs on an ordinary desktop. The real logon scenarios keep requiring a real interactive-session token; taking the fallback there would put Chromium on the secure desktop with SYSTEM's token. - SetUsageScenario only accepts CPUS_CREDUI when the cached capabilities carry `debug`, but sys_caps only ever writes `debug: false`, so nothing set that flag and the pre-existing field_descriptors test could not have passed on a clean machine. The harness now seeds the key and restores it on drop. Because they bind the real ak-sysd pipe and write real HKLM state, the process-level tests are opt-in behind AK_WCP_E2E=1 and skip otherwise, which keeps `make test` meaningful on a dev machine. Everything else is hermetic and always runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sysd-client was a crate for two callers with nothing in common: sys_caps is only ever called by the DLL, and sys_auth_start_async/sys_auth_url only by the browser host. They share no code beyond ak_platform's grpc_request and wire's redirect constants, so a shared crate bought nothing. Each half now sits in a `sysd` module in the crate that uses it. e2e's harness declares the capability-cache key and struct itself, since it drives credprovider through LoadLibraryW rather than by linking it (a cdylib); drift there fails loudly rather than silently, because a mismatched field makes sys_caps fail to decode what the harness wrote. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The four wcp crates were their own workspace so that Windows-only code stayed out of the Linux root workspace. Fold them into the root workspace instead: one lockfile, one dependency graph. That costs two accommodations, both of which the separate workspace was avoiding: - `make lint-rs` runs `cargo clippy --workspace` on ubuntu, which cannot build credprovider (Win32-only) or cef-host (build script fetches and compiles CEF). It now passes --exclude for those and e2e off Windows; `make ee/wcp/lint` covers all four on Windows. wire is pure prost and stays linted everywhere. - windows/windows-core are pinned at 0.61 in [workspace.dependencies] for these crates while ak-sysd keeps its inline 0.62. Both resolve side by side; unifying means porting the provider to 0.62's #[implement] and UI::Shell surface, which isn't worth coupling to this move. ee/wcp is now also a windows-2025-only target in test.yml's rust matrix, so the process-level e2e tests actually run in CI and report coverage to codecov like every other rust target. `make ee/wcp/test` sets AK_WCP_E2E=1 and uses llvm-cov's show-env flow rather than $(call cargo_test,...): the e2e tests LoadLibraryW the real DLL and spawn the real exe, and nextest on its own only builds test harnesses, never the cdylib and bin artifacts. The test recipe is one chained shell command because .ONESHELL: is a no-op on the GNU Make 3.81 in GnuWin32, so cd/export/source would not carry across recipe lines. Also runs rustfmt over these crates, which had drifted while they were in a workspace that `cargo fmt --all` never reached. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All three DLL-loading tests aborted in CI with STATUS_ACCESS_VIOLATION. Rust runs a type's own Drop::drop before dropping its fields, so LoadedProvider::drop called FreeLibrary and unmapped the DLL, and only then did the ICredentialProvider field drop and call COM Release() -- through a vtable in freed pages. That matches the symptoms exactly: field_descriptors aborted with no assertion failure at all (its body had completed), and the two sign_in_flow tests aborted after their panics, during unwind. Hold the interface in an Option and take it in Drop before FreeLibrary, and expose it via provider() so it can't outlive the mapping. Also makes field_descriptors hermetic. GetFieldDescriptorCount/At read a static table and never consult the usage scenario, so the test needs no SetUsageScenario -- and therefore no mock ak-sysd and no HKLM seeding. It now runs on a plain `cargo test`, which is how this was reproduced locally: the opt-in gate meant it had only ever been observed skipping. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lback The CI e2e failure reported only "Sign-in failed. Please try again.", the one generic string the provider shows a user. The real reason went to the Windows event log, because the DLL initialises logging with allow_stdout(false) -- so a test runner captures nothing. Debug builds now log to stdout as well. That is what e2e runs, and it is the only way a failure inside the DLL reaches a test runner's output. Release builds are unchanged: LogonUI has no console. Adds a unit test that drives the real attribute-list/handle-inheritance/ CreateProcess path against a throwaway target, needing no interactive token, no elevation and nothing on the ak-sysd pipe. It passes locally, which rules out the spawn machinery being generically broken and pins the CI failure to something environmental. The likeliest such difference is a runner that holds SE_TCB_NAME, where acquire_interactive_token() succeeds and CreateProcessAsUserW is used -- a path the CPUS_CREDUI fallback did not cover, since it only triggered when acquiring the token failed. Holding a token is not the same as being allowed to assign it: without SE_ASSIGNPRIMARYTOKEN, CreateProcessAsUserW fails where a plain CreateProcessW in this session would work. Retry there rather than failing the flow. Still unreachable for CPUS_LOGON/CPUS_UNLOCK_WORKSTATION. This last part is precautionary, not a proven fix -- the stdout logging is what will name the actual cause on the next run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…platform into ee/wcp/rs-cef-fresh
…phic value' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Jens L. <jens@beryju.org>
The user agent was built from CARGO_PKG_VERSION, which is the crate version rather than the build's; ak-meta::full_version() is what the rest of the platform reports and carries the build hash with it. Sentry is initialised before the --type branch so the renderer/GPU/utility re-execs report too. A crash in one of those is otherwise invisible, which is the position the secure-desktop failure has been in all along.
Design only, nothing implemented. Covers the authentik backend, ak-sysd and ee/wcp changes needed to sign in at the logon screen with a security key when the server is unreachable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
hmac-secret only works against a credential created on that authenticator, so enrolment is a real makeCredential ceremony and the key must be physically present. Nothing the backend holds substitutes for it. "Automatic" therefore means automatically prompted and retried, not silent. Primary trigger moves to the user's desktop session via the agent, where the key can be plugged in comfortably and none of the secure-desktop constraints apply; opportunistic enrolment at the logon screen becomes a secondary trigger once the tile is a state machine. That also fixes the payload: sealing the live Windows password would force enrolment to coincide with a password rotation, i.e. require the key at a logon -- the one moment we cannot rely on it being present. Seal a random offline_root instead and derive the password from it offline, which needs the key exactly once, at a time of the user's choosing. Records that forcing the online logon through authentik's WebAuthn stage is not a viable way to guarantee presence: it runs in the CEF window, and Chromium's WebAuthn wants a foreground interactive window, which LogonUI's secure desktop is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
winlogon_token_for_session broke out of the snapshot at the first winlogon.exe it saw, whatever session that one was in. There is one per session, so as soon as the enumeration reached another session's copy first the search gave up and the spawn failed with 0x80004005 — which is what a logoff/logon cycle produces once the console session id has moved on. Search to the end, and say which sessions were seen when nothing usable turns up.
…_CREDUI Every existing test reaches ak_cred_provider.dll via LoadLibraryW + DllGetClassObject, called directly -- bypassing the registry entirely. That is not the path LogonUI or CredUI use: they find the CLSID's InprocServer32 entry and let ole32 load and instantiate it. Nothing in this suite would catch a wrong or missing registry value. Add registration::RegisteredProvider, which writes the same HKLM keys the MSI installer does (vpkg/windows/Package.wxs: CLSID/InprocServer32/ ThreadingModel, plus the Credential Providers key), and a new test that activates through CoCreateInstance under CPUS_CREDUI and drives the same completed-sign-in flow as sign_in_flow.rs. It refuses to run rather than overwrite an existing registration -- verified against this machine, which turned out to have the real product installed; the guard caught it correctly, and the registration format matches the real install exactly. Real activation makes the DLL an STA object (ThreadingModel=Apartment), so every call has to stay on the one OS thread that called CoInitializeEx -- the new test uses flavor="current_thread" rather than "multi_thread" so a tokio worker migration across an .await can't hand the interface to a thread COM never blessed for it. ComGuard scopes CoInitializeEx/CoUninitialize to the test; declaring it before any COM interface relies on Rust's reverse-declaration-order drop to release everything before uninitializing. get_serialization moves from sign_in_flow.rs into dll.rs so both tests share it, since it's generic over any ICredentialProviderCredential regardless of how the provider was activated. Not run end-to-end here: this sandbox has no interactive session to grant UAC consent, and the real install already on this machine means the un-elevated run only exercises the refusal path (which it does correctly). The registration-write and CoCreateInstance-activation code is unexercised under elevation; needs a run on a clean elevated machine to confirm. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
wipe_browser_state's doc comment had drifted long across the last few real-machine fixes; tighten it. app.rs's diff is cargo fmt reflowing a struct literal that now exceeds the line width, unrelated to the comment change but picked up by the same pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…platform into ee/wcp/rs-cef-fresh
Confirmed against the actual CI run (goauthentik/platform#31906761982, job 95065724430): every other test finished, then registered_provider_completes_sign_in_via_real_com_activation sat in nextest's SLOW list past 1560s with nothing after it able to run (--test-threads 1). Root cause: the test used flavor = "current_thread", but mock_sysd::start spawns the mock ak-sysd server as its own tokio task, and Connect() blocks synchronously -- no .await anywhere under it -- for as long as the real ak_cef.exe takes to complete its round trip against that mock. On a current_thread runtime that block starves the only OS thread the runtime has, so the mock server's task can never be polled again to answer ak_cef.exe's real gRPC calls. Full deadlock: ak_cef.exe waits on a response that can now never come, Connect() waits on ak_cef.exe, and nothing times out on either side (query_continue(None) never cancels). Switch to flavor = "multi_thread", matching sign_in_flow.rs, so the mock server runs on a separate worker thread from the one Connect() blocks. Apartment-affinity safety (the reason current_thread was chosen) now comes from ordering instead: the test's one .await (mock_sysd::start) is moved before CoInitializeEx, so nothing after the COM apartment is created can yield and let tokio migrate the task to a different worker mid-flight. Verified locally that the non-elevated refusal path (this machine already has the product installed) still completes in under a second, i.e. this change didn't introduce a new hang on that path. The happy path itself (elevated, clean machine) is still unverified here -- this sandbox has no interactive session for UAC and no GitHub auth configured to pull the raw CI log, so the fix is confirmed against the reasoning and the SLOW-list evidence, not a green re-run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The window opened, but behind whatever already had focus -- needed alt+tab to find it. This worked in earlier builds; the layered fixes since (sentry init, browser-state wipe, sandbox/GPU changes, and especially posting window creation as a deferred task instead of doing it inline to fix a crash) added enough latency between credprovider spawning ak_cef.exe and the window actually appearing that Windows no longer treats it as belonging to that launch. Foreground activation is time-gated: past a short grace period from process creation, a process's own SetForegroundWindow-equivalent calls silently no-op, no matter what the process itself does. Two-sided fix, matching the standard Win32 pattern for this: - credprovider (ipc.rs, spawn_cef_host): call AllowSetForegroundWindow(pi.dwProcessId) right after a successful spawn. This is what actually removes the time limit -- the parent explicitly grants the child foreground rights regardless of how long it then takes to show a window. - cef-host (window.rs): call Window::activate() alongside the existing show(), since show() only makes the window visible, not focused -- activate() is CEF's dedicated bring-to-front-and-focus call. Neither one alone is sufficient: activate() without the grant still no-ops past the timing window, and the grant without an explicit activation call leaves whatever CEF's Show() does on Windows (ShowWindow, not SetForegroundWindow) as the only thing asking for focus. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neither this implementation nor the C++ has ever run the browser sandboxed, and on a fresh logon both run it as SYSTEM — the existing token machinery works hard to arrive at exactly that, and is broken on unlock. Records the two ways out, enabling the CEF sandbox and launching as a dedicated installer-created account, along with the decisions that shape them: whether integrated auth is ever in scope (it rules out S4U and forces stored credentials), and whether a non-SYSTEM account may be granted access to the logon desktop.
The obvious replacements for randomising the local account's password have both been ruled out, and one of them the hard way: ak_lsa was a working LSA authentication package that was abandoned over Microsoft's signing requirements for LSA plugins, which a third party cannot satisfy and which LSA protection makes more binding over time, not less. Records that as closed rather than unfinished, so it does not get proposed again, along with why MSV1_0_S4U_LOGON cannot substitute (a credential provider does not perform the logon) and what is actually left: certificate logon for domain-joined, the password reset for local accounts. Flags the DPAPI cost of the reset as unverified and worth measuring.
# Conflicts: # Cargo.lock # ee/wcp/ak_common/include/ak_version.h
BeryJu
marked this pull request as ready for review
August 15, 2026 23:03
BeryJu
added a commit
that referenced
this pull request
Aug 16, 2026
The branch's earlier rebase onto main replayed these commits onto main's actual (simpler) syscalls.rs/credential.rs -- the one merged via #1374/#1378 -- but syscalls.rs still carried PasswordStore, KeyringPasswordStore, ak_platform_keyring and the richer LocalAccountPassword trait from a different, unmerged branch (ee/wcp/stored-pw) that this branch was never meant to depend on. credential.rs never had that machinery to begin with here, so the mismatch didn't compile (LocalAccountPasswordReset vs LocalAccountPassword, missing ak_platform_keyring). Rebuilds the service-account additions directly on main's actual LocalAccountPasswordReset/reset-only API instead. No behavior change to any of this branch's own logic -- ensure_service_account_password_rotated still calls the same single reset, just through the trait that actually exists here.
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.