A third-party identity provider for PKU2U
On the accepting side, PKU2U does not decide alone who the peer is. It asks the identity provider that owns the peer certificate, and builds the token from its answer. Windows has two of them: Microsoft Entra ID (through CloudAP) and a local provider, disabled by default. With the local one, the certificate must name an existing local account. A third provider can be registered from a plain security package, for users issued by a custom CA, with their own SIDs and no local account. This page shows how. The proof of concept is on GitHub.
DummyFunction6. This page covers Windows 11 24H2 and 25H2. The sample provider accepts every certificate mapped to it and makes the user a local administrator: use it in a lab VM.
Two steps, in two different DLLs:
- Which provider owns the certificate.
certpoleng!IntPstMapCertificateToProviderreads the providers underHKLM\SOFTWARE\Microsoft\IdentityStore\Providers. Each one has aTrustedRootContainer, the name of a LocalMachine certificate store. If the issuer of the certificate is in this store, the certificate belongs to the provider. A provider is skipped unlessLoadParameters\{guid}\Enabled = 1(Entra ID is the only exception). - Who the peer is. When
Pku2uCreatePacbuilds the PAC, it calls entry 1 of theLsaGetInterface(1003)table,lsasrv!LsapAcceptPeerCertificate. lsasrv finds the provider registered with that GUID and calls the function at offset0x78of its table:AcceptPeerCertificate. The provider returns anLSA_TOKEN_INFORMATION_V2and a nameDOMAIN\user. pku2u puts them in the PAC, and the token comes from there.
A provider therefore needs two things: a registry key for certpoleng, and a provider registered in lsasrv with the same GUID.
CloudAP registers its plug-ins (the Entra ID one is aadcloudap.dll) through the LSA function table it receives like any package. The entry is named DummyFunction6 in ntsecpkg.h, at offset 0x1A8 on x64. In lsasrv it is LsapRegisterIdentityProvider:
NTSTATUS NTAPI LsapRegisterIdentityProvider(
const GUID* ProviderGuid, // must exist under IdentityStore\Providers
PSID ProviderSid, // checked with RtlValidSid
ULONG Flags, // only bit 0 is kept
PVOID Table, // a size, then 16 callbacks (0x88 bytes)
PVOID* HostTable, // out: a copy of lsasrv's host table
PVOID* Provider); // out: first argument of every callback
Nothing checks who calls it: any package loaded in LSASS can call it from SpInitialize. The table has the layout of cloudAP!g_CloudAPIdProvTable. pku2u only needs the 15th callback:
typedef struct _IDPROV_PEER_TOKEN {
ULONG TokenInformationType; // LsaTokenInformationV2
PLSA_TOKEN_INFORMATION_V2 TokenInformation;
PWSTR Name; // "MYIDP\alice"
} IDPROV_PEER_TOKEN; // 3 blocks of LSA heap, freed by pku2u
NTSTATUS NTAPI AcceptPeerCertificate(PVOID Provider, ULONG Flags,
PCCERT_CONTEXT Certificate, IDPROV_PEER_TOKEN** Token);
When Flags is not zero, lsasrv only checks that the provider accepts the certificate: CloudAP returns success with no token, and the sample does the same. The other 15 callbacks in the sample's table return STATUS_NOT_SUPPORTED; pku2u never calls them.
The sample provider gives the peer MYIDP\<CN> and this token:
| SID | Name | |
|---|---|---|
| User | S-1-5-110-2026-10-22-<rid> | MYIDP\alice |
| Primary group | S-1-5-110-2026-10-22-513 | MYIDP\Users |
| Groups | S-1-5-32-544, S-1-5-32-545 | BUILTIN\Administrators, BUILTIN\Users |
The RID is a hash (FNV-1a) of the CN, so a user keeps the same SID on every machine and every logon.
Out of the box, these SIDs show as (no name) everywhere: nothing in LSA can translate them. Windows has an API for that, used by NT SERVICE\... and IIS APPPOOL\...: LsaManageSidNameMapping in advapi32. It only accepts SIDs from S-1-5-80 to S-1-5-111, hence S-1-5-110. The mappings live in memory only, so the package adds them again at each start (the domain and Users) and for each accepted peer, from a work item: calling the lookup service from inside an LSA callback could wait on itself.
At boot the package is loaded before the LSA lookup server is ready, and the call fails with RPC_NT_INVALID_BINDING (0xC0020003). The work item first waits for the event LSA_RPC_SERVER_ACTIVE, like services.exe does.
Everything is on the accepting machine. The client does nothing special: it is the usual PKU2U setup, a certificate in Credential Manager (see PKU2U internals).
HKLM\SOFTWARE\Microsoft\IdentityStore\Providers\{5A0C2E61-7D4B-4C1A-9F3E-2B8D6E1A4C70}
Name REG_SZ MYIDP
TrustedRootContainer REG_SZ MyIdp Cert Issuer
HKLM\SOFTWARE\Microsoft\IdentityStore\LoadParameters\{5A0C2E61-7D4B-4C1A-9F3E-2B8D6E1A4C70}
Enabled REG_DWORD 1
- The provider key.
Providersbelongs to TrustedInstaller: even SYSTEM cannot create a subkey. The ACL can stay as it is: withSeRestorePrivilegeenabled,RegCreateKeyExwithREG_OPTION_BACKUP_RESTOREdoes not check the DACL. - The CA. Its certificate goes in
LocalMachine\MyIdp Cert Issuer. If it is also inLocal Cert Issuer, remove it from there, else the built-in local provider takes the certificate first. - The package. The DLL is not signed, so LSA protection must be off (
RunAsPPL = 0and a reboot). Then addPku2uIdptoLsa\Security Packagesand reboot, or load it right away withAddSecurityPackage.
An SSPI server running as SYSTEM accepts a PKU2U client whose certificate says CN=alice. There is no account alice on the machine. The provider log (C:\Windows\Temp\Pku2uIdp.txt, times removed):
LsaManageSidNameMapping(MYIDP\ = S-1-5-110) = 0x00000000, error 0 LsaManageSidNameMapping(MYIDP\Users = S-1-5-110-2026-10-22-513) = 0x00000000, error 0 AcceptPeerCertificate flags 0x0, CN=alice -> MYIDP\alice, RID 0x56979887, + BUILTIN\Administrators
The server gets a token for MYIDP\alice, primary group MYIDP\Users. SID lookups on the machine return these names, also after a reboot with the package loaded from Security Packages.
- Interactive logon through CloudAP.
cloudAP!LoadPlugintakes any provider withEnabledand anApPluginDLLPathvalue under its key, and then would callCloudAPPluginInitialize. It never gets there: the DLL is loaded withLOAD_LIBRARY_REQUIRE_SIGNED_TARGET, and the load fails with error 577, for an unsigned DLL and for a DLL signed by a lab CA trusted in Root and TrustedPublisher, even with LSA protection off. Only Microsoft-signed plug-ins are accepted. For the logon screen, the way is an authentication package of your own and a credential provider. What a plug-in would have to implement is described in Inside CloudAP: what a third-party plug-in needs. - Domain controllers. lsasrv does not register interface 1003 on a DC: pku2u has no provider at all.
- The other callbacks (profile loaded, SID lookups, certificate renewal...). Only their names are known, from CloudAP's symbols.