Verify Downloads with SHA256 in PowerShell: Temp, Verify, Atomic Promote
You should treat every downloaded file as untrusted until you prove otherwise. A simple, reliable pattern in PowerShell is to download to a temporary path, compute a SHA-256 hash, compare it case-insensitively to a known-good value, and only then promote the file atomically into its final location. This approach hardens your pipelines against tampering, reduces partial file issues, and makes rollbacks straightforward.
The minimal pattern: temp, verify, promote
Here is a concise pattern you can drop into scripts. It downloads (or stages) to a temp path, verifies SHA-256, and atomically promotes on success.
# Minimal, safe pattern
$src = 'C:\Temp\tool.zip'
$dst = 'C:\Apps\tool.zip'
$expected = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
$dir = Split-Path -Parent $dst
New-Item -ItemType Directory -Path $dir -Force | Out-Null
$h = (Get-FileHash -Path $src -Algorithm SHA256).Hash
if ($h -notlike '*') { throw 'Failed to compute SHA-256.' }
# Case-insensitive comparison is essential (hex may be upper or lower case)
if ($h -ieq $expected) {
$temp = "$dst.part"
if (Test-Path -Path $temp) { Remove-Item -Path $temp -Force }
Copy-Item -Path $src -Destination $temp -Force
# Move within the same volume for an atomic rename
Move-Item -Path $temp -Destination $dst -Force
Write-Host ("Saved -> {0}" -f (Resolve-Path -Path $dst))
} else {
throw ("Hash mismatch. Expected {0} got {1}" -f $expected, $h)
}
- Always compute the hash from the temporary file you intend to promote.
- Compare using case-insensitive logic because publishers may format hexadecimal in upper or lower case.
- Promote using Move-Item on the same volume for atomicity. If temp and destination are on different volumes, the move becomes a copy+delete (not atomic).
- Use the .part extension to clearly mark incomplete artifacts.
A production-ready function you can reuse
The following function encapsulates the workflow: stage to a temp file, compute SHA-256, compare case-insensitively, and atomically promote to the final path. It supports both local sources and HTTP(S) URLs.
function Save-VerifiedFile {
[CmdletBinding()] param(
[Parameter(Mandatory)] [string] $Source, # Local path or HTTP(S) URL
[Parameter(Mandatory)] [string] $DestinationPath, # Final path
[Parameter(Mandatory)] [string] $ExpectedSha256, # 64-char hex string
[int] $TimeoutSec = 300,
[switch] $UseBits # Use BITS for robust downloads
)
$dir = Split-Path -Parent $DestinationPath
New-Item -ItemType Directory -Path $dir -Force | Out-Null
$temp = Join-Path $dir ((Split-Path -Leaf $DestinationPath) + '.part')
if (Test-Path $temp) { Remove-Item $temp -Force -ErrorAction SilentlyContinue }
# Stage the file into $temp
if ($Source -match '^https?://') {
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {}
if ($UseBits) {
Start-BitsTransfer -Source $Source -Destination $temp -RetryInterval 5 -ErrorAction Stop
} else {
Invoke-WebRequest -Uri $Source -OutFile $temp -TimeoutSec $TimeoutSec -ErrorAction Stop
}
} else {
Copy-Item -Path $Source -Destination $temp -Force -ErrorAction Stop
}
# Compute and compare the hash
$hash = (Get-FileHash -Path $temp -Algorithm SHA256).Hash
if (-not [String]::Equals($hash, $ExpectedSha256, [StringComparison]::OrdinalIgnoreCase)) {
Remove-Item $temp -Force -ErrorAction SilentlyContinue
throw ("Hash mismatch. Expected {0} got {1}" -f $ExpectedSha256, $hash)
}
# Atomic promotion within the same volume
Move-Item -Path $temp -Destination $DestinationPath -Force
return (Get-Item $DestinationPath | Select-Object FullName, Length, @{n='Sha256';e={$hash}})
}
Example: verify a file you already downloaded to %TEMP%
$tmp = Join-Path $env:TEMP 'tool.zip'
Invoke-WebRequest -Uri 'https://example.com/tool.zip' -OutFile $tmp -ErrorAction Stop
$expected = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
Save-VerifiedFile -Source $tmp -DestinationPath 'C:\Apps\tool.zip' -ExpectedSha256 $expected | Format-List
Example: verify and save straight from a URL
$expected = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
Save-VerifiedFile -Source 'https://example.com/tool.zip' -DestinationPath 'C:\Apps\tool.zip' -ExpectedSha256 $expected
Example: parse a sidecar .sha256 or .sha256sum file
Vendors often publish a sidecar file containing the expected hash. Parse the first token as the hash.
$sumLine = Get-Content 'C:\Downloads\tool.zip.sha256' -Raw
$expected = ($sumLine -split '\\s+')[0]
Save-VerifiedFile -Source 'https://example.com/tool.zip' -DestinationPath 'C:\Apps\tool.zip' -ExpectedSha256 $expected
Hardening tips and CI/CD integration
Make atomic promotion truly atomic
- Keep the temp file and destination on the same volume so Move-Item performs a rename, not a copy. If you must cross volumes, stage the download in the same directory as the final destination.
- Use a .part extension to clearly identify incomplete artifacts and simplify cleanup.
Case-insensitive and strict comparisons
- Use [String]::Equals($a, $b, OrdinalIgnoreCase) or -ieq for case-insensitive comparison; never rely on casing of published hashes.
- Validate inputs: ensure $ExpectedSha256 matches ^[0-9A-Fa-f]{64}$ to avoid accidental whitespace or malformed values.
Prefer resilient downloads for large files
- Start-BitsTransfer is resilient to transient network errors and supports background transfers. It writes directly to the target path you specify, which you should set to your .part file.
- For Invoke-WebRequest, set -TimeoutSec and consider retries around the download step.
Add optional authenticity checks
- Hash validation ensures integrity. For authenticity, verify signatures if the vendor provides them.
- Windows binaries: Get-AuthenticodeSignature 'C:\Apps\tool.exe' and confirm SignatureStatus is Valid.
- Open-source projects: verify a detached signature with GPG in addition to SHA-256.
Logging, observability, and rollbacks
- Log the final path, size, and hash so you can trace exactly what was deployed.
- To support rollbacks, keep the previous file as a .bak or versioned path before promotion, then delete once the new file passes health checks.
$dst = 'C:\Apps\tool.zip'
$temp = "$dst.part"
if (Test-Path $dst) { Move-Item $dst ($dst + '.bak') -Force }
Move-Item $temp $dst -Force
# Later, remove the .bak if the new artifact passes validation
Wire it into CI/CD
- Use Save-VerifiedFile in your pipeline step that fetches third-party tools, CLI dependencies, or build artifacts.
- Fail the job on any mismatch; do not soft-warn. A mismatch should be treated as a potential supply-chain event.
- Cache verified artifacts by hash to avoid re-downloading known-good binaries across jobs.
# Example pipeline script fragment
$toolUri = 'https://example.com/releases/tool-win-x64.zip'
$expected = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
$final = 'C:\Tools\tool.zip'
$result = Save-VerifiedFile -Source $toolUri -DestinationPath $final -ExpectedSha256 $expected -UseBits
Write-Host ("Verified {0} ({1} bytes) with SHA-256 {2}" -f $result.FullName, $result.Length, $result.Sha256)
Security best practices to remember
- Always verify before use. Never execute, extract, or import a file until the hash matches the expected value.
- Pin to exact versions and expected hashes in code or configuration; avoid floating tags like latest.
- Fetch expected hashes over authenticated or TLS-protected channels, and prefer vendor-official sources of truth (release notes, signed checksums).
- Run your promotion step with the least privileges required and write to directories with well-defined ACLs.
Further reading
Build safer pipelines with PowerShell. PowerShell Advanced CookBook: https://www.amazon.com/PowerShell-Advanced-Cookbook-scripting-advanced-ebook/dp/B0D5CPP2CQ/
What you get
- Safer downloads: integrity and authenticity checks before any use.
- Predictable updates: version-pinned, hash-verified artifacts.
- Cleaner logs: explicit records of what was installed and when.
- Simpler rollbacks: keep prior versions and atomically swap.