[CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$HostDirectory, [Parameter(Mandatory = $true)] [ValidatePattern('^[A-Fa-f0-9]{40}$')] [string]$CertificateThumbprint, [ValidateSet('CurrentUser', 'LocalMachine')] [string]$CertificateStoreLocation = 'CurrentUser', [Parameter(Mandatory = $true)][string]$TimestampUrl, [string]$SignToolPath = '' ) Set-StrictMode -Version 2.0 $ErrorActionPreference = 'Stop' function Test-RegularFile([string]$Path, [long]$MaximumBytes) { if (-not [IO.File]::Exists($Path)) { return $false } $item = Get-Item -LiteralPath $Path -Force return $item.Length -gt 0 -and $item.Length -le $MaximumBytes -and (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -eq 0) } function Find-SignTool([string]$ExplicitPath) { if (-not [string]::IsNullOrWhiteSpace($ExplicitPath)) { $resolved = [IO.Path]::GetFullPath($ExplicitPath) if (Test-RegularFile $resolved 128MB) { return $resolved } throw 'signtool_invalid' } $command = Get-Command signtool.exe -ErrorAction SilentlyContinue if ($null -ne $command -and (Test-RegularFile $command.Source 128MB)) { return $command.Source } $programFilesX86 = [Environment]::GetFolderPath( [Environment+SpecialFolder]::ProgramFilesX86) foreach ($kitsVersion in @('10', '8.1')) { $binRoot = Join-Path $programFilesX86 ("Windows Kits\{0}\bin" -f $kitsVersion) if (-not [IO.Directory]::Exists($binRoot)) { continue } $candidates = @(Get-ChildItem -LiteralPath $binRoot -Directory -Force | Sort-Object Name -Descending | ForEach-Object { Join-Path $_.FullName 'x86\signtool.exe' Join-Path $_.FullName 'x64\signtool.exe' }) $candidates += @(Join-Path $binRoot 'x86\signtool.exe') foreach ($candidate in $candidates) { if (Test-RegularFile $candidate 128MB) { return $candidate } } } throw 'signtool_not_found' } if ($env:OS -ne 'Windows_NT') { throw 'windows_required' } $timestampUri = $null if (-not [Uri]::TryCreate($TimestampUrl, [UriKind]::Absolute, [ref]$timestampUri) -or $timestampUri.Scheme -ne 'https' -or $timestampUri.UserInfo -or $timestampUri.Fragment) { throw 'https_timestamp_url_required' } $hostRoot = [IO.Path]::GetFullPath($HostDirectory).TrimEnd( [char[]]@('\', '/')) + [IO.Path]::DirectorySeparatorChar if (-not [IO.Directory]::Exists($hostRoot)) { throw 'host_directory_missing' } $rootItem = Get-Item -LiteralPath $hostRoot -Force if (($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'host_directory_reparse_forbidden' } $criticalRelativePaths = @( 'Lskj.AgentPet.Host.exe', 'Lskj.AgentPet.Host.dll', 'Lskj.AgentPet.Host.Core.dll', 'lserp-agent-cli.exe' ) $criticalPaths = @() foreach ($relative in $criticalRelativePaths) { $full = [IO.Path]::GetFullPath((Join-Path $hostRoot $relative)) if (-not $full.StartsWith($hostRoot, [StringComparison]::OrdinalIgnoreCase) -or -not (Test-RegularFile $full 512MB)) { throw 'host_critical_binary_missing' } $criticalPaths += $full } $normalizedThumbprint = $CertificateThumbprint.ToUpperInvariant() $certificatePath = "Cert:\{0}\My\{1}" -f ` $CertificateStoreLocation, $normalizedThumbprint if (-not (Test-Path -LiteralPath $certificatePath)) { throw 'host_signing_certificate_missing' } $certificate = Get-Item -LiteralPath $certificatePath $codeSigningOid = '1.3.6.1.5.5.7.3.3' $hasCodeSigningEku = @($certificate.EnhancedKeyUsageList | Where-Object { $_.ObjectId.Value -eq $codeSigningOid }).Count -gt 0 $now = [DateTime]::UtcNow if (-not $certificate.HasPrivateKey -or -not $hasCodeSigningEku -or $certificate.NotBefore.ToUniversalTime() -gt $now -or $certificate.NotAfter.ToUniversalTime() -le $now) { throw 'host_signing_certificate_invalid' } $signTool = Find-SignTool $SignToolPath $signed = @() foreach ($criticalPath in $criticalPaths) { $arguments = @( 'sign', '/nologo', '/sha1', $certificate.Thumbprint, '/s', 'My', '/fd', 'SHA256', '/tr', $timestampUri.AbsoluteUri, '/td', 'SHA256', $criticalPath ) if ($CertificateStoreLocation -eq 'LocalMachine') { $arguments = @( 'sign', '/nologo', '/sm', '/sha1', $certificate.Thumbprint, '/s', 'My', '/fd', 'SHA256', '/tr', $timestampUri.AbsoluteUri, '/td', 'SHA256', $criticalPath ) } & $signTool @arguments | Out-Null if ($LASTEXITCODE -ne 0) { throw 'host_authenticode_signing_failed' } $signature = Get-AuthenticodeSignature -LiteralPath $criticalPath if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or $null -eq $signature.SignerCertificate -or $signature.SignerCertificate.Thumbprint.ToUpperInvariant() -ne $normalizedThumbprint -or $null -eq $signature.TimeStamperCertificate) { throw 'host_authenticode_verification_failed' } $signed += [ordered]@{ file = [IO.Path]::GetFileName($criticalPath) sha256 = (Get-FileHash -LiteralPath $criticalPath -Algorithm SHA256).Hash.ToLowerInvariant() } } [ordered]@{ schemaVersion = '1.0' certificateThumbprint = $normalizedThumbprint timestampUrl = $timestampUri.AbsoluteUri signed = $signed } | ConvertTo-Json -Depth 4 -Compress