I recently discovered the windows-rs framework and have been looking to build a Windows Credential Provider in Rust by implementing their ICredentialProvider COM interface.
I've been working on a proof-of-concept implementation using the information put together under one of the existing issues, but I'm not sure how to actually expose the compiled rust as a proper DLL to then register with the windows system.
use std::cell::RefCell;
use windows::{
core::implement,
Win32::UI::Shell::{ICredentialProvider, ICredentialProvider_Impl},
};
fn main() -> windows::core::Result<()> {
#[implement(ICredentialProvider)]
struct Provider {
mutable_state: RefCell<u32>,
}
impl Provider {
fn new() -> Self {
Self {
mutable_state: RefCell::new(0),
}
}
}
impl ICredentialProvider_Impl for Provider {
fn Advise(
&self,
pcpe: &core::option::Option<windows::Win32::UI::Shell::ICredentialProviderEvents>,
upadvisecontext: usize,
) -> windows::core::Result<()> {
*self.mutable_state.borrow_mut() = 42;
todo!();
}
fn GetCredentialAt(
&self,
dwindex: u32,
) -> windows::core::Result<windows::Win32::UI::Shell::ICredentialProviderCredential>
{
todo!();
}
fn GetCredentialCount(
&self,
pdwcount: *mut u32,
pdwdefault: *mut u32,
pbautologonwithdefault: *mut windows::Win32::Foundation::BOOL,
) -> windows::core::Result<()> {
todo!();
}
fn GetFieldDescriptorAt(
&self,
dwindex: u32,
) -> windows::core::Result<
*mut windows::Win32::UI::Shell::CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR,
> {
todo!();
}
fn GetFieldDescriptorCount(&self) -> windows::core::Result<u32> {
todo!();
}
fn SetSerialization(
&self,
pcpcs: *const windows::Win32::UI::Shell::CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION,
) -> windows::core::Result<()> {
todo!();
}
fn SetUsageScenario(
&self,
cpus: windows::Win32::UI::Shell::CREDENTIAL_PROVIDER_USAGE_SCENARIO,
dwflags: u32,
) -> windows::core::Result<()> {
todo!();
}
fn UnAdvise(&self) -> windows::core::Result<()> {
todo!();
}
}
Ok(())
}
I compiled a Sample Credential Provider written in C++, supplied by Windows in their SDK, and used a tool to view the exported functions available in the generated DLL
There have been similar efforts in the windows-rs community to expose rust for WinRT but this COM interface needed for the Credential Provider is different enough I don't really know where to begin.
Are there any rust tricks to generate a similar DLL that can expose my interface and make it available to windows? Any help is appreciated.
A credential provider needs to be implemented as a COM server. A COM server is a PE image (EXE or DLL) that provides the following exports:
DllGetClassObject
is where the magic happens: It checks to see if the requested class (identified by rclsid
) is implemented by the module, and subsequently returns a pointer to the requested interface riid
(commonly an IClassFactory
or IClassFactory2
interface). Once a client (the operating system, in case of a credential provider) received a class factory, it can use it to instantiate objects that implement interfaces such as the ICredentialProvider
interface.
The key point here is that those two exports are all that's required for a module to be a COM server, that can expose arbitrary interfaces2.
That covers the systemic invariants. Moving into Rust the following issues need to be addressed:
First up, let's create a library crate. The following will do:
cargo new --lib cp_demo
Add this to Cargo.toml (removing anything beyond the [package]
table):
[lib]
crate-type = ["cdylib"]
That addresses the first issue by having the crate produce a DLL. Running cargo build
will produce cp_demo.dll inside targets/debug (assuming default settings). Mind you, this isn't making forward progress just yet. It merely creates a DLL that doesn't export anything (evidenced by running dumpbin /EXPORTS targets\debug\cp_demo.dll
). It merely removes the obligation to have a fn main()
.
Having an "empty" DLL in place, we'll want to expose functionality to the outside world. The mechanics are very similar to the referenced resources, requiring a combination of an extern "system"
block as well as the no_mangle
attribute, so that the exports can be discovered by the system by name.
The former designates the calling convention, the contract between callers (the system) and callees (the implementation), formalizing how arguments are passed and who is responsible for (stack) cleanup on return. The latter instructs the linker to keep the exported symbols undecorated, so that the system can discover them by calling GetProcAddress(hmod, "DllGetClassObject")
, for example.
Dumping the following into src/lib.rs
use std::{ffi, ptr};
use windows::{
core::{GUID, HRESULT},
Win32::Foundation::{CLASS_E_CLASSNOTAVAILABLE, E_POINTER, S_OK},
};
#[no_mangle]
extern "system" fn DllGetClassObject(
_rclsid: *const GUID,
_riid: *const GUID,
ppv: *mut *mut ffi::c_void,
) -> HRESULT {
// Implement basic COM contract
if ppv.is_null() {
E_POINTER
} else {
unsafe { *ppv = ptr::null_mut() };
CLASS_E_CLASSNOTAVAILABLE
}
}
#[no_mangle]
extern "system" fn DllCanUnloadNow() -> HRESULT {
// It's always safe to unload this module
S_OK
}
and updating Cargo.toml to include
[dependencies.windows]
version = "0.44.0"
features = [
"Win32_Foundation",
]
complies into a credential provider skeleton DLL. Running dumpbin /EXPORTS targets\debug\cp_demo.dll
again produces output that includes
ordinal hint RVA name
1 0 00001080 DllCanUnloadNow = DllCanUnloadNow
2 1 00001000 DllGetClassObject = DllGetClassObject
Sweet! Now we have a credential provider that doesn't provide anything (and certainly not credentials). But it looks like a potential credential provider to the system already, that can be registered using the following .reg script (make sure to use a fresh GUID; this one won't be "globally unique" anymore by the time you're reading this).
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{DED30376-B312-4168-B2D3-2D0B3EADE513}]
@="cp_demo"
[HKEY_CLASSES_ROOT\CLSID\{DED30376-B312-4168-B2D3-2D0B3EADE513}]
@="cp_demo"
[HKEY_CLASSES_ROOT\CLSID\{DED30376-B312-4168-B2D3-2D0B3EADE513}\InprocServer32]
@="cp_demo.dll"
"ThreadingModel"="Apartment"
The interface implementations follow the pattern you've already used in the question: Declare a custom type to hold local state (if any), apply the #[implement]
attribute, and supply an implementation for the generated trait named <interface>_Impl
.
For the ICredentialProvider
interface3 this would roughly look like what is in the question. The only exception being that the following snippet replaces the todo!()
's with returning error codes, as it isn't legal for panics to cross the ABI4.
#[implement(ICredentialProvider)]
struct Provider {
_mutable_state: cell::RefCell<u32>,
}
impl Provider {
fn new() -> Self {
Self {
_mutable_state: cell::RefCell::new(0),
}
}
}
impl ICredentialProvider_Impl for Provider {
fn SetUsageScenario(
&self,
_cpus: CREDENTIAL_PROVIDER_USAGE_SCENARIO,
_dwflags: u32,
) -> Result<()> {
Err(E_NOTIMPL.into())
}
// ...
}
This isn't doing anything useful, other than failing gracefully in case the system manages to instantiate this implementation. To do this it needs an IClassFactory
implementation that does:
#[implement(IClassFactory)]
struct ProviderFactory;
impl IClassFactory_Impl for ProviderFactory {
fn CreateInstance(
&self,
punkouter: &core::option::Option<windows::core::IUnknown>,
riid: *const windows::core::GUID,
ppvobject: *mut *mut core::ffi::c_void,
) -> windows::core::Result<()> {
// Validate arguments
if ppvobject.is_null() {
return Err(E_POINTER.into());
}
unsafe { *ppvobject = ptr::null_mut() };
if riid.is_null() {
return Err(E_INVALIDARG.into());
}
let riid = unsafe { *riid };
if punkouter.is_some() {
return Err(CLASS_E_NOAGGREGATION.into());
}
// We're only handling requests for `IID_ICredentialProvider`
if riid != ICredentialProvider::IID {
return Err(E_NOINTERFACE.into());
}
// Construct credential provider and return it as an `ICredentialProvider`
// interface
let provider: ICredentialProvider = Provider::new().into();
unsafe { *ppvobject = mem::transmute(provider) };
Ok(())
}
fn LockServer(&self, _flock: windows::Win32::Foundation::BOOL) -> windows::core::Result<()> {
Err(E_NOTIMPL.into())
}
}
This represents a fully functional class factory for an ICredentialProvider
interface implemented by Provider
. The better part of CreateInstance()
consists of argument validation as mandated by the contract for IClassFactory::CreateInstance
. The actual magic happens here:
let provider: ICredentialProvider = Provider::new().into();
This is doing a lot! Provider::new()
is the obvious part: It instantiates a new Provider
object. Not nearly as obvious is what the into()
part does. It exercises the following From
trait implementation that is ultimately generated by the #[implement]
macro (cargo-expand
is an indispensable tool for uncovering those hidden details):
impl ::core::convert::From<Provider> for ICredentialProvider {
fn from(this: Provider) -> Self {
let this = Provider_Impl::new(this);
let mut this = ::core::mem::ManuallyDrop::new(::std::boxed::Box::new(this));
let vtable_ptr = &this.vtables.0;
unsafe { ::core::mem::transmute(vtable_ptr) }
}
}
It takes a Provider
instance, moves it into a (generated) Provider_Impl
, then moves that into heap storage (Box::new()
), wraps everything behind a ManuallyDrop
, and finally transmutes the object pointer into the respective interface pointer.
Every bit of this is of crucial importance: Moving the Provider
instance into heap memory makes sure that it remains valid across stack unwinding as part of function return, wrapping the Box
inside a ManuallyDrop
inhibits running the Drop
implementation as the object falls out of scope (which would otherwise decrement the reference count to 0, destroying the object along the way), and returning an interface pointer allows the system to call into an unknown implementation through a known interface.
A corollary of the latter is that object cleanup is at the discretion of the implementation: When the reference count drops to 0, as clients call through Release()
, it is the concrete implementation that frees resources (Provider_Impl::Release()
specifically), irrespective of who the caller happens to be. Provider_Impl
makes sure to match the allocation strategy used by the From
trait implementation, so we don't have to worry about this detail.
A note on the LockServer
implementation: It retuns E_NOTIMPL
, mostly because I'm not confident to have understood it's purpose. This doesn't appear to have had any adverse effect while debugging, so I'm leaving it for now (more on this later).
With all that in place, we are in a good position to socialize: We have an ICredentialProvider
implementation (Provider
), that'll faithfully respond with E_NOTIMPL
on every request, and an IClassFactory
implementation (ProviderFactory
), that'll dutifully procure as many of the aforementioned rascals as requested. What's missing in becoming BFF with the OS is to allow it to partake in our parenthood.
Detour on credential provider lookup and instantiation: Whenever the system needs to discover credential providers, it enumerates all values under the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\
key in the registry. Each value's data names a UUID (generally referred to as a "GUID" in Windows programming) that identifies a credential provider implementation.
With a GUID identifying a COM interface implementation, what follows is rather generic COM infrastructure business. This applies to all of COM, not just credential providers. The (approximately accurate) algorithm is this:
HKEY_CLASSES_ROOT\CLSID\
key. Its InprocServer32
's data contains the (fully qualified) path to the implementing module.DllGetClassObject
(the literal name is part of the contract, hence the #[no_mangle]
attribute).IID
as in "interface ID", whereas the latter is referred to as CLSID
as in "class ID").That out of the way, it should be apparent why the .reg script above writes to two different keys, and also what's required to nudge the system into acknowledging this creation of our own: Step 3.
above is the missing link, so let's fill that in:
#[no_mangle]
extern "system" fn DllGetClassObject(
rclsid: *const GUID,
riid: *const GUID,
ppv: *mut *mut ffi::c_void,
) -> HRESULT {
// The "class ID" this credential provider is identified by. This value needs to
// match the value used when registering the credential provider (see the .reg
// script above)
const CLSID_CP_DEMO: GUID = GUID::from_u128(0xDED30376_B312_4168_B2D3_2D0B3EADE513);
// Validate arguments
if ppv.is_null() {
return E_POINTER;
}
unsafe { *ppv = ptr::null_mut() };
if rclsid.is_null() || riid.is_null() {
return E_INVALIDARG;
}
let rclsid = unsafe { *rclsid };
let riid = unsafe { *riid };
// The following isn't strictly correct; a client *could* request an interface other
// than `IClassFactory::IID`, which this implementation is simply failing.
// This is safe, even if overly restrictive
if rclsid != CLSID_CP_DEMO || riid != IClassFactory::IID {
return CLASS_E_CLASSNOTAVAILABLE;
}
// Construct the factory object and return its `IClassFactory` interface
let factory: IClassFactory = ProviderFactory.into();
unsafe { *ppv = mem::transmute(factory) };
S_OK
}
#[no_mangle]
extern "system" fn DllCanUnloadNow() -> HRESULT {
// Since we aren't tracking module references (yet), it's never safe to unload this
// module
S_FALSE
}
This is following a familiar pattern: Most of DllGetClassObject()
is argument validation, with let factory: IClassFactory = ProviderFactory.into();
doing the actual work.
The only difference here being that ProviderFactory
is stateless (a "unit struct" in Rust parlance). It doesn't carry any information beyond it being a type. Which doesn't immediately appear to be useful, though it does allow us to implement traits on it (such as From
). With ProviderFactory
a unit struct expression we can call into()
on it, kicking off the same machinery as described above, leaving use with a manually managed COM object living in heap memory so that we can return a pointer to it (*ppv = mem::transmute(factory)
).
Note that the DllCanUnloadNow
implementation changed from returning S_OK
to S_FALSE
. This isn't wrong as such, but it's also not what I'd like to do1. To address this we would need to record all references into this module, incrementing the reference count on every object creation, and decrementing it whenever objects implemented by this module get destroyed. I'm not entirely sure how to do that yet.
The final step closes the loop. With the above we can compile a credential provider DLL, allow the system to discover it and load the module, request a class factory from it, and have it create a credential provider. The credential provider doesn't do anything just yet; fleshing out the ICredentialProvider
skeleton with functionality is for another update.
For debugging I've set up a VM using Hyper-V. The choice was a matter of convenience, as Hyper-V comes as an optional system component, with OS installations available for immediate download (I'm using "Windows 10 MSIX packaging environment" as the smallest option). Any other VM solution or OS installation should work the same.
TBD: VM and debugger setup
While possible to attach a debugger to the Windows logon screen, it's much more convenient to run everything under the local user's account. To that end I'd implemented a tiny test program (cp_test
) that exercises the credential provider by way of the CredUIPromptForWindowsCredentialsW
API call.
This program can be launched from a debugger, with all the credential provider business running in-process. There's no need to attach a debugger to an external process, and everything just works as you'd expect.
This is the test application:
Cargo.toml
[package]
name = "cp_test"
version = "0.0.0"
edition = "2021"
[dependencies.windows]
version = "0.44.0"
features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_Security_Credentials",
]
src/main.rs
use std::{mem, ptr};
use windows::{
w,
Win32::{
Foundation::BOOL,
Security::Credentials::{
CredUIPromptForWindowsCredentialsW, CREDUIWIN_CHECKBOX, CREDUI_INFOW,
},
},
};
fn main() {
let ui_info = CREDUI_INFOW {
cbSize: mem::size_of::<CREDUI_INFOW>() as _,
pszMessageText: w!("Enter credentials"),
pszCaptionText: w!("Testing custom credential provider"),
..Default::default()
};
let mut auth_package = 0;
let mut auth_buffer = ptr::null_mut();
let mut auth_buffer_size = 0;
let mut save = BOOL::default();
let _ = unsafe {
CredUIPromptForWindowsCredentialsW(
Some(&ui_info),
0,
&mut auth_package,
None,
0,
&mut auth_buffer,
&mut auth_buffer_size,
Some(&mut save),
CREDUIWIN_CHECKBOX,
)
};
}
If the credential provider was properly registered, the system will discover it and load the COM server into this process. Using sxe ld cp_demo.dll
in WinDbg makes sure that we break into the debugger when the module gets loaded, allowing us to easily set breakpoints as desired.
CLSID_CP_DEMO
1 This one doesn't strictly implement functionality; its purpose initially was to act as a performance optimization, allowing the implementing module to get unloaded when it's no longer needed. A conforming implementation could simply return S_FALSE
unconditionally, and opt out of this optimization, though for a credential provider it's probably wise to allow the system to unload it as soon as possible. Keeping an attack surface into high value information around for longer than necessary ain't no smart move.
2 The actual interface methods do not need to be exported; they are returned to clients by way of arrays of function pointers.
3 A full implementation will also require us to provide an implementation of ICredentialProviderCredential
, as that is returned from ICredentialProvider::GetCredentialAt
.