openclaw/scripts/windows-testbox-openssh.ps1

99 lines
5.9 KiB
PowerShell

function Get-OpenSshAclDisposition {
param([Security.AccessControl.RawSecurityDescriptor]$Descriptor, [bool]$Directory)
$trustedOwners = @('S-1-5-18', 'S-1-5-32-544',
'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464')
if ($null -eq $Descriptor -or $null -eq $Descriptor.Owner) { return 'unknown' }
if ($Descriptor.Owner.Value -notin $trustedOwners) { return 'unsafe' }
if (-not ($Descriptor.ControlFlags -band [Security.AccessControl.ControlFlags]::DiscretionaryAclPresent) -or
$null -eq $Descriptor.DiscretionaryAcl) { return 'unsafe' }
$write = [long][Security.AccessControl.FileSystemRights]::Write -bor
[long][Security.AccessControl.FileSystemRights]::Delete -bor
[long][Security.AccessControl.FileSystemRights]::ChangePermissions -bor
[long][Security.AccessControl.FileSystemRights]::TakeOwnership
if ($Directory) { $write = $write -bor [long][Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles }
$known = 0xf0000000L -bor [long][Security.AccessControl.FileSystemRights]::FullControl
foreach ($ace in $Descriptor.DiscretionaryAcl) {
# Inherit-only rights apply to future children, not this object. Inspect raw ACEs:
# GetAccessRules omits unsupported ACEs and leaves signed generic masks unmapped.
if ($ace.AceFlags -band [Security.AccessControl.AceFlags]::InheritOnly) { continue }
if ($ace -isnot [Security.AccessControl.CommonAce] -or $ace.IsCallback -or
$ace.AceQualifier -notin @([Security.AccessControl.AceQualifier]::AccessAllowed,
[Security.AccessControl.AceQualifier]::AccessDenied) -or
([int]$ace.AceFlags -band 0xe0)) { return 'unknown' }
$mask = [long]$ace.AccessMask -band 0xffffffffL
if ($mask -band (-bnot $known)) { return 'unknown' }
# Denies cannot cancel an unsafe allow here; this screens the installation,
# not a particular token's effective access. GENERIC_READ/EXECUTE are read-only.
if ($ace.AceQualifier -eq [Security.AccessControl.AceQualifier]::AccessAllowed -and
$ace.SecurityIdentifier.Value -notin $trustedOwners -and
($mask -band (0x50000000L -bor $write))) { return 'unsafe' }
}
return 'safe'
}
function Get-WindowsTestboxOpenSshInstallation {
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
try {
$services = @(Get-CimInstance Win32_Service -Filter "Name='sshd'")
if ($services.Count -ne 1) { throw 'service' }
$service = $services[0]
if ($service.State -ne 'Running' -or $service.StartName -ne 'LocalSystem' -or
$service.ProcessId -le 0) { throw 'service' }
$directories = @("$env:WINDIR\System32\OpenSSH", "$env:ProgramFiles\OpenSSH",
"$env:ProgramFiles\OpenSSH-Win64")
$selected = @($directories | Where-Object {
$candidate = "$_\sshd.exe"
$service.PathName.Trim() -ieq "`"$candidate`"" -or
($candidate -notmatch '\s' -and $service.PathName.Trim() -ieq $candidate)
})
if ($selected.Count -ne 1 -or $selected[0] -notmatch '^[A-Za-z]:\\') { throw 'path' }
$directory = $selected[0]
$sshd = "$directory\sshd.exe"
$keygen = "$directory\ssh-keygen.exe"
$process = Get-CimInstance Win32_Process -Filter "ProcessId=$($service.ProcessId)"
if (-not $process -or $process.ExecutablePath -ine $sshd -or -not $process.CreationDate) { throw 'process' }
$owner = Invoke-CimMethod -InputObject $process -MethodName GetOwnerSid
if ($owner.ReturnValue -ne 0 -or $owner.Sid -ne 'S-1-5-18') { throw 'owner' }
# Check each component before following it. OS ancestors and provider DLLs
# remain image-owned; only the selected directory and executed pair get ACL checks.
$component = [IO.Path]::GetPathRoot($directory)
$paths = @($component)
foreach ($part in $directory.Substring($component.Length).Split('\')) {
$component = Join-Path $component $part
$paths += $component
}
foreach ($entry in @($paths) + @($sshd, $keygen)) {
$item = Get-Item -LiteralPath $entry -Force
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
$item.PSIsContainer -ne ($entry -in $paths)) { throw 'filesystem' }
}
foreach ($entry in @($directory, $sshd, $keygen)) {
$acl = Get-Acl -LiteralPath $entry
$descriptor = [Security.AccessControl.RawSecurityDescriptor]::new($acl.GetSecurityDescriptorBinaryForm(), 0)
if ((Get-OpenSshAclDisposition $descriptor ($entry -eq $directory)) -ne 'safe') { throw 'acl' }
}
foreach ($entry in @($sshd, $keygen)) {
$signature = Get-AuthenticodeSignature -LiteralPath $entry
if ($signature.Status -ne 'Valid' -or $null -eq $signature.SignerCertificate -or
$signature.SignerCertificate.GetNameInfo([Security.Cryptography.X509Certificates.X509NameType]::SimpleName, $false) -cnotin
@('Microsoft Corporation', 'Microsoft Windows')) { throw 'signature' }
}
# Admission belongs to the same running service/process observed before the
# filesystem checks; a restart or configuration change requires a fresh attempt.
$currentServices = @(Get-CimInstance Win32_Service -Filter "Name='sshd'")
if ($currentServices.Count -ne 1) { throw 'service changed' }
$current = $currentServices[0]
if ($current.ProcessId -ne $service.ProcessId -or $current.State -ne $service.State -or
$current.StartName -ne $service.StartName -or $current.PathName -cne $service.PathName) { throw 'service changed' }
$currentProcess = Get-CimInstance Win32_Process -Filter "ProcessId=$($current.ProcessId)"
if (-not $currentProcess -or $currentProcess.ExecutablePath -ine $sshd -or
-not $currentProcess.CreationDate -or $currentProcess.CreationDate -ne $process.CreationDate) { throw 'process changed' }
return [pscustomobject]@{ Sshd = $sshd; Keygen = $keygen }
} catch {
throw 'Cannot admit the native Windows OpenSSH installation; verify the provider service, Microsoft binaries, and installation permissions.'
}
}