Added RequireAuthenticode.
This commit is contained in:
@@ -762,6 +762,9 @@ function Test-DownloadedFile {
|
||||
}
|
||||
Write-Log "$Name SHA256 verified: $actualHash" 'OK'
|
||||
}
|
||||
if ($RequireAuthenticode -and $file.Extension -notin @('.exe', '.dll')) {
|
||||
throw "$Name requires an Authenticode signature, but $($file.Extension) files cannot be validated."
|
||||
}
|
||||
if ($file.Extension -in @('.exe', '.dll')) {
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $Path
|
||||
if ($signature.Status -ne 'Valid') {
|
||||
@@ -815,6 +818,9 @@ function Install-PortableArchive {
|
||||
|
||||
$expected = Get-ChildItem -LiteralPath $source -Filter $ExpectedExecutable -File -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $expected) { throw "$Name validation failed: $ExpectedExecutable was not found." }
|
||||
if ($RequireAuthenticode) {
|
||||
Test-DownloadedFile -Path $expected.FullName -Name $Name -MinimumBytes 1 -RequireAuthenticode
|
||||
}
|
||||
|
||||
# Replace only after download and validation succeed. Preserve prior version as a rollback copy.
|
||||
$backup = "$Destination.previous"
|
||||
@@ -866,10 +872,15 @@ function Install-PortableExecutable {
|
||||
function Test-PortableInstallation {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Destination,
|
||||
[Parameter(Mandatory)][string]$ExpectedExecutable
|
||||
[Parameter(Mandatory)][string]$ExpectedExecutable,
|
||||
[switch]$RequireAuthenticode
|
||||
)
|
||||
return [bool](Get-ChildItem -LiteralPath $Destination -Filter $ExpectedExecutable -File -Recurse -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1)
|
||||
$expected = Get-ChildItem -LiteralPath $Destination -Filter $ExpectedExecutable -File -Recurse -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1
|
||||
if (-not $expected) { return $false }
|
||||
if (-not $RequireAuthenticode) { return $true }
|
||||
if ($expected.Extension -notin @('.exe', '.dll')) { return $false }
|
||||
return (Get-AuthenticodeSignature -LiteralPath $expected.FullName).Status -eq 'Valid'
|
||||
}
|
||||
|
||||
function New-PortableAppShortcut {
|
||||
@@ -1298,7 +1309,7 @@ if (-not $SkipDownloads) {
|
||||
$downloadWithCurl = [bool]($configuredApp.PSObject.Properties['DownloadWithCurl'] -and $configuredApp.DownloadWithCurl)
|
||||
$expectedSHA256 = if ($configuredApp.PSObject.Properties['ExpectedSHA256']) { [string]$configuredApp.ExpectedSHA256 } else { '' }
|
||||
$requireAuthenticode = [bool]($configuredApp.PSObject.Properties['RequireAuthenticode'] -and $configuredApp.RequireAuthenticode)
|
||||
if (-not $ForcePortableUpdates -and (Test-PortableInstallation -Destination $destination -ExpectedExecutable $configuredApp.Executable)) {
|
||||
if (-not $ForcePortableUpdates -and (Test-PortableInstallation -Destination $destination -ExpectedExecutable $configuredApp.Executable -RequireAuthenticode:$requireAuthenticode)) {
|
||||
$PortableDefinitions.Add([pscustomobject]@{
|
||||
Name=$configuredApp.Name; Type=if ($isDirectExe) { 'Exe' } else { 'Zip' }; Uri=''; Folder=$configuredApp.Folder;
|
||||
Executable=$configuredApp.Executable; Archive=''; Flatten=$flattenSingleDirectory; Shortcut=$createShortcut;
|
||||
@@ -1339,7 +1350,7 @@ if (-not $SkipDownloads) {
|
||||
|
||||
foreach ($app in $PortableDefinitions) {
|
||||
$destination = Join-Path $PortableRoot $app.Folder
|
||||
if (-not $ForcePortableUpdates -and (Test-PortableInstallation -Destination $destination -ExpectedExecutable $app.Executable)) {
|
||||
if (-not $ForcePortableUpdates -and (Test-PortableInstallation -Destination $destination -ExpectedExecutable $app.Executable -RequireAuthenticode:$app.RequireAuthenticode)) {
|
||||
Add-Result 'PortableInstall' $app.Name 'Skipped' "Validated installation already exists: $destination"
|
||||
Write-Log "$($app.Name) is already installed and validated; skipped. Use -ForcePortableUpdates to download it again." 'INFO'
|
||||
}
|
||||
@@ -1354,7 +1365,8 @@ if (-not $SkipDownloads) {
|
||||
$downloadWithCurl = $app.PSObject.Properties['DownloadWithCurl'] -and $app.DownloadWithCurl
|
||||
Install-PortableArchive -Name $app.Name -Uri $app.Uri -Destination $destination `
|
||||
-ExpectedExecutable $app.Executable -ArchiveName $app.Archive -FlattenSingleDirectory:$app.Flatten `
|
||||
-Headers $requestHeaders -DownloadWithCurl:$downloadWithCurl -ExpectedSHA256 $app.ExpectedSHA256
|
||||
-Headers $requestHeaders -DownloadWithCurl:$downloadWithCurl -ExpectedSHA256 $app.ExpectedSHA256 `
|
||||
-RequireAuthenticode:$app.RequireAuthenticode
|
||||
}
|
||||
} | Out-Null
|
||||
}
|
||||
|
||||
@@ -38,3 +38,89 @@ Describe 'Execution modes' {
|
||||
$content | Should -Not -Match '\$ProfileInstallerRoot\s*=\s*["'']P:\\Install'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Portable archive signature validation' {
|
||||
BeforeAll {
|
||||
$script:repositoryRoot = Split-Path $PSScriptRoot -Parent
|
||||
$tokens = $null
|
||||
$parseErrors = $null
|
||||
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
|
||||
(Join-Path $script:repositoryRoot 'Automotive-Workstation-Setup.ps1'),
|
||||
[ref]$tokens, [ref]$parseErrors)
|
||||
if ($parseErrors.Count) { throw 'Entry point could not be parsed.' }
|
||||
foreach ($name in @('Test-DownloadedFile', 'Install-PortableArchive', 'Test-PortableInstallation')) {
|
||||
$definition = $ast.Find({ param($node)
|
||||
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name
|
||||
}, $true)
|
||||
Invoke-Expression $definition.Extent.Text
|
||||
}
|
||||
function Test-DownloadUri { param($Uri) }
|
||||
function Write-Log { param($Message, $Level) }
|
||||
function Invoke-ReliableDownload {
|
||||
param($Name, $Uri, $Destination)
|
||||
Copy-Item -LiteralPath $script:archivePath -Destination $Destination
|
||||
}
|
||||
}
|
||||
|
||||
It 'passes the configured signature requirement to archive installation' {
|
||||
$tokens = $null
|
||||
$parseErrors = $null
|
||||
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
|
||||
(Join-Path $script:repositoryRoot 'Automotive-Workstation-Setup.ps1'),
|
||||
[ref]$tokens, [ref]$parseErrors)
|
||||
$archiveCalls = @($ast.FindAll({ param($node)
|
||||
$node -is [System.Management.Automation.Language.CommandAst] -and
|
||||
$node.GetCommandName() -eq 'Install-PortableArchive'
|
||||
}, $true))
|
||||
$archiveCalls.Count | Should -BeGreaterThan 0
|
||||
foreach ($call in $archiveCalls) {
|
||||
$call.Extent.Text | Should -Match '-RequireAuthenticode:\$app\.RequireAuthenticode'
|
||||
}
|
||||
$cachedCalls = @($ast.FindAll({ param($node)
|
||||
$node -is [System.Management.Automation.Language.CommandAst] -and
|
||||
$node.GetCommandName() -eq 'Test-PortableInstallation'
|
||||
}, $true))
|
||||
$cachedCalls.Count | Should -Be 2
|
||||
foreach ($call in $cachedCalls) {
|
||||
$call.Extent.Text | Should -Match '-RequireAuthenticode:'
|
||||
}
|
||||
}
|
||||
|
||||
It 'does not skip an unsigned cached executable when a signature is required' {
|
||||
$root = Join-Path ([IO.Path]::GetTempPath()) ('automotive-cached-' + [guid]::NewGuid().ToString('N'))
|
||||
try {
|
||||
$null = New-Item -ItemType Directory -Path $root -Force
|
||||
[IO.File]::WriteAllBytes((Join-Path $root 'demo.exe'), (New-Object byte[] 100))
|
||||
Test-PortableInstallation -Destination $root -ExpectedExecutable 'demo.exe' | Should -BeTrue
|
||||
Test-PortableInstallation -Destination $root -ExpectedExecutable 'demo.exe' -RequireAuthenticode | Should -BeFalse
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
It 'preserves an installed version when the extracted executable is unsigned' {
|
||||
$root = Join-Path ([IO.Path]::GetTempPath()) ('automotive-signature-' + [guid]::NewGuid().ToString('N'))
|
||||
try {
|
||||
$script:Paths = @{ Downloads = Join-Path $root 'Downloads'; Temp = Join-Path $root 'Temp' }
|
||||
$script:archivePath = Join-Path $root 'source.zip'
|
||||
$destination = Join-Path $root 'Installed'
|
||||
$source = Join-Path $root 'Source'
|
||||
$null = New-Item -ItemType Directory -Path $script:Paths.Downloads, $script:Paths.Temp, $destination, $source -Force
|
||||
[IO.File]::WriteAllText((Join-Path $destination 'existing.txt'), 'keep this version')
|
||||
$bytes = New-Object byte[] 100000
|
||||
[Random]::new().NextBytes($bytes)
|
||||
[IO.File]::WriteAllBytes((Join-Path $source 'demo.exe'), $bytes)
|
||||
Compress-Archive -LiteralPath (Join-Path $source 'demo.exe') -DestinationPath $script:archivePath
|
||||
|
||||
{ Install-PortableArchive -Name 'Demo' -Uri 'https://example.invalid/demo.zip' `
|
||||
-Destination $destination -ExpectedExecutable 'demo.exe' -RequireAuthenticode } |
|
||||
Should -Throw '*Authenticode signature is not valid*'
|
||||
(Get-Content -LiteralPath (Join-Path $destination 'existing.txt') -Raw) | Should -Be 'keep this version'
|
||||
Test-Path -LiteralPath (Join-Path $destination 'demo.exe') | Should -BeFalse
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ The checkpoint file is `Config\\.workstation-setup-state.json`. Resume a prior a
|
||||
-WorkstationProfile 'DailyTech & Tuning' -ResumeFromCheckpoint
|
||||
```
|
||||
|
||||
For portable ZIP entries, `RequireAuthenticode: true` validates the extracted expected `.exe` or `.dll` before replacing an installed version. Entries whose expected file cannot carry an Authenticode signature fail validation when this option is enabled. This option is not currently enabled for any bundled portable entry; it must be explicitly set in the configuration.
|
||||
|
||||
## Workspace Data Handling
|
||||
|
||||
Always preserve original ECU, EEPROM, and coding reads in the `Originals` or profile-equivalent folder. Calculate hashes before editing. Keep modified files in `WorkingCopies`, `CodingBackups`, or the matching profile folder. Do not flash or code a vehicle without authorization, verified backups, stable power, and a vendor-supported procedure.
|
||||
|
||||
Reference in New Issue
Block a user