138 lines
5.3 KiB
PowerShell
138 lines
5.3 KiB
PowerShell
[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
|