Skip to content

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.

Undocumented, and a test only. The function used to register the provider is an entry of the LSA function table that the SDK calls 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.
How pku2u asks a provider

Two steps, in two different DLLs:

  • Which provider owns the certificate. certpoleng!IntPstMapCertificateToProvider reads the providers under HKLM\SOFTWARE\Microsoft\IdentityStore\Providers. Each one has a TrustedRootContainer, 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 unless LoadParameters\{guid}\Enabled = 1 (Entra ID is the only exception).
  • Who the peer is. When Pku2uCreatePac builds the PAC, it calls entry 1 of the LsaGetInterface(1003) table, lsasrv!LsapAcceptPeerCertificate. lsasrv finds the provider registered with that GUID and calls the function at offset 0x78 of its table: AcceptPeerCertificate. The provider returns an LSA_TOKEN_INFORMATION_V2 and a name DOMAIN\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.

Registering a provider: DummyFunction6

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 token, and names for the SIDs

The sample provider gives the peer MYIDP\<CN> and this token:

SIDName
UserS-1-5-110-2026-10-22-<rid>MYIDP\alice
Primary groupS-1-5-110-2026-10-22-513MYIDP\Users
GroupsS-1-5-32-544, S-1-5-32-545BUILTIN\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.

Setting it up

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
  1. The provider key. Providers belongs to TrustedInstaller: even SYSTEM cannot create a subkey. The ACL can stay as it is: with SeRestorePrivilege enabled, RegCreateKeyEx with REG_OPTION_BACKUP_RESTORE does not check the DACL.
  2. The CA. Its certificate goes in LocalMachine\MyIdp Cert Issuer. If it is also in Local Cert Issuer, remove it from there, else the built-in local provider takes the certificate first.
  3. The package. The DLL is not signed, so LSA protection must be off (RunAsPPL = 0 and a reboot). Then add Pku2uIdp to Lsa\Security Packages and reboot, or load it right away with AddSecurityPackage.
Result

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.

What does not work
  • Interactive logon through CloudAP. cloudAP!LoadPlugin takes any provider with Enabled and an ApPluginDLLPath value under its key, and then would call CloudAPPluginInitialize. It never gets there: the DLL is loaded with LOAD_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.
Need PKU2U or certificate logon with your own identities? Contact us.