@@ -28,7 +28,7 @@ internal interface IManagedLocalGatewayPortPlatform
2828{
2929 WindowsTcpListenerSnapshotResult CaptureListeners ( ) ;
3030 string ? GetProcessCommandLine ( int processId ) ;
31- bool IsTrustedWslRelayBinary ( string processPath ) ;
31+ WslRelayTrustResult InspectWslRelayBinary ( string processPath ) ;
3232 bool IsExpectedWslGatewayListening ( string distroName , int port ) ;
3333 string ? ReadScheduledTaskXml ( string taskName ) ;
3434 string ? ReadFile ( string path ) ;
@@ -40,6 +40,13 @@ Task<bool> StopProcessAsync(
4040 CancellationToken cancellationToken ) ;
4141}
4242
43+ internal readonly record struct WslRelayTrustResult ( bool IsTrusted , string ? Detail )
44+ {
45+ public static WslRelayTrustResult Trusted ( ) => new ( true , null ) ;
46+
47+ public static WslRelayTrustResult Rejected ( string detail ) => new ( false , detail ) ;
48+ }
49+
4350internal sealed class WindowsManagedLocalGatewayPortPlatform : IManagedLocalGatewayPortPlatform
4451{
4552 public WindowsTcpListenerSnapshotResult CaptureListeners ( ) =>
@@ -48,7 +55,12 @@ public WindowsTcpListenerSnapshotResult CaptureListeners() =>
4855 public string ? GetProcessCommandLine ( int processId ) =>
4956 WindowsTcpListenerSnapshot . GetProcessCommandLine ( processId ) ;
5057
51- public bool IsTrustedWslRelayBinary ( string processPath )
58+ public WslRelayTrustResult InspectWslRelayBinary ( string processPath ) =>
59+ EvaluateWslRelayBinary ( processPath , WindowsAuthenticodeVerifier . VerifyMicrosoftSignedFile ) ;
60+
61+ internal static WslRelayTrustResult EvaluateWslRelayBinary (
62+ string processPath ,
63+ Func < string , AuthenticodeTrustResult > verifySignature )
5264 {
5365 try
5466 {
@@ -64,50 +76,24 @@ public bool IsTrustedWslRelayBinary(string processPath)
6476 ( fullPath . StartsWith ( windowsAppsRoot , StringComparison . OrdinalIgnoreCase ) &&
6577 string . Equals ( Path . GetFileName ( fullPath ) , "wslrelay.exe" , StringComparison . OrdinalIgnoreCase ) ) ;
6678 if ( ! isCanonical )
67- return false ;
68-
69- for ( var attempt = 0 ; attempt < 2 ; attempt ++ )
7079 {
71- var psi = CreateWslRelaySignatureProbe ( fullPath ) ;
72- using var process = Process . Start ( psi ) ;
73- if ( process is null )
74- return false ;
75- if ( process . WaitForExit ( 5_000 ) )
76- return process . ExitCode == 0 ;
77-
78- try { process . Kill ( entireProcessTree : true ) ; } catch { }
80+ return WslRelayTrustResult . Rejected (
81+ "WSL relay executable path is not canonical." ) ;
7982 }
80- return false ;
83+
84+ var signature = verifySignature ( fullPath ) ;
85+ return signature . IsTrusted
86+ ? WslRelayTrustResult . Trusted ( )
87+ : WslRelayTrustResult . Rejected (
88+ signature . Detail ?? "WSL relay Authenticode verification failed." ) ;
8189 }
8290 catch
8391 {
84- return false ;
92+ return WslRelayTrustResult . Rejected (
93+ "WSL relay Authenticode verification could not complete." ) ;
8594 }
8695 }
8796
88- internal static ProcessStartInfo CreateWslRelaySignatureProbe ( string fullPath )
89- {
90- var startInfo = new ProcessStartInfo
91- {
92- FileName = "powershell.exe" ,
93- UseShellExecute = false ,
94- CreateNoWindow = true ,
95- } ;
96-
97- // A pwsh parent can prepend PowerShell 7 modules that Windows PowerShell
98- // 5.1 cannot load. Let the child rebuild its native module path so the
99- // built-in Authenticode cmdlet remains available.
100- startInfo . Environment . Remove ( "PSModulePath" ) ;
101- startInfo . Environment [ "OPENCLAW_VERIFY_PATH" ] = fullPath ;
102- startInfo . ArgumentList . Add ( "-NoProfile" ) ;
103- startInfo . ArgumentList . Add ( "-NonInteractive" ) ;
104- startInfo . ArgumentList . Add ( "-Command" ) ;
105- startInfo . ArgumentList . Add (
106- "$s=Get-AuthenticodeSignature -LiteralPath $env:OPENCLAW_VERIFY_PATH; " +
107- "if($s.Status -eq 'Valid' -and $s.SignerCertificate.Subject -match 'Microsoft'){exit 0}; exit 1" ) ;
108- return startInfo ;
109- }
110-
11197 public bool IsExpectedWslGatewayListening ( string distroName , int port )
11298 {
11399 try
@@ -386,10 +372,10 @@ private GatewayEndpointProvenance InspectCore(GatewayRecord record)
386372 . Distinct ( StringComparer . OrdinalIgnoreCase )
387373 . ToDictionary (
388374 path => path ,
389- path => _platform . IsTrustedWslRelayBinary ( path ) ,
375+ path => _platform . InspectWslRelayBinary ( path ) ,
390376 StringComparer . OrdinalIgnoreCase ) ;
391377 var expectedDistroListening =
392- relayTrustByPath . Values . Any ( trusted => trusted ) &&
378+ relayTrustByPath . Values . Any ( result => result . IsTrusted ) &&
393379 _platform . IsExpectedWslGatewayListening ( managedDistroName , uri . Port ) ;
394380 var classified = listeners
395381 . Select ( listener => ClassifyListener (
@@ -426,7 +412,8 @@ private GatewayEndpointProvenance InspectCore(GatewayRecord record)
426412 . Where ( item => item . Kind != GatewayEndpointProvenanceKind . ExpectedManagedGateway )
427413 . Select ( item =>
428414 $ "{ item . ProcessName ?? "unknown" } (PID { item . ProcessId ? . ToString ( ) ?? "?" } ): " +
429- ( item . Detail ?? "listener verification failed" ) ) ) ;
415+ ( item . Detail ?? "listener verification failed" ) )
416+ . Distinct ( StringComparer . Ordinal ) ) ;
430417 return new GatewayEndpointProvenance (
431418 GatewayEndpointProvenanceKind . UnknownListener ,
432419 uri . Port ,
@@ -533,16 +520,17 @@ private GatewayEndpointProvenance ClassifyListener(
533520 string managedDistroName ,
534521 int port ,
535522 WindowsTcpListenerInfo listener ,
536- IReadOnlyDictionary < string , bool > relayTrustByPath ,
523+ IReadOnlyDictionary < string , WslRelayTrustResult > relayTrustByPath ,
537524 bool expectedDistroListening )
538525 {
539526 var isWslRelay =
540527 string . Equals ( listener . ProcessName , "wslrelay" , StringComparison . OrdinalIgnoreCase ) ;
541528 var relayPath = listener . ProcessPath ;
542- var trustedRelay =
529+ var relayTrust = default ( WslRelayTrustResult ) ;
530+ var hasRelayTrust =
543531 relayPath is not null &&
544- relayTrustByPath . TryGetValue ( relayPath , out var trusted ) &&
545- trusted ;
532+ relayTrustByPath . TryGetValue ( relayPath , out relayTrust ) ;
533+ var trustedRelay = hasRelayTrust && relayTrust . IsTrusted ;
546534 if ( isWslRelay && trustedRelay && expectedDistroListening )
547535 {
548536 return new GatewayEndpointProvenance (
@@ -572,7 +560,8 @@ relayPath is not null &&
572560 ? relayPath is null
573561 ? "WSL relay executable path could not be read."
574562 : ! trustedRelay
575- ? "WSL relay is not the canonical Microsoft-signed binary."
563+ ? relayTrust . Detail ??
564+ "WSL relay Authenticode verification failed."
576565 : $ "Expected distro '{ managedDistroName } ' does not report its systemd gateway MainPID owning port { port } ."
577566 : "Process is not a verified managed WSL relay or proven obsolete OpenClaw gateway." ;
578567 return new GatewayEndpointProvenance (
0 commit comments