Skip to content

Commit 5720bb6

Browse files
committed
Add Authenticode validation and improve hash validation
1 parent 8bf16a1 commit 5720bb6

1 file changed

Lines changed: 121 additions & 22 deletions

File tree

Utils.ps1

Lines changed: 121 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,20 @@ function Find-First {
197197
return @(Get-ChildItem -Filter "${Filter}" -Path "${Path}")[0]
198198
}
199199

200+
function Get-CertificateInfo {
201+
<#
202+
.SYNOPSIS
203+
Get basic info of a certificate as a string
204+
#>
205+
[OutputType([string])]
206+
param([Parameter(Mandatory=$true)][System.Security.Cryptography.X509Certificates.X509Certificate]$Certificate)
207+
$NotBefore = $Certificate.NotBefore.ToUniversalTime().ToString("yyyy-MM-dd")
208+
$NotAfter = $Certificate.NotAfter.ToUniversalTime().ToString("yyyy-MM-dd")
209+
$Algorithm = $Certificate.SignatureAlgorithm.FriendlyName
210+
return "$($Certificate.Subject) (${NotBefore} - ${NotAfter}, ${Algorithm}, " `
211+
+ "Currently valid: $($Certificate.Verify()), Thumbprint: $($Certificate.Thumbprint))"
212+
}
213+
200214
function Get-InstallBitness {
201215
[OutputType([string])]
202216
param(
@@ -309,8 +323,10 @@ function Install-Executable {
309323
[OutputType([int])]
310324
param(
311325
[string]$Name,
312-
[string]$Path
326+
[string]$Path,
327+
[switch]$BypassAuthenticode = $false
313328
)
329+
# Validate the file path
314330
try {
315331
$File = Get-Item "${Path}" -ErrorAction Stop
316332
} catch {
@@ -323,14 +339,23 @@ function Install-Executable {
323339
}
324340
return 1
325341
}
342+
# Validate Authenticode
343+
if (-not $BypassAuthenticode) {
344+
try {
345+
Test-AuthenticodeSignature -FilePath "${Path}"
346+
} catch {
347+
return 2
348+
}
349+
}
350+
# Install the executable
326351
Show-Output "Installing ${Name}"
327352
if ($File.Extension -eq ".msi") {
328353
$Process = Start-Process -NoNewWindow -Wait -PassThru "msiexec" -ArgumentList "/i","${Path}"
329354
} else {
330355
$Process = Start-Process -NoNewWindow -Wait -PassThru "${Path}"
331356
}
332357
$ExitCode = $Process.ExitCode
333-
if ($ExitCode -ne 0) {
358+
if ($ExitCode) {
334359
Show-Output -ForegroundColor Red "${Name} installation returned non-zero exit code ${ExitCode}. Perhaps the installation failed?"
335360
}
336361
return $ExitCode
@@ -346,34 +371,71 @@ function Install-FromUri {
346371
[Parameter(mandatory=$false)][string]$UnzipFolderName,
347372
[Parameter(mandatory=$false)][string]$UnzippedFilePath,
348373
[Parameter(mandatory=$false)][string]$MD5,
349-
[Parameter(mandatory=$false)][string]$SHA256
374+
[Parameter(mandatory=$false)][string]$SHA256,
375+
[switch]$BypassAuthenticode = $false
350376
)
351377
Show-Output "Downloading ${Name}"
352378
$Path = "${Downloads}\${Filename}"
353-
Invoke-WebRequestFast -Uri "${Uri}" -OutFile "${Path}"
379+
380+
# Test if the file already exists and matches the given hash
381+
$FileAlreadyOK = $false
382+
if (Test-Path $Path) {
383+
if ($PSBoundParameters.ContainsKey("SHA256")) {
384+
$FileHash = (Get-FileHash -Path "${Path}" -Algorithm "SHA256").Hash
385+
if ($FileHash -eq $SHA256) {
386+
$FileAlreadyOK = $true
387+
Show-Output "The file `"${Path}`" is already downloaded and has the correct SHA-256 hash ${SHA256}. Skipping download."
388+
} else {
389+
Show-Output "The file `"${Path}` was already downloaded, but had an incorrect SHA-256 hash. " `
390+
+ "(Expected ${SHA256}, got ${FileHash}.) The previous download may have been interrupted. Downloading again."
391+
}
392+
} elseif ($PSBoundParameters.ContainsKey("MD5")) {
393+
$FileHash = (Get-FileHash -Path "${Path}" -Algorithm "MD5").Hash
394+
if ($FileHash -eq $MD5) {
395+
$FileAlreadyOK = $true
396+
Show-Output "The file `"${Path}`" is already downloaded and has the correct MD5 hash ${MD5}. Skipping download."
397+
} else {
398+
Show-Output "The file `"${Path}` was already downloaded, but had an incorrect MD5 hash. " `
399+
+ "(Expected ${MD5}, got ${FileHash}.) The previous download may have been interrupted. Downloading again."
400+
}
401+
}
402+
}
403+
404+
# Download the file
405+
if (-not $FileAlreadyOK) {
406+
Invoke-WebRequestFast -Uri "${Uri}" -OutFile "${Path}"
407+
}
408+
409+
# Get the file object
354410
try {
355411
$File = Get-Item "${Path}" -ErrorAction Stop
356412
} catch {
357413
Show-Output "Downloaded file was not found at ${Path}"
358414
return 1
359415
}
360-
if ($PSBoundParameters.ContainsKey("SHA256")) {
361-
$FileHash = (Get-FileHash -Path "${Path}" -Algorithm "SHA256").Hash
362-
if ($FileHash -eq $SHA256) {
363-
Show-Output "SHA256 checksum OK"
364-
} else {
365-
Show-Output -ForegroundColor Red "Downloaded file has an invalid SHA256 checksum. Expected: ${SHA256}, got: ${FileHash}"
366-
return 1
367-
}
368-
} elseif ($PSBoundParameters.ContainsKey("MD5")) {
369-
$FileHash = (Get-FileHash -Path "${Path}" -Algorithm "MD5").Hash
370-
if ($FileHash -eq $MD5) {
371-
Show-Output "MD5 checksum OK"
372-
} else {
373-
Show-Output -ForegroundColor Red "Downloaded file has an invalid MD5 checksum. Expected: ${MD5}, got: ${FileHash}"
374-
return 1
416+
417+
# Verify the file checksum if not already verified
418+
if (-not $FileAlreadyOK) {
419+
if ($PSBoundParameters.ContainsKey("SHA256")) {
420+
$FileHash = (Get-FileHash -Path "${Path}" -Algorithm "SHA256").Hash
421+
if ($FileHash -eq $SHA256) {
422+
Show-Output "SHA256 checksum OK"
423+
} else {
424+
Show-Output -ForegroundColor Red "Downloaded file has an invalid SHA256 checksum. Expected: ${SHA256}, got: ${FileHash}"
425+
return 1
426+
}
427+
} elseif ($PSBoundParameters.ContainsKey("MD5")) {
428+
$FileHash = (Get-FileHash -Path "${Path}" -Algorithm "MD5").Hash
429+
if ($FileHash -eq $MD5) {
430+
Show-Output "MD5 checksum OK"
431+
} else {
432+
Show-Output -ForegroundColor Red "Downloaded file has an invalid MD5 checksum. Expected: ${MD5}, got: ${FileHash}"
433+
return 1
434+
}
375435
}
376436
}
437+
438+
# Process the downloaded file
377439
if ($File.Extension -eq ".zip") {
378440
if (-not $PSBoundParameters.ContainsKey("UnzipFolderName")) {
379441
Show-Output -ForegroundColor Red "UnzipFolderName was not provided for a zip file."
@@ -384,11 +446,12 @@ function Install-FromUri {
384446
return 1
385447
}
386448
Show-Output "Extracting ${Name}"
387-
Expand-Archive -Path "${Path}" -DestinationPath "${Downloads}\${UnzipFolderName}"
388-
Install-Executable -Name "${Name}" -Path "${Downloads}\${UnzipFolderName}\${UnzippedFilePath}"
449+
Expand-Archive -Path "${Path}" -DestinationPath "${Downloads}\${UnzipFolderName}" -Force
450+
$ExecutablePath = "${Downloads}\${UnzipFolderName}\${UnzippedFilePath}"
389451
} else {
390-
Install-Executable -Name "${Name}" -Path "${Downloads}\${Filename}"
452+
$ExecutablePath = "${Downloads}\${Filename}"
391453
}
454+
Install-Executable -Name "${Name}" -Path "${ExecutablePath}" -BypassAuthenticode:$BypassAuthenticode
392455
}
393456

394457
function Install-Geekbench {
@@ -705,6 +768,42 @@ function Test-Admin {
705768
return $CurrentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
706769
}
707770

771+
function Test-AuthenticodeSignature {
772+
<#
773+
.SYNOPSIS
774+
Validate that a file has a valid Authenticode signature
775+
#>
776+
param(
777+
[Parameter(Mandatory=$true)][string]$FilePath,
778+
[switch]$Silent = $false
779+
)
780+
$Signature = Get-AuthenticodeSignature -FilePath "${FilePath}"
781+
$Failed = $Signature.Status -ne "Valid"
782+
if (-not $Silent) {
783+
$Status = "Status: $($Signature.Status) ($($Signature.StatusMessage))"
784+
if ($Failed) {
785+
Show-Output -ForegroundColor Red "Authenticode signature verification failed for `"$($Signature.Path)`"."
786+
Show-Output -ForegroundColor Red "${Status}"
787+
Show-Output -ForegroundColor Red "This may be an indication of malicious tampering with the file."
788+
Show-Output -ForegroundColor Red "Please contact your system administrator or the developers of the software."
789+
} else {
790+
Show-Output "Authenticode signature verification successful for `"$($Signature.Path)`"."
791+
Show-Output "${Status}, Signature type: $($Signature.SignatureType)"
792+
}
793+
$Signer = $Signature.SignerCertificate
794+
if ($Signer) {
795+
Show-Output "Certificate: $(Get-CertificateInfo $Signer)"
796+
}
797+
$TimeStamper = $Signature.TimeStamperCertificate
798+
if ($TimeStamper) {
799+
Show-Output "Timestamper: $(Get-CertificateInfo $TimeStamper)"
800+
}
801+
}
802+
if ($Failed) {
803+
throw "Authenticode signature verification failed for `"$($Signature.Path)`"."
804+
}
805+
}
806+
708807
function Test-CommandExists {
709808
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Not plural")]
710809
[OutputType([bool])]

0 commit comments

Comments
 (0)