Skip to content

Instantly share code, notes, and snippets.

@martincostello
Last active November 6, 2025 11:00
Show Gist options
  • Select an option

  • Save martincostello/132011ee7ec00688379eefee578f8c0f to your computer and use it in GitHub Desktop.

Select an option

Save martincostello/132011ee7ec00688379eefee578f8c0f to your computer and use it in GitHub Desktop.
Verify NuGet Package DLL Attestations
<#
.SYNOPSIS
Downloads a specific NuGet package (.nupkg), extracts it, and runs
`gh attestation verify --owner <owner> <file>` for each .dll found under the extracted contents.
Writes an error to the console if `gh` returns a non-zero exit code for any file,
and exits with non-zero code if any verification fails.
.PARAMETER Package
The NuGet package ID (e.g. Polly.Core).
.PARAMETER Version
The package version (e.g. 8.6.4).
.PARAMETER Owner
The GitHub owner to pass to `gh attestation verify --owner` (e.g. App-vNext).
.PARAMETER OutputDir
Where to save the downloaded .nupkg and the extracted files. Defaults to a temporary directory under the current folder.
.PARAMETER Keep
Switch. If present, keeps the downloaded .nupkg and extracted files. By default the script cleans up the temporary files on success.
.EXAMPLE
.\download-and-verify-nuget.ps1 -Package "Polly.Core" -Version "8.6.4" -Owner App-vNext
.EXAMPLE
.\download-and-verify-nuget.ps1 -Package "My.Package" -Version "2.0.0" -Owner "my-github-login" -Keep
.NOTES
- Requires `gh` (GitHub CLI) available on PATH.
- Uses the NuGet v3 flat container API to download the nupkg:
https://api.nuget.org/v3-flatcontainer/{id-lower}/{version}/{id-lower}.{version}.nupkg
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0)]
[string] $Package,
[Parameter(Mandatory = $true, Position = 1)]
[string] $Version,
[Parameter(Mandatory = $true, Position = 2)]
[string] $Owner,
[Parameter(Mandatory = $false)]
[string] $OutputDir = (Join-Path -Path (Get-Location) -ChildPath ("nuget_verify_{0}_{1}" -f $Package, $Version)),
[switch] $Keep
)
function Assert-GhAvailable {
$gh = Get-Command gh -ErrorAction SilentlyContinue
if (-not $gh) {
Write-Error "The 'gh' (GitHub CLI) executable was not found on PATH. Install or add it to PATH and try again."
exit 2
}
}
function Download-Nupkg {
param(
[string] $packageId,
[string] $version,
[string] $outPath
)
$idLower = $packageId.ToLowerInvariant()
$fileName = "$($idLower).$($version).nupkg"
$url = "https://api.nuget.org/v3-flatcontainer/$idLower/$version/$fileName"
Write-Verbose "Downloading $url"
try {
Invoke-WebRequest -Uri $url -OutFile $outPath -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error "Failed to download package from $url. $_"
return $false
}
return $true
}
function Extract-Nupkg {
param(
[string] $nupkgPath,
[string] $destDir
)
try {
if (-not (Test-Path -Path $destDir)) {
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
}
if (Get-Command Expand-Archive -ErrorAction SilentlyContinue) {
Expand-Archive -LiteralPath $nupkgPath -DestinationPath $destDir -Force
} else {
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($nupkgPath, $destDir)
}
} catch {
Write-Error "Failed to extract $nupkgPath to $destDir. $_"
return $false
}
return $true
}
function Verify-Dlls {
param(
[string] $rootDir,
[string] $owner
)
$dlls = Get-ChildItem -Path $rootDir -Filter *.dll -Recurse -File -ErrorAction SilentlyContinue
if (-not $dlls -or $dlls.Count -eq 0) {
Write-Warning "No .dll files found under $rootDir"
return @()
}
$failures = @()
foreach ($dll in $dlls) {
$dllPath = $dll.FullName
Write-Host "Verifying attestation for $dllPath ..."
gh attestation verify --owner $owner $dllPath
$exit = $LASTEXITCODE
if ($exit -ne 0) {
$msg = "gh attestation verify failed for '$dllPath' with exit code $exit"
Write-Error $msg
$failures += [PSCustomObject]@{
File = $dllPath
ExitCode = $exit
}
} else {
Write-Host "Verified: $dllPath"
}
}
Write-Output $failures
return $failures
}
try {
Assert-GhAvailable
$outDirFull = [System.IO.Path]::GetFullPath($OutputDir)
$deleteOutDir = $false
if (-not (Test-Path -Path $outDirFull)) {
New-Item -ItemType Directory -Path $outDirFull -Force | Out-Null
$deleteOutDir = $true
}
$idLower = $Package.ToLowerInvariant()
$nupkgFileName = "$($idLower).$($Version).nupkg"
$nupkgPath = Join-Path -Path $outDirFull -ChildPath $nupkgFileName
$extractDir = Join-Path -Path $outDirFull -ChildPath "extracted"
Write-Output "Downloading package $Package v$Version to $nupkgPath"
$downloaded = Download-Nupkg -packageId $Package -version $Version -outPath $nupkgPath
if (-not $downloaded) {
Write-Error "Download failed. Exiting."
exit 3
}
Write-Output "Extracting $nupkgPath to $extractDir"
$extracted = Extract-Nupkg -nupkgPath $nupkgPath -destDir $extractDir
if (-not $extracted) {
Write-Error "Extraction failed. Exiting."
exit 4
}
Write-Output "Searching for DLL files to verify for owner $Owner ..."
$failures = Verify-Dlls -rootDir $extractDir -owner $Owner
Write-Host $failures
if ($failures.Count -gt 0) {
Write-Host ""
Write-Host "Summary: $($failures.Count) file(s) failed verification:"
foreach ($f in $failures) {
Write-Host " - $($f.File) (exit $($f.ExitCode))"
}
if (-not $Keep) {
Write-Output "Cleaning up downloaded/extracted files (use -Keep to preserve files)..."
try {
Remove-Item -LiteralPath $nupkgPath -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $extractDir -Recurse -Force -ErrorAction SilentlyContinue
if ($deleteOutDir) {
Remove-Item -LiteralPath $outDirFull -Force -ErrorAction SilentlyContinue
}
} catch {}
}
exit 1
} else {
Write-Output "All DLL files verified successfully."
if (-not $Keep) {
Write-Output "Cleaning up downloaded/extracted files..."
try {
Remove-Item -LiteralPath $nupkgPath -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $extractDir -Recurse -Force -ErrorAction SilentlyContinue
if ($deleteOutDir) {
Remove-Item -LiteralPath $outDirFull -Force -ErrorAction SilentlyContinue
}
} catch {}
}
exit 0
}
} catch {
Write-Error "Unexpected error: $_"
exit 5
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment