[Settings] Settings UI changes are dropped on machines that cannot reach Microsoft's CRL/OCSP endpoints (runner rejects PowerToys.Settings.exe as not-microsoft-signed)
Environment
- PowerToys 0.101.2362.0 (x64,
PowerToysUserSetupper-user install), Windows 11 25H2 10.0.26200.8875, AMD64, zh-CN. - The affected machine/profile has no usable access to Microsoft's PKI endpoints:
crl.microsoft.com/www.microsoft.comdo not resolve, direct HTTP fails, and the configured WinHTTP proxy is unreachable. %LOCALAPPDATA%\Microsoft\Windows\INetCache\CryptnetUrlCachedoes not exist for that profile, i.e. CryptoAPI has never successfully retrieved a CRL/OCSP/AIA object there.- The binary itself is fine:
Get-AuthenticodeSignature PowerToys.Settings.exe→Valid, signerCN=Microsoft Corporation, O=Microsoft Corporation, …; the signature is intact and the version matches the runner.
Summary
Every time the Settings UI connects to the runner's settings pipe it is rejected:
[warning] Rejected unauthenticated Settings pipe client: pid=25852 image='C:\Programs\PowerToys\WinUI3Apps\PowerToys.Settings.exe' reason=not-microsoft-signed
VerifyMicrosoftSignedMachineRoot() (added in #49527) fails on its final chain-policy call even though the binary is genuinely Microsoft-signed and untampered. As a result no Settings change ever reaches the runner: nothing is applied to the modules and nothing is persisted, so from the user's point of view "the Settings UI does nothing, and changes are reverted when the window is closed and re-opened" (the runner rewrites settings.json from its in-memory state before opening Settings, so the UI comes back with the previous values).
For a whole day of active Settings use the runner log contains apply_general_settings at startup and then only Rejected unauthenticated Settings pipe client lines — no dispatch_received_json from the Settings UI at all.
Impact
On an offline / air-gapped / restricted-proxy machine the elevated Settings pipe becomes permanently unusable, i.e. PowerToys cannot be configured at all through its UI. The only workarounds are (a) giving the machine revocation access, or (b) editing the JSON files by hand and restarting PowerToys, or (c) downgrading PowerToys.
Steps to reproduce
- Use a machine (or user profile) whose CryptoAPI URL cache has no cached revocation data for the Microsoft code-signing chain and that cannot reach Microsoft's CRL/OCSP distribution points.
- Install PowerToys 0.101.2362 and open the Settings UI (tray icon, or
PowerToys.exe --open-settings=Dashboard). - Inspect
%LOCALAPPDATA%\Microsoft\PowerToys\RunnerLogs\runner-log_<date>.log.
Expected behavior
The genuine, correctly versioned Microsoft-signed PowerToys.Settings.exe is accepted; Settings changes are applied and persisted.
Actual behavior
Rejected unauthenticated Settings pipe client: pid=… image='…\WinUI3Apps\PowerToys.Settings.exe' reason=not-microsoft-signed
Settings changes are applied nowhere and saved nowhere.
Root cause
VerifyMicrosoftSignedMachineRoot() in src/common/interop/pipe_caller_auth.cpp performs four checks; the first three pass and the fourth fails:
Directory / basename / file version — pass (the logged
reasonis the last-stage value, so these already succeeded).HasIntactAuthenticodeSignature()—WinVerifyTrust(…, WTD_REVOKE_NONE | WTD_SAFER_FLAG | WTD_CACHE_ONLY_URL_RETRIEVAL)→ success (0x0). The file is intact and Microsoft-signed.Chain build + machine-root anchoring —
CertGetCertificateChain(HCCE_LOCAL_MACHINE, leaf, NULL, <PKCS#7 store from the file>, CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL | CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT)succeeds and builds the complete chain from the certificates embedded in the Authenticode signature (leaf →Microsoft Code Signing PCA 2024→Microsoft Root Certificate Authority 2011; the intermediate comes from the embedded PKCS#7 store), but the trust status is:TrustStatus.dwErrorStatus = 0x01000040 = CERT_TRUST_IS_OFFLINE_REVOCATION | CERT_TRUST_REVOCATION_STATUS_UNKNOWNwhich is exactly what the code intends to tolerate:
const DWORD ignore = CERT_TRUST_REVOCATION_STATUS_UNKNOWN | CERT_TRUST_IS_OFFLINE_REVOCATION; if ((chain->TrustStatus.dwErrorStatus & ~ignore) == 0) // <- passes { CERT_CHAIN_POLICY_PARA policyPara = {}; policyPara.cbSize = sizeof(policyPara); // <- dwFlags stays 0! CERT_CHAIN_POLICY_STATUS policyStatus = {}; policyStatus.cbSize = sizeof(policyStatus); if (CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_AUTHENTICODE, chain, &policyPara, &policyStatus)) ok = (policyStatus.dwError == 0); }CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_AUTHENTICODE, …)returnspolicyStatus.dwError = 0x800B010E (CERT_E_REVOCATION_FAILURE)because
policyPara.dwFlags == 0, i.e. the Authenticode policy does enforce revocation, contradicting the ignore-mask that was applied one line earlier.→
ChainsToMachineRoot()returnsfalse→VerifyMicrosoftSignedMachineRoot()returnsfalse→reason = L"not-microsoft-signed"→ the connection is dropped before any command is dispatched.
In other words the whole authentication can only succeed when the local CryptoAPI cache already holds usable CRL/OCSP responses for the Microsoft code-signing chain, and the check itself is cache-only, so it will never fetch them. On a machine that cannot obtain them, the genuine Microsoft-signed PowerToys.Settings.exe is indistinguishable from an unsigned binary.
Verified against the shipped binary as well: PowerToys.Interop.dll (0.101.2362) contains all of the pipe_caller_auth.cpp reason strings (not-microsoft-signed, bad-directory, bad-basename, version-mismatch, image-path-failed, no-client-pid, pid-mismatch, open-process-failed) and imports CertVerifyCertificateChainPolicy, CertGetCertificateChain, CryptQueryObject, CertGetSubjectCertificateFromStore and WinVerifyTrust.
Suggested fix
Make the policy call consistent with the revocation status the code has already decided to tolerate:
CERT_CHAIN_POLICY_PARA policyPara = {};
policyPara.cbSize = sizeof(policyPara);
policyPara.dwFlags = CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS; // <-- add
or the narrower CERT_CHAIN_POLICY_IGNORE_END_REV_UNKNOWN_FLAG | CERT_CHAIN_POLICY_IGNORE_CA_REV_UNKNOWN_FLAG. A positive CERT_TRUST_IS_REVOKED must of course still be rejected.
Alternatively, skip / soften the policy call when the only remaining problem in dwErrorStatus is offline/unknown revocation, since that case is already handled by the ignore-mask above.
Workarounds
- Provide revocation data: give the machine access to Microsoft's CRL/OCSP (then the cache-only chain build finds the cached response and the policy returns 0). Measured on the affected machine: after one non-cache-only chain build through a working proxy, a fresh process reproduces the runner's verdict as
trustStatus=0x00000000 policyErr=0x00000000 ==> ACCEPTED, and the Settings UI works again in 0.101.2362. - Downgrade to the last release before #49527 (v0.100.2): its
PowerToys.Interop.dllcontains none of the authentication code, and the runner immediately dispatches Settings-UI messages again (dispatch_received_json: {"get_all_hotkey_conflicts":{}}right after the window opens). - Not sufficient: importing the Microsoft CRLs into
LocalMachine\CA/CurrentUser\CA. The chain engine withCERT_CHAIN_CACHE_ONLY_URL_RETRIEVALdoes not use store CRLs here (measured: trust status and policy error unchanged). - Editing
%LOCALAPPDATA%\Microsoft\PowerToys\settings.jsonand the per-modulesettings.jsonfiles by hand and restarting PowerToys also works (the runner reads them at startup, no pipe involved).
Verification script
The script below reproduces exactly what the runner computes, with no debugger needed: it resolves the signer certificate from the file's Authenticode PKCS#7 (CryptQueryObject → CMSG_SIGNER_INFO_PARAM → CertGetSubjectCertificateFromStore), builds the chain with the runner's engine/flag combination (HCCE_LOCAL_MACHINE, CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL | CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT), prints TrustStatus.dwErrorStatus, then calls CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_AUTHENTICODE, …) and prints dwError, and finally prints the resulting verdict.
On the affected machine:
machine-engine : trustStatus=0x01000040 policyErr=0x800B010E ==> REJECTED (reason=not-microsoft-signed)
user-engine : trustStatus=0x01000040 policyErr=0x800B010E ==> REJECTED (reason=not-microsoft-signed)
On a machine with a populated CryptoAPI cache (or with revocation access) the same run prints policyErr=0x00000000 ==> ACCEPTED. Adjust $f to the installed WinUI3Apps\PowerToys.Settings.exe path before running.
$ErrorActionPreference='Stop'
$src = @'
using System;
using System.Runtime.InteropServices;
using System.Text;
public static class PtVerdict
{
[StructLayout(LayoutKind.Sequential)] public struct CRYPT_DATA_BLOB { public uint cbData; public IntPtr pbData; }
[StructLayout(LayoutKind.Sequential)] public struct CRYPT_ALGORITHM_IDENTIFIER { public IntPtr pszObjId; public CRYPT_DATA_BLOB Parameters; }
[StructLayout(LayoutKind.Sequential)] public struct CRYPT_ATTRIBUTES { public uint cAttr; public IntPtr rgAttr; }
[StructLayout(LayoutKind.Sequential)] public struct FILETIME_T { public uint dwLowDateTime; public uint dwHighDateTime; }
[StructLayout(LayoutKind.Sequential)]
public struct CERT_INFO
{
public uint dwVersion; public CRYPT_DATA_BLOB SerialNumber; public CRYPT_ALGORITHM_IDENTIFIER SignatureAlgorithm;
public CRYPT_DATA_BLOB Issuer; public FILETIME_T NotBefore; public FILETIME_T NotAfter;
public CRYPT_ALGORITHM_IDENTIFIER SubjectAlgorithm; public CRYPT_DATA_BLOB Subject;
public uint cExtension; public IntPtr rgExtension;
}
[StructLayout(LayoutKind.Sequential)]
public struct CMSG_SIGNER_INFO
{
public uint dwVersion; public CRYPT_DATA_BLOB Issuer; public CRYPT_DATA_BLOB SerialNumber;
public CRYPT_ALGORITHM_IDENTIFIER HashAlgorithm; public CRYPT_ALGORITHM_IDENTIFIER HashEncryptionAlgorithm;
public CRYPT_DATA_BLOB EncryptedHash; public CRYPT_ATTRIBUTES AuthAttrs; public CRYPT_ATTRIBUTES UnauthAttrs;
}
[StructLayout(LayoutKind.Sequential)] public struct CERT_ENHANCED_KEY_USAGE { public uint cUsageIdentifier; public IntPtr rgpszUsageIdentifier; }
[StructLayout(LayoutKind.Sequential)] public struct CERT_USAGE_MATCH { public uint dwType; public CERT_ENHANCED_KEY_USAGE Usage; }
[StructLayout(LayoutKind.Sequential)]
public struct CERT_CHAIN_PARA
{
public uint cbSize; public CERT_USAGE_MATCH RequestedUsage; public CERT_USAGE_MATCH RequestedIssuancePolicy;
public uint dwUrlRetrievalTimeout; public int fCheckRevocationFreshnessTime; public uint dwRevocationFreshnessTime;
public IntPtr pftCacheResync; public IntPtr pStrongSignPara; public uint dwStrongSignFlags;
}
[StructLayout(LayoutKind.Sequential)] public struct CERT_TRUST_STATUS { public uint dwErrorStatus; public uint dwInfoStatus; }
[StructLayout(LayoutKind.Sequential)]
public struct CERT_CHAIN_CONTEXT
{
public uint cbSize; public CERT_TRUST_STATUS TrustStatus; public uint cChain; public IntPtr rgpChain;
public uint cLowerQualityChainContext; public IntPtr rgpLowerQualityChainContext;
public int fHasRevocationFreshnessTime; public uint dwRevocationFreshnessTime;
}
[StructLayout(LayoutKind.Sequential)] public struct CERT_CHAIN_POLICY_PARA { public uint cbSize; public uint dwFlags; public IntPtr pvExtraPolicyPara; }
[StructLayout(LayoutKind.Sequential)] public struct CERT_CHAIN_POLICY_STATUS { public uint cbSize; public uint dwError; public int lChainIndex; public int lElementIndex; public IntPtr pvExtraPolicyStatus; }
[DllImport("crypt32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern bool CryptQueryObject(uint objType, string obj, uint contentFlags, uint formatFlags, uint flags,
out uint enc, out uint ct, out uint ft, out IntPtr hStore, out IntPtr hMsg, out IntPtr pvContext);
[DllImport("crypt32.dll", SetLastError=true)] public static extern bool CryptMsgGetParam(IntPtr h, uint t, uint i, IntPtr pv, ref uint pcb);
[DllImport("crypt32.dll", SetLastError=true)] public static extern bool CryptMsgClose(IntPtr h);
[DllImport("crypt32.dll", SetLastError=true)] public static extern IntPtr CertGetSubjectCertificateFromStore(IntPtr hStore, uint enc, ref CERT_INFO id);
[DllImport("crypt32.dll", SetLastError=true)] public static extern bool CertGetCertificateChain(IntPtr eng, IntPtr pCert, IntPtr pTime, IntPtr hAdd, ref CERT_CHAIN_PARA para, uint flags, IntPtr rsv, out IntPtr ppChain);
[DllImport("crypt32.dll", SetLastError=true)] public static extern void CertFreeCertificateChain(IntPtr p);
[DllImport("crypt32.dll", SetLastError=true)] public static extern bool CertCloseStore(IntPtr h, uint f);
[DllImport("crypt32.dll", SetLastError=true)] public static extern bool CertFreeCertificateContext(IntPtr p);
[DllImport("crypt32.dll", SetLastError=true)] public static extern bool CertVerifyCertificateChainPolicy(IntPtr pol, IntPtr chain, ref CERT_CHAIN_POLICY_PARA para, ref CERT_CHAIN_POLICY_STATUS status);
public static string Judge(string path, IntPtr engine)
{
uint enc, ct, ft; IntPtr store, msg, ctx;
if (!CryptQueryObject(1, path, 0x400, 2, 0, out enc, out ct, out ft, out store, out msg, out ctx)) return "query failed";
uint size = 0; CryptMsgGetParam(msg, 6, 0, IntPtr.Zero, ref size);
IntPtr buf = Marshal.AllocHGlobal((int)size); CryptMsgGetParam(msg, 6, 0, buf, ref size);
CMSG_SIGNER_INFO si = (CMSG_SIGNER_INFO)Marshal.PtrToStructure(buf, typeof(CMSG_SIGNER_INFO));
CERT_INFO ci = new CERT_INFO(); ci.Issuer = si.Issuer; ci.SerialNumber = si.SerialNumber;
IntPtr leaf = CertGetSubjectCertificateFromStore(store, 0x00010001, ref ci);
Marshal.FreeHGlobal(buf);
if (leaf == IntPtr.Zero) return "leaf not found";
CERT_CHAIN_PARA para = new CERT_CHAIN_PARA();
para.cbSize = (uint)Marshal.SizeOf(typeof(CERT_CHAIN_PARA));
IntPtr oid = Marshal.StringToHGlobalAnsi("1.3.6.1.5.5.7.3.3");
IntPtr arr = Marshal.AllocHGlobal(IntPtr.Size); Marshal.WriteIntPtr(arr, oid);
para.RequestedUsage.dwType = 0; para.RequestedUsage.Usage.cUsageIdentifier = 1; para.RequestedUsage.Usage.rgpszUsageIdentifier = arr;
IntPtr chain;
bool ok = CertGetCertificateChain(engine, leaf, IntPtr.Zero, store, ref para, 0x00000004u | 0x40000000u, IntPtr.Zero, out chain);
string res;
if (!ok) res = "chain build FAILED";
else
{
CERT_CHAIN_CONTEXT cc = (CERT_CHAIN_CONTEXT)Marshal.PtrToStructure(chain, typeof(CERT_CHAIN_CONTEXT));
uint err = cc.TrustStatus.dwErrorStatus;
bool statusPass = (err & ~(0x40u | 0x01000000u)) == 0;
CERT_CHAIN_POLICY_PARA pp = new CERT_CHAIN_POLICY_PARA(); pp.cbSize = (uint)Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA));
CERT_CHAIN_POLICY_STATUS ps = new CERT_CHAIN_POLICY_STATUS(); ps.cbSize = (uint)Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS));
bool pc = CertVerifyCertificateChainPolicy((IntPtr)2, chain, ref pp, ref ps);
bool sigOk = statusPass && pc && ps.dwError == 0;
res = "trustStatus=0x" + err.ToString("X8") + " policyErr=0x" + ps.dwError.ToString("X8") + " ==> " + (sigOk ? "ACCEPTED" : "REJECTED (reason=not-microsoft-signed)");
CertFreeCertificateChain(chain);
}
Marshal.FreeHGlobal(oid); Marshal.FreeHGlobal(arr);
CertFreeCertificateContext(leaf); CryptMsgClose(msg); CertCloseStore(store, 0);
return res;
}
}
'@
Add-Type -TypeDefinition $src -Language CSharp
$f='C:\Programs\PowerToys\WinUI3Apps\PowerToys.Settings.exe'
"machine-engine : " + [PtVerdict]::Judge($f, [IntPtr]1)
"user-engine : " + [PtVerdict]::Judge($f, [IntPtr]0)
Source: microsoft/PowerToys