diff --git a/.github/workflows/release-velopack.yml b/.github/workflows/release-velopack.yml index 618b5aa..9808409 100644 --- a/.github/workflows/release-velopack.yml +++ b/.github/workflows/release-velopack.yml @@ -366,6 +366,35 @@ jobs: tenant-id: ${{ vars.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + # The pinned official action installs this tool with --add-source, which + # conflicts with NuGet package source mapping. Stage its verified package + # in the action's own dependency directory without changing caller policy. + - name: Prepare pinned Artifact Signing CLI dependency + if: inputs.sign + shell: pwsh + run: | + param( + [string]$PackageDirectory = $env:RUNNER_TEMP, + [string]$InstallRoot = $env:LOCALAPPDATA + ) + $ErrorActionPreference = 'Stop' + $version = '0.9.1-beta.26227.3' + $package = Join-Path $PackageDirectory "sign.$version.nupkg" + $destination = Join-Path $InstallRoot "ArtifactSigning/sign/sign.$version" + if (Test-Path -LiteralPath $destination) { throw 'Artifact Signing CLI directory already exists; use a fresh release runner.' } + Invoke-WebRequest -Uri "https://api.nuget.org/v3-flatcontainer/sign/$version/sign.$version.nupkg" -OutFile $package + if ((Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash -ne '34FD0D4AEABDBC363A48883881865B4DD65E6C11BA916028082FA23F3E1B1BA1') + { + throw 'Artifact Signing CLI package checksum mismatch.' + } + dotnet nuget verify $package --all + if ($LASTEXITCODE -ne 0) { throw 'Artifact Signing CLI NuGet signature verification failed.' } + [IO.Compression.ZipFile]::ExtractToDirectory($package, $destination) + if (-not (Test-Path -LiteralPath (Join-Path $destination 'tools/net8.0/any/sign.dll') -PathType Leaf)) + { + throw 'Artifact Signing CLI package is missing its executable.' + } + # Signs the app's own executables and DLLs (and the CLI) before packing, so the # binaries inside the installer, the portable zip, and the update packages are # all signed (K22-REL-05). diff --git a/tools/VelopackSigning.Tests.ps1 b/tools/VelopackSigning.Tests.ps1 index fefa988..fe48a1d 100644 --- a/tools/VelopackSigning.Tests.ps1 +++ b/tools/VelopackSigning.Tests.ps1 @@ -14,6 +14,13 @@ BeforeAll ` if($start -lt 0 -or $end -ge $lines.Count) { throw 'Cannot locate the actual Velopack signing wrapper.' } $script:Wrapper = Join-Path $TestDrive 'sign-one.ps1' [IO.File]::WriteAllLines($script:Wrapper, @($lines[($start + 1)..($end - 1)] | ForEach-Object { $_.Substring(10) })) + $dependencyStart = [Array]::IndexOf($lines, ' - name: Prepare pinned Artifact Signing CLI dependency') + if($dependencyStart -lt 0) { throw 'Cannot locate the actual signing dependency bootstrap.' } + $dependencyStart += 4 + $dependencyEnd = $dependencyStart + while($dependencyEnd -lt $lines.Count -and $lines[$dependencyEnd].StartsWith(' ')) { $dependencyEnd++ } + $script:Bootstrap = Join-Path $TestDrive 'sign-dependency.ps1' + [IO.File]::WriteAllLines($script:Bootstrap, @($lines[$dependencyStart..($dependencyEnd - 1)] | ForEach-Object { $_.Substring(10) })) $script:OldTool = $env:K22_VPK_SIGN_TOOL $env:K22_VPK_SIGN_TOOL = 'Invoke-WrapperTool' @@ -31,6 +38,69 @@ BeforeAll ` } } +Describe 'The actual Artifact Signing dependency bootstrap' ` +{ + BeforeEach ` + { + $script:InstallRoot = Join-Path $TestDrive ([guid]::NewGuid().ToString('N')) + $script:PackageDirectory = Join-Path $script:InstallRoot 'downloads' + [void][IO.Directory]::CreateDirectory($script:PackageDirectory) + $script:Destination = Join-Path $script:InstallRoot 'ArtifactSigning/sign/sign.0.9.1-beta.26227.3' + Mock Invoke-WebRequest ` + { + $fixture = Join-Path (Split-Path -Parent $OutFile) 'fixture' + [void][IO.Directory]::CreateDirectory((Join-Path $fixture 'tools/net8.0/any')) + [IO.File]::WriteAllText((Join-Path $fixture 'tools/net8.0/any/sign.dll'), 'fixture') + [IO.Compression.ZipFile]::CreateFromDirectory($fixture, $OutFile) + } + Mock Get-FileHash { [pscustomobject]@{ Hash = '34FD0D4AEABDBC363A48883881865B4DD65E6C11BA916028082FA23F3E1B1BA1' } } + Mock dotnet { $global:LASTEXITCODE = 0 } + } + + It 'stages the verified tool where the official action finds it' ` + { + & $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory + Get-Content -LiteralPath (Join-Path $script:Destination 'tools/net8.0/any/sign.dll') | Should -Be 'fixture' + Should -Invoke dotnet -Exactly 1 + } + + It 'rejects a changed download before verification or extraction' ` + { + Mock Get-FileHash { [pscustomobject]@{ Hash = 'bad' } } + { & $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory } | Should -Throw '*checksum mismatch*' + Test-Path -LiteralPath $script:Destination | Should -BeFalse + Should -Invoke dotnet -Exactly 0 + } + + It 'rejects an invalid NuGet signature before extraction' ` + { + Mock dotnet { $global:LASTEXITCODE = 1 } + { & $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory } | Should -Throw '*signature verification failed*' + Test-Path -LiteralPath $script:Destination | Should -BeFalse + } + + It 'preserves a preexisting dependency directory' ` + { + [void][IO.Directory]::CreateDirectory($script:Destination) + $marker = Join-Path $script:Destination 'marker.txt' + [IO.File]::WriteAllText($marker, 'preserve') + { & $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory } | Should -Throw '*already exists*' + Get-Content -LiteralPath $marker | Should -Be 'preserve' + Should -Invoke Invoke-WebRequest -Exactly 0 + } + + It 'rejects a package without the expected executable' ` + { + Mock Invoke-WebRequest ` + { + $emptyFixture = Join-Path (Split-Path -Parent $OutFile) 'empty' + [void][IO.Directory]::CreateDirectory($emptyFixture) + [IO.Compression.ZipFile]::CreateFromDirectory($emptyFixture, $OutFile) + } + { & $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory } | Should -Throw '*missing its executable*' + } +} + AfterAll ` { $env:K22_VPK_SIGN_TOOL = $script:OldTool