Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16e528bc53
|
||
|
|
cd20e945bd
|
No files matched your search
@@ -1,32 +0,0 @@
|
||||
# Gitea CI for the standards repository. The .github workflows are the reusable
|
||||
# GitHub gates this repository publishes; Gitea does not run them.
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
jobs:
|
||||
docs:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/lint.yaml@v1
|
||||
with:
|
||||
markdownlint: '**/*.md'
|
||||
yamllint: |
|
||||
**/*.yml
|
||||
**/*.yaml
|
||||
# Internal links and anchors between documents (offline: deterministic). The
|
||||
# static musl build: the image's glibc is older than the gnu build needs.
|
||||
commands: |
|
||||
LYCHEE_VERSION=0.24.2
|
||||
LYCHEE_SHA256=73657a111819a30c47c08352896796f23d64e4eb2b3ed39b6d32149241566fc5
|
||||
archive=lychee-x86_64-unknown-linux-musl.tar.gz
|
||||
mkdir -p /tmp/lychee
|
||||
curl -fsSLo "/tmp/lychee/$archive" "https://github.com/lycheeverse/lychee/releases/download/lychee-v$LYCHEE_VERSION/$archive"
|
||||
(cd /tmp/lychee && echo "$LYCHEE_SHA256 $archive" | sha256sum --check --strict && tar -xzf "$archive")
|
||||
/tmp/lychee/lychee-x86_64-unknown-linux-musl/lychee --offline --include-fragments --no-progress './**/*.md'
|
||||
conformance:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/windows.yaml@v1
|
||||
with:
|
||||
commands: |
|
||||
pwsh -NoProfile -File ./tools/Test-RepoConformance.ps1 -SelfTest
|
||||
pwsh -NoProfile -File ./tools/Test-RepoConformance.Fuzz.ps1 -Seed 22023 -Iterations 1000
|
||||
pwsh -NoProfile -File ./tools/Test-RepoConformance.Fuzz.ps1 -Seed 99173 -Iterations 1000
|
||||
pwsh -NoProfile -File ./tools/Test-StandardsPackage.ps1
|
||||
pwsh -NoProfile -File ./tools/Test-RepoConformance.ps1 -Repository KofTwentyTwo/standards -LocalPath . -StaticOnly
|
||||
@@ -1,70 +0,0 @@
|
||||
name: Windows signing verification
|
||||
description: Prepare explicit owned-file catalogs and verify final Authenticode signatures and preserved binary bytes.
|
||||
inputs:
|
||||
operation:
|
||||
description: prepare, verify, or verify-distribution.
|
||||
required: true
|
||||
root:
|
||||
description: Root directory for the files to sign.
|
||||
default: .
|
||||
signing-files:
|
||||
description: Newline-separated required file names or relative path patterns. No blanket dependency signing.
|
||||
default: ""
|
||||
preserve-other-binaries:
|
||||
description: Record and check binaries outside the signing catalog.
|
||||
default: "false"
|
||||
expected-subject:
|
||||
description: Verified certificate subject read from the deployed Azure profile.
|
||||
default: ""
|
||||
manifest:
|
||||
description: Manifest created by prepare; empty uses RUNNER_TEMP/k22-signing-manifest.json.
|
||||
default: ""
|
||||
directory:
|
||||
description: Directory of final packaged assets to verify.
|
||||
default: ""
|
||||
outputs:
|
||||
catalog:
|
||||
description: File catalog consumed by azure/artifact-signing-action.
|
||||
value: ${{ steps.run.outputs.catalog }}
|
||||
manifest:
|
||||
description: Owned files and third-party preservation manifest.
|
||||
value: ${{ steps.run.outputs.manifest }}
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- id: run
|
||||
shell: pwsh
|
||||
env:
|
||||
K22_SIGNING_OPERATION: ${{ inputs.operation }}
|
||||
K22_SIGNING_ROOT: ${{ inputs.root }}
|
||||
K22_SIGNING_FILES: ${{ inputs.signing-files }}
|
||||
K22_SIGNING_PRESERVE: ${{ inputs.preserve-other-binaries }}
|
||||
K22_SIGNING_SUBJECT: ${{ inputs.expected-subject }}
|
||||
K22_SIGNING_MANIFEST: ${{ inputs.manifest }}
|
||||
K22_SIGNING_DIRECTORY: ${{ inputs.directory }}
|
||||
run: |
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Import-Module (Join-Path $env:GITHUB_ACTION_PATH '../../../tools/WindowsSigning.psm1') -Force
|
||||
$manifest = $env:K22_SIGNING_MANIFEST
|
||||
if (-not $manifest) { $manifest = Join-Path $env:RUNNER_TEMP 'k22-signing-manifest.json' }
|
||||
$catalog = Join-Path $env:RUNNER_TEMP 'k22-signing-catalog.txt'
|
||||
switch ($env:K22_SIGNING_OPERATION)
|
||||
{
|
||||
'prepare' {
|
||||
$patterns = @($env:K22_SIGNING_FILES -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
if ($patterns.Count -eq 0) { throw 'Explicit owned signing-files are required.' }
|
||||
if ([string]::IsNullOrWhiteSpace($env:K22_SIGNING_SUBJECT)) { throw 'The verified publisher subject is required before selecting signing files.' }
|
||||
New-WindowsSigningCatalog -Root $env:K22_SIGNING_ROOT -Patterns $patterns -CatalogPath $catalog -ManifestPath $manifest -ExpectedSubject $env:K22_SIGNING_SUBJECT -PreserveOtherBinaries:($env:K22_SIGNING_PRESERVE -eq 'true')
|
||||
}
|
||||
'verify' {
|
||||
Assert-WindowsSigningManifest -ManifestPath $manifest -ExpectedSubject $env:K22_SIGNING_SUBJECT
|
||||
}
|
||||
'verify-distribution' {
|
||||
Assert-WindowsSigningManifest -ManifestPath $manifest -ExpectedSubject $env:K22_SIGNING_SUBJECT
|
||||
Assert-WindowsDistribution -Directory $env:K22_SIGNING_DIRECTORY -ManifestPath $manifest -ExpectedSubject $env:K22_SIGNING_SUBJECT
|
||||
}
|
||||
default { throw "Unsupported signing operation: $env:K22_SIGNING_OPERATION." }
|
||||
}
|
||||
"catalog=$catalog" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||
"manifest=$manifest" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
contents: read
|
||||
security-events: write # upload SARIF results to code scanning
|
||||
with:
|
||||
languages: '["actions", "javascript-typescript"]'
|
||||
languages: '["actions"]'
|
||||
|
||||
docs:
|
||||
name: docs
|
||||
@@ -112,60 +112,9 @@ jobs:
|
||||
shell: pwsh
|
||||
run: ./tools/Test-RepoConformance.ps1 -LocalPath . -StaticOnly
|
||||
|
||||
windows-signing:
|
||||
name: windows-signing
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Lint and test Windows signing gates
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Install-Module PSScriptAnalyzer -RequiredVersion 1.25.0 -Scope CurrentUser -Force
|
||||
Install-Module Pester -RequiredVersion 5.7.1 -Scope CurrentUser -Force
|
||||
Import-Module PSScriptAnalyzer -RequiredVersion 1.25.0
|
||||
Import-Module Pester -RequiredVersion 5.7.1
|
||||
# Keep every configured rule; isolate concurrent command-metadata consumers.
|
||||
# https://github.com/PowerShell/PSScriptAnalyzer/issues/1708
|
||||
$settings = 'configs/shell/PSScriptAnalyzerSettings.psd1'
|
||||
$casingFixture = @(Invoke-ScriptAnalyzer -ScriptDefinition "get-content -literalpath 'fixture.txt'" -Settings $settings -IncludeRule PSUseCorrectCasing)
|
||||
if ('PSUseCorrectCasing' -notin $casingFixture.RuleName) { throw 'The casing pass did not detect incorrect casing.' }
|
||||
$reservedFixture = @(Invoke-ScriptAnalyzer -ScriptDefinition 'function Get-Fixture? { [CmdletBinding()] param() }; Export-ModuleMember -Function Get-Fixture?' -Settings $settings -IncludeRule PSReservedCmdletChar)
|
||||
if ('PSReservedCmdletChar' -notin $reservedFixture.RuleName) { throw 'The reserved-character pass did not detect the exported function.' }
|
||||
$remainingFixture = @(Invoke-ScriptAnalyzer -ScriptDefinition "Write-Host 'fixture'" -Settings $settings -ExcludeRule PSUseCorrectCasing, PSReservedCmdletChar)
|
||||
if ('PSAvoidUsingWriteHost' -notin $remainingFixture.RuleName) { throw 'The remaining-rule pass did not detect Write-Host.' }
|
||||
foreach ($path in @('tools/WindowsSigning.psm1', 'tools/WindowsSigning.Tests.ps1', 'tools/VelopackSigning.Tests.ps1'))
|
||||
{
|
||||
Write-Output "Analyzing $path"
|
||||
try
|
||||
{
|
||||
$findings = @(
|
||||
Invoke-ScriptAnalyzer -Path $path -Settings $settings -IncludeRule PSUseCorrectCasing
|
||||
Invoke-ScriptAnalyzer -Path $path -Settings $settings -IncludeRule PSReservedCmdletChar
|
||||
Invoke-ScriptAnalyzer -Path $path -Settings $settings -ExcludeRule PSUseCorrectCasing, PSReservedCmdletChar
|
||||
)
|
||||
}
|
||||
catch
|
||||
{
|
||||
Write-Output $_.Exception.ToString()
|
||||
throw
|
||||
}
|
||||
if ($findings.Count -gt 0) { $findings | Format-Table; throw "Script analysis failed: $path" }
|
||||
}
|
||||
$configuration = New-PesterConfiguration
|
||||
$configuration.Run.Path = @('tools/WindowsSigning.Tests.ps1', 'tools/VelopackSigning.Tests.ps1')
|
||||
$configuration.Run.Exit = $true
|
||||
Invoke-Pester -Configuration $configuration
|
||||
|
||||
release-please:
|
||||
name: release-please
|
||||
needs: [security, codeql, docs, conformance, windows-signing]
|
||||
needs: [security, codeql, docs, conformance]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && vars.RELEASE_APP_CLIENT_ID != ''
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
|
||||
@@ -64,14 +64,6 @@ on:
|
||||
description: Newline-separated test projects to run with coverage before packing.
|
||||
type: string
|
||||
default: ""
|
||||
sign:
|
||||
description: Authenticode-sign owned Windows binaries before packing, using Azure OIDC in the protected release environment.
|
||||
type: boolean
|
||||
default: false
|
||||
signing-files:
|
||||
description: Newline-separated required owned binary paths relative to the checkout; required when sign is true.
|
||||
type: string
|
||||
default: ""
|
||||
outputs:
|
||||
version:
|
||||
description: The released version (tag without the leading v).
|
||||
@@ -87,10 +79,8 @@ jobs:
|
||||
name: build
|
||||
runs-on: ${{ inputs.runs-on }}
|
||||
timeout-minutes: 60
|
||||
environment: release
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # Azure OIDC is available only in the protected release environment.
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
prerelease: ${{ steps.version.outputs.prerelease }}
|
||||
@@ -196,86 +186,12 @@ jobs:
|
||||
}
|
||||
if ($failed) { exit 1 }
|
||||
|
||||
- name: Prepare owned binary signing catalog
|
||||
if: inputs.sign
|
||||
id: signing
|
||||
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
|
||||
with:
|
||||
operation: prepare
|
||||
signing-files: ${{ inputs.signing-files }}
|
||||
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
|
||||
|
||||
- name: Require verified Azure signing configuration
|
||||
if: inputs.sign
|
||||
shell: pwsh
|
||||
env:
|
||||
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
|
||||
AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
|
||||
ARTIFACT_SIGNING_ENDPOINT: ${{ vars.ARTIFACT_SIGNING_ENDPOINT || vars.TRUSTED_SIGNING_ENDPOINT }}
|
||||
ARTIFACT_SIGNING_ACCOUNT: ${{ vars.ARTIFACT_SIGNING_ACCOUNT || vars.TRUSTED_SIGNING_ACCOUNT }}
|
||||
ARTIFACT_SIGNING_PROFILE: ${{ vars.ARTIFACT_SIGNING_PROFILE || vars.TRUSTED_SIGNING_PROFILE }}
|
||||
ARTIFACT_SIGNING_SUBJECT: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
|
||||
run: |
|
||||
foreach ($name in @('AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_SUBSCRIPTION_ID', 'ARTIFACT_SIGNING_ENDPOINT', 'ARTIFACT_SIGNING_ACCOUNT', 'ARTIFACT_SIGNING_PROFILE', 'ARTIFACT_SIGNING_SUBJECT'))
|
||||
{
|
||||
if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) { throw "Missing verified signing variable: $name" }
|
||||
}
|
||||
|
||||
- name: Azure login (OIDC)
|
||||
if: inputs.sign
|
||||
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0
|
||||
with:
|
||||
client-id: ${{ vars.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Sign owned binaries before packing
|
||||
if: inputs.sign
|
||||
uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
|
||||
with:
|
||||
endpoint: ${{ vars.ARTIFACT_SIGNING_ENDPOINT || vars.TRUSTED_SIGNING_ENDPOINT }}
|
||||
signing-account-name: ${{ vars.ARTIFACT_SIGNING_ACCOUNT || vars.TRUSTED_SIGNING_ACCOUNT }}
|
||||
certificate-profile-name: ${{ vars.ARTIFACT_SIGNING_PROFILE || vars.TRUSTED_SIGNING_PROFILE }}
|
||||
files-catalog: ${{ steps.signing.outputs.catalog }}
|
||||
file-digest: SHA256
|
||||
timestamp-rfc3161: http://timestamp.acs.microsoft.com
|
||||
timestamp-digest: SHA256
|
||||
cache-dependencies: false
|
||||
exclude-environment-credential: true
|
||||
exclude-workload-identity-credential: true
|
||||
exclude-managed-identity-credential: true
|
||||
exclude-shared-token-cache-credential: true
|
||||
exclude-visual-studio-credential: true
|
||||
exclude-visual-studio-code-credential: true
|
||||
exclude-azure-cli-credential: false
|
||||
exclude-azure-powershell-credential: true
|
||||
exclude-azure-developer-cli-credential: true
|
||||
exclude-interactive-browser-credential: true
|
||||
|
||||
- name: Verify signed binaries
|
||||
if: inputs.sign
|
||||
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
|
||||
with:
|
||||
operation: verify
|
||||
manifest: ${{ steps.signing.outputs.manifest }}
|
||||
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
|
||||
|
||||
- name: Pack
|
||||
shell: pwsh
|
||||
env:
|
||||
K22_VERSION: ${{ steps.version.outputs.version }}
|
||||
run: dotnet pack $env:K22_SOLUTION -c Release "-p:Platform=$env:K22_PLATFORM" "-p:Version=$env:K22_VERSION" --no-build -o artifacts/nuget
|
||||
|
||||
- name: Verify the exact binaries shipped in NuGet packages
|
||||
if: inputs.sign
|
||||
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
|
||||
with:
|
||||
operation: verify-distribution
|
||||
directory: artifacts/nuget
|
||||
manifest: ${{ steps.signing.outputs.manifest }}
|
||||
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
|
||||
|
||||
# Maps each package to the project folder whose packages.lock.json describes its
|
||||
# dependencies, for the SBOM job. Package ids come from the produced file names.
|
||||
- name: List packages
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
# app unpackaged and self-contained, and the optional CLI as a single file.
|
||||
# package (Windows, environment `release`): optionally Authenticode-sign the
|
||||
# published binaries with Azure Trusted Signing over OIDC (input `sign`,
|
||||
# enabled after repository signing verification), download the previous release on the
|
||||
# off until exception EX-0002 closes), download the previous release on the
|
||||
# same channel as the delta baseline, `vpk pack` (which signs Setup.exe with
|
||||
# the same identity when `sign` is on), and zip the CLI. Only this build's
|
||||
# Velopack assets (from assets.<channel>.json) are kept, with a
|
||||
@@ -100,16 +100,7 @@ on:
|
||||
Authenticode-sign binaries and Setup.exe with Azure Trusted Signing (OIDC). Requires the
|
||||
variables AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, TRUSTED_SIGNING_ENDPOINT,
|
||||
TRUSTED_SIGNING_ACCOUNT, and TRUSTED_SIGNING_PROFILE in the caller's `release` environment.
|
||||
ARTIFACT_SIGNING_SUBJECT must contain the verified publisher subject. Legacy TRUSTED_SIGNING_*
|
||||
names remain accepted; ARTIFACT_SIGNING_* names take precedence.
|
||||
type: boolean
|
||||
default: false
|
||||
signing-files:
|
||||
description: Newline-separated required owned file names or publish-relative patterns, including the optional CLI; required when sign is true.
|
||||
type: string
|
||||
default: ""
|
||||
msi:
|
||||
description: Generate a per-user MSI alongside Setup.exe and portable ZIP; requires the same signing and verification.
|
||||
Off until exception EX-0002 (signing identity validation) closes.
|
||||
type: boolean
|
||||
default: false
|
||||
outputs:
|
||||
@@ -334,34 +325,9 @@ jobs:
|
||||
name: publish-cli
|
||||
path: publish/cli
|
||||
|
||||
- name: Prepare owned binary signing catalog
|
||||
if: inputs.sign
|
||||
id: signing
|
||||
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
|
||||
with:
|
||||
operation: prepare
|
||||
root: publish
|
||||
signing-files: ${{ inputs.signing-files }}
|
||||
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
|
||||
preserve-other-binaries: "true"
|
||||
|
||||
- name: Require verified Azure signing configuration
|
||||
if: inputs.sign
|
||||
shell: pwsh
|
||||
env:
|
||||
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
|
||||
AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
|
||||
ARTIFACT_SIGNING_ENDPOINT: ${{ vars.ARTIFACT_SIGNING_ENDPOINT || vars.TRUSTED_SIGNING_ENDPOINT }}
|
||||
ARTIFACT_SIGNING_ACCOUNT: ${{ vars.ARTIFACT_SIGNING_ACCOUNT || vars.TRUSTED_SIGNING_ACCOUNT }}
|
||||
ARTIFACT_SIGNING_PROFILE: ${{ vars.ARTIFACT_SIGNING_PROFILE || vars.TRUSTED_SIGNING_PROFILE }}
|
||||
ARTIFACT_SIGNING_SUBJECT: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
|
||||
run: |
|
||||
foreach ($name in @('AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_SUBSCRIPTION_ID', 'ARTIFACT_SIGNING_ENDPOINT', 'ARTIFACT_SIGNING_ACCOUNT', 'ARTIFACT_SIGNING_PROFILE', 'ARTIFACT_SIGNING_SUBJECT'))
|
||||
{
|
||||
if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) { throw "Missing verified signing variable: $name" }
|
||||
}
|
||||
|
||||
# Signing is OFF by default: exception EX-0002 records that the Azure Trusted
|
||||
# Signing identity is still being validated. Until then, integrity and origin rest
|
||||
# on SHA256SUMS and the build-provenance attestations (K22-REL-04).
|
||||
- name: Azure login (OIDC)
|
||||
if: inputs.sign
|
||||
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0
|
||||
@@ -370,48 +336,37 @@ 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
|
||||
# Signs the executables and DLLs under publish that carry no valid signature
|
||||
# yet (the app's own binaries and unsigned third-party ones), so the binaries
|
||||
# inside the installer, the portable zip and the update packages are all
|
||||
# signed (K22-REL-05). Already-signed files (the .NET runtime, Windows App SDK)
|
||||
# keep their publisher's signature and are not re-signed: a self-contained
|
||||
# publish holds ~300 of them at ~2 s each, which is how the first real run
|
||||
# (gclo v1.0.2-beta.2, #26) hit the signing action's 300 s default timeout.
|
||||
- name: Select the binaries to sign
|
||||
if: inputs.sign
|
||||
id: sign-list
|
||||
shell: pwsh
|
||||
env:
|
||||
K22_MAIN_EXE: ${{ inputs.main-exe }}
|
||||
K22_CLI_PROJECT: ${{ inputs.cli-project }}
|
||||
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.'
|
||||
}
|
||||
} @args
|
||||
$all = Get-ChildItem publish -Recurse -Include *.exe, *.dll
|
||||
$unsigned = @($all | Where-Object { (Get-AuthenticodeSignature -LiteralPath $_.FullName).Status -ne 'Valid' })
|
||||
$names = $unsigned | ForEach-Object { $_.Name }
|
||||
if ($names -notcontains $env:K22_MAIN_EXE) { throw "publish/app/$env:K22_MAIN_EXE is missing or already signed by someone else." }
|
||||
if ($env:K22_CLI_PROJECT -and -not ($unsigned | Where-Object { $_.Extension -eq '.exe' -and $_.FullName -like '*\publish\cli\*' })) { throw 'publish/cli has no unsigned exe to sign.' }
|
||||
Write-Output "Signing $($unsigned.Count) of $($all.Count) binaries; $($all.Count - $unsigned.Count) already carry a valid signature."
|
||||
"files<<EOF`n$(($unsigned.FullName) -join "`n")`nEOF" | Add-Content -LiteralPath $env:GITHUB_OUTPUT
|
||||
|
||||
# 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).
|
||||
- name: Sign only owned published binaries (Azure Artifact Signing)
|
||||
- name: Sign published binaries (Azure Trusted Signing)
|
||||
if: inputs.sign
|
||||
uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
|
||||
with:
|
||||
endpoint: ${{ vars.ARTIFACT_SIGNING_ENDPOINT || vars.TRUSTED_SIGNING_ENDPOINT }}
|
||||
signing-account-name: ${{ vars.ARTIFACT_SIGNING_ACCOUNT || vars.TRUSTED_SIGNING_ACCOUNT }}
|
||||
certificate-profile-name: ${{ vars.ARTIFACT_SIGNING_PROFILE || vars.TRUSTED_SIGNING_PROFILE }}
|
||||
files-catalog: ${{ steps.signing.outputs.catalog }}
|
||||
endpoint: ${{ vars.TRUSTED_SIGNING_ENDPOINT }}
|
||||
signing-account-name: ${{ vars.TRUSTED_SIGNING_ACCOUNT }}
|
||||
certificate-profile-name: ${{ vars.TRUSTED_SIGNING_PROFILE }}
|
||||
files: ${{ steps.sign-list.outputs.files }}
|
||||
file-digest: SHA256
|
||||
timestamp-rfc3161: http://timestamp.acs.microsoft.com
|
||||
timestamp-digest: SHA256
|
||||
@@ -423,60 +378,41 @@ jobs:
|
||||
exclude-shared-token-cache-credential: true
|
||||
exclude-visual-studio-credential: true
|
||||
exclude-visual-studio-code-credential: true
|
||||
exclude-azure-cli-credential: false
|
||||
exclude-azure-powershell-credential: true
|
||||
exclude-azure-developer-cli-credential: true
|
||||
exclude-interactive-browser-credential: true
|
||||
|
||||
- name: Verify signed binaries and preserved dependencies
|
||||
if: inputs.sign
|
||||
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
|
||||
with:
|
||||
operation: verify
|
||||
manifest: ${{ steps.signing.outputs.manifest }}
|
||||
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
|
||||
|
||||
# vpk signs what it adds (Setup.exe and the update stub) with the same identity,
|
||||
# reading this metadata file and the Azure CLI login above.
|
||||
- name: Write signing metadata for vpk
|
||||
# vpk signs what it adds (Setup.exe, Update.exe, the execution stub) with the
|
||||
# same identity. It calls this wrapper once per file (--signTemplate) instead
|
||||
# of its built-in Azure path (--azureTrustedSignFile), which hung for over ten
|
||||
# minutes on nine files in testing; the wrapper uses the SignTool and the
|
||||
# Artifact Signing client the signing step above already installed, with the
|
||||
# Azure CLI login, and skips files that already carry a valid signature.
|
||||
- name: Write the signing wrapper for vpk
|
||||
if: inputs.sign
|
||||
shell: pwsh
|
||||
env:
|
||||
TRUSTED_SIGNING_ENDPOINT: ${{ vars.ARTIFACT_SIGNING_ENDPOINT || vars.TRUSTED_SIGNING_ENDPOINT }}
|
||||
TRUSTED_SIGNING_ACCOUNT: ${{ vars.ARTIFACT_SIGNING_ACCOUNT || vars.TRUSTED_SIGNING_ACCOUNT }}
|
||||
TRUSTED_SIGNING_PROFILE: ${{ vars.ARTIFACT_SIGNING_PROFILE || vars.TRUSTED_SIGNING_PROFILE }}
|
||||
TRUSTED_SIGNING_ENDPOINT: ${{ vars.TRUSTED_SIGNING_ENDPOINT }}
|
||||
TRUSTED_SIGNING_ACCOUNT: ${{ vars.TRUSTED_SIGNING_ACCOUNT }}
|
||||
TRUSTED_SIGNING_PROFILE: ${{ vars.TRUSTED_SIGNING_PROFILE }}
|
||||
run: |
|
||||
@{
|
||||
Endpoint = $env:TRUSTED_SIGNING_ENDPOINT
|
||||
CodeSigningAccountName = $env:TRUSTED_SIGNING_ACCOUNT
|
||||
CertificateProfileName = $env:TRUSTED_SIGNING_PROFILE
|
||||
ExcludeCredentials = @('EnvironmentCredential', 'WorkloadIdentityCredential', 'ManagedIdentityCredential', 'SharedTokenCacheCredential', 'VisualStudioCredential', 'VisualStudioCodeCredential', 'AzurePowerShellCredential', 'AzureDeveloperCliCredential', 'InteractiveBrowserCredential')
|
||||
} | ConvertTo-Json | Set-Content -LiteralPath "$env:RUNNER_TEMP\trusted-signing.json" -Encoding utf8
|
||||
$signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter signtool.exe |
|
||||
Where-Object { $_.FullName -match '\\x64\\' } | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
$dlib = Get-ChildItem "$env:LOCALAPPDATA\ArtifactSigning" -Recurse -Filter Azure.CodeSigning.Dlib.dll |
|
||||
Where-Object { $_.FullName -match '\\x64\\' } | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if (-not $signtool -or -not $dlib) { throw 'Installed x64 SignTool or Artifact Signing dlib was not found.' }
|
||||
foreach ($setting in @{
|
||||
K22_VPK_SIGN_TOOL = $signtool.FullName
|
||||
K22_VPK_SIGN_DLIB = $dlib.FullName
|
||||
K22_VPK_SIGN_METADATA = "$env:RUNNER_TEMP\trusted-signing.json"
|
||||
}.GetEnumerator())
|
||||
{
|
||||
"$($setting.Key)=$($setting.Value)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||
}
|
||||
@'
|
||||
param([Parameter(Mandatory)][string] $File)
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $File
|
||||
if ($signature.Status -eq 'Valid') { exit 0 }
|
||||
if ($signature.Status -ne 'NotSigned') { throw "Refusing to replace an invalid signature on $File." }
|
||||
& $env:K22_VPK_SIGN_TOOL sign /fd SHA256 /tr http://timestamp.acs.microsoft.com /td SHA256 /dlib $env:K22_VPK_SIGN_DLIB /dmdf $env:K22_VPK_SIGN_METADATA $File
|
||||
if ($LASTEXITCODE -ne 0) { throw "Artifact Signing failed on $File." }
|
||||
& $env:K22_VPK_SIGN_TOOL verify /pa /all /v $File
|
||||
if ($LASTEXITCODE -ne 0) { throw "Signature verification failed on $File." }
|
||||
'@ | Set-Content -LiteralPath "$env:RUNNER_TEMP\sign-one.ps1" -Encoding utf8NoBOM
|
||||
if (-not $signtool -or -not $dlib) { throw "signtool.exe ($signtool) or Azure.CodeSigning.Dlib.dll ($dlib) not found." }
|
||||
@"
|
||||
param([Parameter(Mandatory)][string] `$File)
|
||||
`$ErrorActionPreference = 'Stop'
|
||||
if ((Get-AuthenticodeSignature -LiteralPath `$File).Status -eq 'Valid') { exit 0 }
|
||||
& '$($signtool.FullName)' sign /fd SHA256 /tr http://timestamp.acs.microsoft.com /td SHA256 /dlib '$($dlib.FullName)' /dmdf '$env:RUNNER_TEMP\trusted-signing.json' `$File | Out-Null
|
||||
if (`$LASTEXITCODE -ne 0) { throw "signtool failed (`$LASTEXITCODE) on `$File" }
|
||||
"@ | Set-Content -LiteralPath "$env:RUNNER_TEMP\sign-one.ps1" -Encoding utf8
|
||||
|
||||
# The vpk version matches the Velopack NuGet package the apps reference (AppKit).
|
||||
- name: Install vpk
|
||||
@@ -498,22 +434,32 @@ jobs:
|
||||
|
||||
- name: Velopack pack
|
||||
shell: pwsh
|
||||
env:
|
||||
VPK_MSI: ${{ inputs.msi }}
|
||||
run: |
|
||||
$sign = @()
|
||||
if ($env:K22_SIGN -eq 'true')
|
||||
{
|
||||
# App/CLI binaries are already signed. Only Velopack's branded helpers
|
||||
# may be signed inside pack; preserve every other dependency signature.
|
||||
$exclude = '(?i)[\\/](?!(?:Squirrel\.exe|.*_ExecutionStub\.exe)$)[^\\/]+$'
|
||||
$command = "pwsh -NoProfile -NonInteractive -File `"$env:RUNNER_TEMP\sign-one.ps1`" `"{{file}}`""
|
||||
$sign = @('--signTemplate', $command, '--signExclude', $exclude)
|
||||
}
|
||||
$env:VELOPACK_TEMP = Join-Path $env:RUNNER_TEMP 'k22-velopack'
|
||||
vpk pack --runtime $env:K22_RUNTIME --packId $env:K22_APP_ID --packVersion $env:K22_VERSION --packDir publish/app --mainExe $env:K22_MAIN_EXE --channel $env:K22_CHANNEL --icon $env:K22_ICON --outputDir Releases --instLocation PerUser @sign
|
||||
if ($env:K22_SIGN -eq 'true') { $sign = @('--signTemplate', "pwsh -NoProfile -NonInteractive -File `"$env:RUNNER_TEMP\sign-one.ps1`" `"{{file}}`"") }
|
||||
vpk pack --packId $env:K22_APP_ID --packVersion $env:K22_VERSION --packDir publish/app --mainExe $env:K22_MAIN_EXE --channel $env:K22_CHANNEL --icon $env:K22_ICON --outputDir Releases @sign
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
# Nothing unsigned leaves this job: Setup.exe and every executable inside the
|
||||
# portable zip (the app, Update.exe, the execution stub) must verify before the
|
||||
# release is even drafted (K22-REL-05).
|
||||
- name: Verify signatures before packaging further
|
||||
if: inputs.sign
|
||||
shell: pwsh
|
||||
run: |
|
||||
$signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter signtool.exe |
|
||||
Where-Object { $_.FullName -match '\\x64\\' } | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
Expand-Archive "Releases/$env:K22_APP_ID-$env:K22_CHANNEL-Portable.zip" -DestinationPath "$env:RUNNER_TEMP\portable" -Force
|
||||
$files = @(Get-Item "Releases/$env:K22_APP_ID-$env:K22_CHANNEL-Setup.exe") + @(Get-ChildItem "$env:RUNNER_TEMP\portable" -Recurse -Include *.exe, *.dll)
|
||||
$failed = @()
|
||||
foreach ($f in $files)
|
||||
{
|
||||
& $signtool.FullName verify /pa /all $f.FullName | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { $failed += $f.FullName }
|
||||
}
|
||||
if ($failed) { Write-Output "::error::Unsigned or invalid after packing:`n$($failed -join "`n")"; exit 1 }
|
||||
Write-Output "All $($files.Count) packaged executables and libraries verify with signtool."
|
||||
|
||||
# Keeps only what this build produced, as `vpk upload` does: the files listed in
|
||||
# assets.<channel>.json, plus a releases.<channel>.json feed trimmed to this build's
|
||||
# packages (the local feed may also list the downloaded baseline).
|
||||
@@ -540,139 +486,11 @@ jobs:
|
||||
Write-Output "Assets: $($names -join ', '); feed: $feedName; setup: $setup"
|
||||
"setup=$setup" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||
|
||||
- name: Extract required MSI payload
|
||||
if: inputs.msi
|
||||
id: msi
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$installers = @(Get-ChildItem -LiteralPath release -Filter '*.msi' -File)
|
||||
if ($installers.Count -ne 1) { throw 'Exactly one MSI installer is required.' }
|
||||
$root = Join-Path $env:RUNNER_TEMP ('k22-msi-' + [guid]::NewGuid().ToString('N'))
|
||||
"root=$root" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||
$image = Join-Path $root 'image'
|
||||
$assets = Join-Path $root 'assets'
|
||||
New-Item -ItemType Directory -Path $image, $assets -Force | Out-Null
|
||||
$start = [Diagnostics.ProcessStartInfo]::new('msiexec.exe')
|
||||
$start.UseShellExecute = $false
|
||||
$start.CreateNoWindow = $true
|
||||
foreach ($argument in @('/a', $installers[0].FullName, '/qn', "TARGETDIR=$image", '/l*v', (Join-Path $root 'extract.log')))
|
||||
{
|
||||
$start.ArgumentList.Add($argument)
|
||||
}
|
||||
$process = [Diagnostics.Process]::Start($start)
|
||||
try
|
||||
{
|
||||
if (-not $process.WaitForExit(180000)) { $process.Kill(); throw 'MSI extraction timed out.' }
|
||||
if ($process.ExitCode -ne 0) { throw "MSI extraction failed: $($process.ExitCode)." }
|
||||
}
|
||||
finally { $process.Dispose() }
|
||||
$manifests = @(Get-ChildItem -LiteralPath $image -Recurse -File -Filter sq.version)
|
||||
if ($manifests.Count -ne 1 -or $manifests[0].Directory.Name -ne 'current') { throw 'Expected one installed MSI application payload.' }
|
||||
$payload = $manifests[0].Directory.Parent.FullName
|
||||
[IO.Compression.ZipFile]::CreateFromDirectory($payload, (Join-Path $assets 'msi-Portable.zip'))
|
||||
"directory=$assets" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
|
||||
|
||||
# Reuse the existing archive verifier for MSI's administratively extracted
|
||||
# payload: every owned/third-party byte and generated launcher is checked.
|
||||
- name: Verify exact signed MSI payload
|
||||
if: inputs.msi && inputs.sign
|
||||
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
|
||||
with:
|
||||
operation: verify-distribution
|
||||
directory: ${{ steps.msi.outputs.directory }}
|
||||
manifest: ${{ steps.signing.outputs.manifest }}
|
||||
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
|
||||
|
||||
- name: Verify MSI installs, launches and uninstalls
|
||||
if: inputs.msi
|
||||
shell: pwsh
|
||||
env:
|
||||
MSI_ROOT: ${{ steps.msi.outputs.root }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$msi = @(Get-ChildItem -LiteralPath release -Filter '*.msi' -File)
|
||||
if ($msi.Count -ne 1) { throw 'Exactly one MSI installer is required.' }
|
||||
$installer = New-Object -ComObject WindowsInstaller.Installer
|
||||
$database = $installer.OpenDatabase($msi[0].FullName, 0)
|
||||
$view = $database.OpenView('SELECT `Value` FROM `Property` WHERE `Property` = ''ProductCode''')
|
||||
$view.Execute()
|
||||
$product = $view.Fetch().StringData(1)
|
||||
if ($product -notmatch '^\{[0-9A-Fa-f-]{36}\}$') { throw 'Invalid MSI product identity.' }
|
||||
$target = Join-Path $env:MSI_ROOT 'installed'
|
||||
$application = $null
|
||||
try
|
||||
{
|
||||
$install = Start-Process msiexec.exe -ArgumentList @('/i', ('"' + $msi[0].FullName + '"'), '/qn', ('VELOPACK_INSTALLDIR="' + $target + '"'), '/l*v', ('"' + (Join-Path $env:MSI_ROOT 'install.log') + '"')) -WindowStyle Hidden -PassThru
|
||||
if (-not $install.WaitForExit(180000)) { $install.Kill(); throw 'MSI installation timed out.' }
|
||||
if ($install.ExitCode -ne 0) { throw "MSI installation failed: $($install.ExitCode)." }
|
||||
$executable = Join-Path $target "current/$env:K22_MAIN_EXE"
|
||||
if (-not (Test-Path -LiteralPath $executable -PathType Leaf)) { throw 'MSI did not install the application.' }
|
||||
$application = Start-Process -FilePath $executable -PassThru
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(30)
|
||||
do
|
||||
{
|
||||
$application.Refresh()
|
||||
if ($application.HasExited) { throw 'MSI-installed application exited before opening its window.' }
|
||||
if ($application.MainWindowHandle -ne [IntPtr]::Zero) { break }
|
||||
Start-Sleep -Milliseconds 200
|
||||
} while ([DateTime]::UtcNow -lt $deadline)
|
||||
if ($application.MainWindowHandle -eq [IntPtr]::Zero) { throw 'MSI-installed application did not open a window.' }
|
||||
if (-not $application.CloseMainWindow() -or -not $application.WaitForExit(10000)) { $application.Kill(); throw 'MSI-installed application did not close normally.' }
|
||||
}
|
||||
finally
|
||||
{
|
||||
if ($null -ne $application -and -not $application.HasExited) { $application.Kill() }
|
||||
$uninstall = Start-Process msiexec.exe -ArgumentList @('/x', $product, '/qn', '/l*v', ('"' + (Join-Path $env:MSI_ROOT 'uninstall.log') + '"')) -WindowStyle Hidden -PassThru
|
||||
if (-not $uninstall.WaitForExit(180000)) { $uninstall.Kill(); throw 'MSI uninstall timed out.' }
|
||||
if ($uninstall.ExitCode -notin 0, 1605) { throw "MSI uninstall failed: $($uninstall.ExitCode)." }
|
||||
}
|
||||
if (Test-Path -LiteralPath (Join-Path $target "current/$env:K22_MAIN_EXE")) { throw 'MSI uninstall left the application installed.' }
|
||||
Write-Output "MSI $product installed, opened its window, closed and uninstalled successfully."
|
||||
|
||||
- name: Remove temporary MSI extraction
|
||||
if: always() && inputs.msi
|
||||
shell: pwsh
|
||||
env:
|
||||
MSI_ROOT: ${{ steps.msi.outputs.root }}
|
||||
run: |
|
||||
if ([string]::IsNullOrWhiteSpace($env:MSI_ROOT)) { exit 0 }
|
||||
$root = [IO.Path]::GetFullPath($env:MSI_ROOT)
|
||||
$temporaryRoot = [IO.Path]::TrimEndingDirectorySeparator([IO.Path]::GetFullPath($env:RUNNER_TEMP))
|
||||
if ([IO.Path]::GetDirectoryName($root) -ne $temporaryRoot -or [IO.Path]::GetFileName($root) -notmatch '^k22-msi-[0-9a-f]{32}$') { throw 'Unexpected MSI scratch directory.' }
|
||||
if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
|
||||
|
||||
- name: Zip CLI
|
||||
if: inputs.cli-project != ''
|
||||
shell: pwsh
|
||||
run: Compress-Archive -Path publish/cli/* -DestinationPath "release/$env:K22_APP_ID-cli-$env:K22_RUNTIME-$env:K22_VERSION.zip"
|
||||
|
||||
- name: Verify final installer, portable, full update and CLI assets
|
||||
if: inputs.sign
|
||||
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
|
||||
with:
|
||||
operation: verify-distribution
|
||||
directory: release
|
||||
manifest: ${{ steps.signing.outputs.manifest }}
|
||||
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
|
||||
|
||||
# Verification precedes upload, SBOM attestations, hashes and provenance.
|
||||
- name: Upload signed app output for its SBOM
|
||||
if: inputs.sign
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: signed-publish-app
|
||||
path: publish/app/
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload signed CLI output for its SBOM
|
||||
if: inputs.sign && inputs.cli-project != ''
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: signed-publish-cli
|
||||
path: publish/cli/
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload release files
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
@@ -696,14 +514,14 @@ jobs:
|
||||
- name: Download published app
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: ${{ inputs.sign && 'signed-publish-app' || 'publish-app' }}
|
||||
name: publish-app
|
||||
path: publish/app
|
||||
|
||||
- name: Download published CLI
|
||||
if: inputs.cli-project != ''
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: ${{ inputs.sign && 'signed-publish-cli' || 'publish-cli' }}
|
||||
name: publish-cli
|
||||
path: publish/cli
|
||||
|
||||
- name: Download release files
|
||||
@@ -756,13 +574,6 @@ jobs:
|
||||
subject-path: release/*-cli-*.zip
|
||||
sbom-path: sbom/${{ inputs.app-id }}-cli-${{ needs.build.outputs.version }}.cdx.json
|
||||
|
||||
- name: Attest MSI SBOM
|
||||
if: inputs.msi
|
||||
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
|
||||
with:
|
||||
subject-path: release/*.msi
|
||||
sbom-path: sbom/${{ inputs.app-id }}-${{ needs.build.outputs.version }}.cdx.json
|
||||
|
||||
- name: Upload SBOMs
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
|
||||
@@ -1 +1 @@
|
||||
{".":"1.2.1"}
|
||||
{".":"1.0.0"}
|
||||
@@ -1,6 +0,0 @@
|
||||
# yamllint configuration for the standards repository: every default rule except
|
||||
# line length, which K22-CODE-08 leaves unlimited (pinned action SHAs and URLs
|
||||
# run long).
|
||||
extends: default
|
||||
rules:
|
||||
line-length: disable
|
||||
@@ -3,66 +3,6 @@
|
||||
<!-- Generated by Release Please and GitHub; permit native spacing, links and repeated categories. -->
|
||||
<!-- markdownlint-disable MD012 MD022 MD024 MD032 MD034 -->
|
||||
|
||||
## 1.2.1 (2026-10-13)
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
||||
|
||||
## What's Changed
|
||||
### Changes
|
||||
* fix(releases): record MSI checklist preflight recovery by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/49
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v1.2.0...v1.2.1
|
||||
|
||||
## 1.2.0 (2026-10-13)
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
||||
|
||||
## What's Changed
|
||||
### Changes
|
||||
* feat(releases): publish signed per-user MSI installers by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/47
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v1.1.2...v1.2.0
|
||||
|
||||
## 1.1.2 (2026-10-13)
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
||||
|
||||
## What's Changed
|
||||
### Changes
|
||||
* fix(releases): scope bootstrap parameters under Actions pwsh by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/45
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v1.1.1...v1.1.2
|
||||
|
||||
## 1.1.1 (2026-10-12)
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
||||
|
||||
## What's Changed
|
||||
### Changes
|
||||
* docs(inventory): register MarkdownViewr and the Windows starter by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/36
|
||||
* fix(releases): bootstrap verified Artifact Signing dependency by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/44
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v1.1.0...v1.1.1
|
||||
|
||||
## 1.1.0 (2026-10-12)
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
||||
|
||||
## What's Changed
|
||||
### Changes
|
||||
* docs(exceptions): close EX-0002 for gclo by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/33
|
||||
* docs(exceptions): add EX-0009 to EX-0012 from the repository audit by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/37
|
||||
* feat(releases): verify Windows Artifact Signing before publication by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/31
|
||||
* fix(signing): pin reviewed helpers reachable from main by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/39
|
||||
* fix(ci): isolate racing signing analyzer rules by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/41
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v1.0.0...v1.1.0
|
||||
|
||||
## 1.0.0 (2026-10-10)
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
||||
|
||||
@@ -24,8 +24,6 @@ conformance version in its README once it passes the conformance checker.
|
||||
| --- | --- | --- | --- |
|
||||
| [standards](https://github.com/KofTwentyTwo/standards) | These policies, standards, configs, and reusable workflows | Product | Not yet: rulesets and CI being added |
|
||||
| [AppKit](https://github.com/KofTwentyTwo/AppKit) | Shared foundation packages for Windows desktop apps | Product | Adoption in progress |
|
||||
| [MarkdownViewr.Windows](https://github.com/KofTwentyTwo/MarkdownViewr.Windows) | Windows Markdown reader based on Darin Kelkhoff's macOS markdownViewr and AppKit | Product | Adoption in progress: release acceptance, signing and maintainer review pending |
|
||||
| [windows-app-template](https://github.com/KofTwentyTwo/windows-app-template) | Reusable AppKit Windows app starter with optional CLI and stable/dev release workflows | Product | Adoption in progress: shared defaults and release controls in review |
|
||||
| [gclo](https://github.com/KofTwentyTwo/gclo) | Clone and update every repository of a GitHub organization (Windows app and CLI) | Product | Adoption in progress: standards gates, governance files, and the Kingsrook layout are in review (gclo #62); settings and exceptions pending |
|
||||
|
||||
### Awaiting classification
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
# Windows release signing
|
||||
|
||||
The reusable NuGet and Velopack builders support Azure Artifact Signing over GitHub
|
||||
OIDC. Reuse an existing validated signing account and active PublicTrust profile;
|
||||
retrieve the actual tenant GUID, regional account URI, profile name and certificate
|
||||
subject from Azure before setting variables. Account/domain names and subscription
|
||||
IDs are not tenant GUIDs. Do not export a private key or use an administrator's
|
||||
password in CI.
|
||||
|
||||
## Trust and configuration
|
||||
|
||||
Configure a reviewed Entra application/service principal per repository, or reuse a
|
||||
suitable existing CI identity while preserving other repositories' credentials.
|
||||
Use the issuer `https://token.actions.githubusercontent.com`, audience
|
||||
`api://AzureADTokenExchange` and subject `repo:OWNER/REPOSITORY:environment:release`.
|
||||
Protect the `release` environment against untrusted refs. Assign the built-in
|
||||
**Artifact Signing Certificate Profile Signer** at the existing certificate profile
|
||||
scope; Azure Owner/Contributor is not a signing permission.
|
||||
|
||||
Set these environment variables using verified deployed values:
|
||||
|
||||
| Variable | Value |
|
||||
| --- | --- |
|
||||
| `AZURE_TENANT_ID` | Actual tenant GUID |
|
||||
| `AZURE_CLIENT_ID` | CI application client ID |
|
||||
| `AZURE_SUBSCRIPTION_ID` | Signing subscription ID |
|
||||
| `ARTIFACT_SIGNING_ENDPOINT` | Account's regional HTTPS URI |
|
||||
| `ARTIFACT_SIGNING_ACCOUNT` | Existing account resource name |
|
||||
| `ARTIFACT_SIGNING_PROFILE` | Existing active profile name |
|
||||
| `ARTIFACT_SIGNING_SUBJECT` | Profile certificate's verified subject |
|
||||
|
||||
Callers can retain legacy `TRUSTED_SIGNING_ENDPOINT`, `_ACCOUNT`, `_PROFILE` and
|
||||
`_SUBJECT` names; the new names take precedence. The verified subject is required.
|
||||
Both builders use official full-SHA-pinned `azure/login` v3 and
|
||||
`azure/artifact-signing-action` v2, Windows runners and `AzureCliCredential` only.
|
||||
Signing jobs run in `release` with `contents: read` and `id-token: write`. No
|
||||
pull-request signing path exists. SHA256 file digests and RFC3161 SHA256 timestamps
|
||||
at `http://timestamp.acs.microsoft.com` are mandatory when signing is enabled.
|
||||
|
||||
## Caller inputs and artifact order
|
||||
|
||||
Set `sign: true` and a newline-separated `signing-files` list. Every pattern must
|
||||
match; missing binaries fail rather than quietly omitting a signature.
|
||||
Owned and preserved `.exe`/`.dll` headers must parse before a catalog is written;
|
||||
hash preservation cannot prove an already corrupt source is executable. MSI/MSIX
|
||||
use their package/signature validation paths rather than PE parsing. For NuGet,
|
||||
use exact checkout-relative built DLL paths. For Velopack, use publish-relative
|
||||
paths such as `app/MyApp.exe`, `app/MyApp.dll`, `app/MyApp.Core.dll` and
|
||||
`cli/myapp.exe`. Include all owned modules. Exclude third-party dependencies.
|
||||
|
||||
NuGet signs before `pack --no-build`, then checks the DLLs inside the final packages
|
||||
against their signed originals. Velopack signs owned published binaries, preserves
|
||||
other binary hashes, and uses vpk's signing hook only for its branded Squirrel and
|
||||
execution stubs plus the final Setup/MSI. Final checks validate trust, expected
|
||||
publisher, timestamps and SHA256 for the shipped binaries, require unchanged
|
||||
dependency bytes, and verify Setup's embedded full package matches the verified
|
||||
full update package. Culture-directory satellite assemblies retain their separate
|
||||
paths. Verification precedes upload, SBOM attestations, checksums and provenance.
|
||||
|
||||
Signing or verification failure stops publication when `sign` is true. The default
|
||||
remains false for compatibility; each owner must enable required signing in their
|
||||
caller or block publication during migration. NuGet/ZIP/delta containers are not
|
||||
Authenticode targets. Test reconstructed delta updates on installed channel builds
|
||||
before claiming upgrade coverage. MSIX presets must use the verified subject as
|
||||
their manifest Publisher.
|
||||
|
||||
## Local tools and proof
|
||||
|
||||
Use Microsoft's [current integration instructions](https://learn.microsoft.com/azure/artifact-signing/how-to-signing-integrations)
|
||||
for Artifact Signing Client Tools, matching-architecture SignTool and
|
||||
`Azure.CodeSigning.Dlib.dll`, .NET 8 and the Visual C++ runtime. Metadata fields are
|
||||
`Endpoint`, `CodeSigningAccountName` and `CertificateProfileName`. Authenticate with
|
||||
an identity that has the profile signer role and sign with `/fd SHA256`,
|
||||
`/tr http://timestamp.acs.microsoft.com`, `/td SHA256`, `/dlib` and `/dmdf`.
|
||||
|
||||
Run `signtool verify /pa /all /v` on final installers and extracted shipped binaries.
|
||||
Record publisher, trust, timestamp, exact artifact hashes and the CI run. An active
|
||||
profile, a mocked test or a signed Microsoft tool does not prove an application's
|
||||
signing works. Retain any signing exception until real end-to-end evidence exists.
|
||||
|
||||
## Prepared rollout
|
||||
|
||||
This implementation is prepared against published standards v1.0.0. Preserve that
|
||||
release's metadata, policy changes and signing evidence. Its released Velopack
|
||||
path lacks the explicit owned-file catalog and its NuGet path lacks these signing
|
||||
inputs, so it is not sufficient for an AppKit caller that requires them.
|
||||
|
||||
The prepared workflows reference helper snapshot `4a7f028976163cb80730126eda1b029005803325`,
|
||||
which includes source PE parsing and final missing-dependency checks. These references remain
|
||||
unreleased placeholders until the reviewed helper commit is available on GitHub
|
||||
and the completed builder is published with its proper version comment. Verify
|
||||
that exact helper revision, not an older catalog implementation. Keep consumer
|
||||
publication blocked until the reviewed builder is published, pinned and proven
|
||||
with representative signed artifacts. Local tests do not authorize rollout.
|
||||
@@ -1,18 +0,0 @@
|
||||
# MSI builder release recovery
|
||||
|
||||
The signed v1.2.0 tag is reserved by a preflight failure: its annotation omitted
|
||||
the required MINOR-release checklist. No1.2.0 assets were published. Preserve
|
||||
that tag and recover through a new1.2.1 release proposal and signed tag.
|
||||
|
||||
The optional MSI implementation is unchanged from merged PR47. Hosted checks
|
||||
and40 signing regressions pass; native MSI compilation and the actual wrapped
|
||||
extraction body preserve all6 signed fixture application binaries. Production
|
||||
MSI signing, installation and distributed-payload verification remain mandatory.
|
||||
|
||||
Before tagging the recovery, complete and link the release checklist, retain full
|
||||
conformance/scanning evidence and verify main/release-proposal gates. Include the
|
||||
checklist URL in the signed annotation even for this PATCH recovery. Publish only
|
||||
after its protected deployment and verify all actual bundle hashes/provenance.
|
||||
|
||||
This preserves existing EXE callers, signing identity/profile/trust, branch/tag
|
||||
protections and public1.1.2 assets. No account onboarding is repeated.
|
||||
@@ -71,18 +71,6 @@ trust boundaries change.
|
||||
|
||||
## Review log
|
||||
|
||||
- **Prepared AppKit signing extension, 2026-10-10:** Explicit owned-file selection
|
||||
excludes third-party binaries from re-signing, retains their hashes and requires
|
||||
their presence in final archives. Catalog preparation parses owned/preserved
|
||||
`.exe`/`.dll` headers before writes; already corrupt dependencies cannot pass
|
||||
solely because hashes are unchanged. Final owned binaries and installers require
|
||||
publisher, trust, SHA256 and timestamp checks before upload or publication. The
|
||||
SignTool packaging wrapper preserves valid signatures and refuses invalid ones.
|
||||
NuGet signs before packing and checks embedded DLL bytes. Existing protected
|
||||
release-environment OIDC boundaries remain; real AppKit identity/profile access
|
||||
and signatures are still unproved. The helper reference and consumer rollout
|
||||
remain pending review/publication; this entry is not deployment evidence.
|
||||
|
||||
- **2026-10-10, v1.0.0:** Reviewed changes since v0.1.2: mandatory applicable badge
|
||||
participation and parser fuzzing, completed passing self-assessment, professional
|
||||
release text, scoped frontend audit exception, and unsigned-file-only Windows
|
||||
|
||||
+24
-82
@@ -17,9 +17,6 @@ Open and closed exceptions to the KofTwentyTwo standards. The process is in
|
||||
| [EX-0010](#ex-0010) | Open | K22-CI-01, K22-CI-10 to K22-CI-24, K22-CI-30 (GitHub Actions gates and reusable workflows) | Website-Backend, Website-Frontend, Munitor | 2027-04-12; review 2027-01-12 |
|
||||
| [EX-0011](#ex-0011) | Open | K22-CODE-TS-01, K22-CODE-SH-02 (Kingsrook 3-space layout) | Website-Frontend (TypeScript), Munitor (shell) | 2027-04-12; review 2027-01-12 |
|
||||
| [EX-0012](#ex-0012) | Open | K22-CODE-TF-04 (state never committed), K22-CODE-TF-05 (no secrets in `.tfvars`) | Praetor | Reviewed yearly, next on 2027-10-12 |
|
||||
| [EX-0013](#ex-0013) | Open | K22-DEP-02 automated license classification | QRun-IO/qbit-printing, unmodified H2 2.4.240 only | 2027-04-30 or any H2 version/license change, whichever occurs first |
|
||||
| [EX-0014](#ex-0014) | Proposed, awaiting approval | K22-DEP-02 (Microsoft SDK terms and classification) | QRun-IO/qqq-print-agent, exact 15-package Windows SDK inventory below | 2027-04-30 or any reviewed version/license/scope change, whichever occurs first |
|
||||
| [EX-0015](#ex-0015) | Proposed, awaiting approval | K22-DEP-02 (build-tool license classification) | QRun-IO/qqq-print-protocol and QRun-IO/qqq-print-agent, development-only `uri-js` 4.4.1 only | 2027-04-30 or any `uri-js` version/license/scope change, whichever occurs first |
|
||||
|
||||
## EX-0001
|
||||
|
||||
@@ -349,84 +346,29 @@ Open and closed exceptions to the KofTwentyTwo standards. The process is in
|
||||
|
||||
## EX-0013
|
||||
|
||||
**H2 2.4.240 license metadata classification in QQQ Printing**
|
||||
**Proposed: AppKit ESLint's transitive BSD views license**
|
||||
|
||||
- **Requirements:** `K22-DEP-02` permits MPL-2.0 when used unmodified. This is an exception to the automated dependency-review classification for one already-permitted dependency, not permission to introduce an incompatible license or skip vulnerability review. The pinned `pr.yml` input requires this register entry for a package-specific license exception.
|
||||
- **Scope:** `QRun-IO/qbit-printing` only; Maven package `com.h2database:h2`, unmodified version **2.4.240**, runtime JAR SHA-256 `29b70e427cc1c40cdc376283adbb0cc62853073797bb5fe5761f81fe73d57ce0`. No other package, version, repository or modified H2 binary is covered.
|
||||
- **Reason:** GitHub dependency review reports `LicenseRef-scancode-unknown AND MPL-2.0`. The tagged upstream license expressly offers MPL-2.0 **or** EPL-1.0; this runtime uses the MPL-2.0 option. Published 2.3.232 and 2.4.240 POMs declare the same terms. Upgrading to 2.4.240 fixes confirmed close-time compaction data loss; reverting to the older driver is not an acceptable metadata workaround.
|
||||
- **Risk:** The scanner cannot accurately classify this artifact's license. A package-level exclusion could otherwise hide a later license change because the pinned action ignores PURL versions. The reviewed binary remains unmodified and retains its license/attribution; this record does not waive the applicable redistribution terms.
|
||||
- **Compensating controls:** The caller passes only `pkg:maven/com.h2database/h2` to `allow-dependencies-licenses`; every other package retains its existing license checks. The required `security / sca` Maven inventory guard rejects missing H2, any version other than 2.4.240, and multiple H2 versions, then verifies the complete selected runtime graph against scanner inventories. The runtime payload verifies every copied JAR's SHA-256 and contains exactly one H2 driver. The POM pins the reviewed version. `license-check: true`, `vulnerability-check: true`, low-or-higher dependency advisory review, Trivy severity gates, OSV malicious-package checks and all existing required checks remain enabled. H2 attribution and corresponding source access are documented in NOTICE and the repository license review. Any H2 upgrade requires renewed license review and guard/record updates before release.
|
||||
- **Expires:** 2027-04-30, or an H2 version/license change or correction of GitHub's metadata classification, whichever occurs first. Remove or renew through the normal standards PR process; do not broaden the exclusion silently.
|
||||
- **Owner:** the maintainer of `QRun-IO/qbit-printing`.
|
||||
- **Review date:** 2026-10-30.
|
||||
- **Evidence:** [H2 tagged license](https://github.com/h2database/h2database/blob/version-2.4.240/LICENSE.txt), [tagged license FAQ](https://github.com/h2database/h2database/blob/version-2.4.240/h2/src/docsrc/html/license.html), [published 2.4.240 POM](https://repo.maven.apache.org/maven2/com/h2database/h2/2.4.240/h2-2.4.240.pom), [published 2.3.232 POM](https://repo.maven.apache.org/maven2/com/h2database/h2/2.3.232/h2-2.3.232.pom), [failed dependency-review run](https://github.com/QRun-IO/qbit-printing/actions/runs/41160240342), [upstream compaction fix](https://github.com/h2database/h2database/pull/4249), [pinned workflow input](https://github.com/KofTwentyTwo/standards/blob/fdf5ed029603c938615a32b1beca3628b8787945/.github/workflows/pr.yml), [pinned version-ignoring matcher](https://github.com/actions/dependency-review-action/blob/a1d282b36b6f3519aa1f3fc636f609c47dddb294/src/purl.ts).
|
||||
- **Status:** Proposed; not effective until this register change is reviewed and merged.
|
||||
- **Requirement:** `K22-DEP-02`, enforced by the reusable PR license allowlist.
|
||||
- **Scope:** `KofTwentyTwo/AppKit` repository tooling only, `pkg:npm/uri-js@4.4.1`
|
||||
from ESLint's Ajv dependency. It is development-only and ships in no AppKit package.
|
||||
- **Reason:** hosted dependency review reports `BSD-2-Clause AND BSD-2-Clause-Views`,
|
||||
while npm metadata says `BSD-2-Clause`. The installed 4.4.1 LICENSE matches the
|
||||
[upstream text](https://github.com/garycourt/uri-js/blob/master/LICENSE), including
|
||||
the views sentence. [SPDX](https://spdx.org/licenses/BSD-2-Clause-Views.html)
|
||||
describes this as BSD-2-Clause with the added views-and-conclusions sentence.
|
||||
Keeping ESLint preserves required checks of repository JavaScript and review code.
|
||||
- **Risk:** the default automated license policy cannot accept this expression;
|
||||
a manual decision could miss a license obligation. Source copyright, conditions,
|
||||
disclaimer and views text must be preserved when redistributing this tool.
|
||||
- **Compensating controls:** retain the locked dependency and full license; after
|
||||
approval, exempt only that exact package version from license review. Every other
|
||||
license, all development/runtime vulnerability checks, package signature verification
|
||||
and SCA remain enforced. No shipped dependency gains an exemption.
|
||||
- **Expires:** 2027-04-10, or when this dependency is removed or upgraded, whichever
|
||||
comes first. Reassess the actual replacement version before extending the scope.
|
||||
- **Owner:** KofTwentyTwo maintainer. **Review date:** 2027-01-10.
|
||||
- **Evidence:** [AppKit #26](https://github.com/KofTwentyTwo/AppKit/issues/26) and
|
||||
[failed dependency gate](https://github.com/KofTwentyTwo/AppKit/actions/runs/38093180805).
|
||||
|
||||
## EX-0014
|
||||
|
||||
**Exact Microsoft Windows SDK dependencies in QQQ Print Agent**
|
||||
|
||||
- **Status:** Open upon merge of this record through the normal protected PR process. The maintainer accepted the complete 15-package scope and its Microsoft custom terms on 2026-11-01; the earlier two-package proposal is superseded.
|
||||
- **Requirements:** `K22-DEP-02`, for the Microsoft terms outside the standard license allowlist and their automated classification. This is not merely a metadata correction for an already-permitted SPDX license. No vulnerability, provenance, signing, attestation or branch-protection requirement is waived.
|
||||
- **Scope:** `QRun-IO/qqq-print-agent` only, unmodified Windows x64 use of the exact package/version/raw-NUPKG-SHA-256 tuples below, including the source-linked native test host. BuildTools and MSIX tooling remain build-only. The implicit SDK reference pack also supplies reviewed runtime projections. No other repository, package, version, modified binary, standalone SDK offering, optional execution provider/model or new AI feature is covered. The finite table is authoritative; there is no Microsoft package-family wildcard.
|
||||
- **Reason:** The existing WinUI application and its native regression host require this established SDK dependency set. Evaluated NuGet dependency submission now exposes the actual versions and non-SPDX Microsoft license terms. Removing declarations or returning to incomplete `>=0` metadata would hide dependencies rather than resolve the policy issue. AppKit's MIT dependencies retain ordinary license review and are not exempted.
|
||||
- **Risk:** Microsoft's usage, redistribution, data/privacy/export and third-party obligations remain applicable. A package-name license exception could hide later version/license changes because the pinned action ignores PURL version constraints. Approval does not relicense Microsoft code as MIT or certify that every redistribution obligation has been satisfied.
|
||||
- **Compensating controls:** The caller may list only the 15 exact package names below as `pkg:nuget/<name>` in `allow-dependencies-licenses`, after this record is approved. The required `Test-WindowsSdkInventory.ps1` guard checks exact versions, committed NuGet content hashes, raw package SHA-256, license/notice entry hashes and the corresponding explicit manifest. Raw signed-archive SHA-512 and NuGet lock `contentHash` are distinct and are checked separately. Fourteen packages are checked in app/test-host locks; `Microsoft.Windows.SDK.NET.Ref` is checked through evaluated SDK assets/download dependencies, not a fabricated lock entry. Preserve the five distinct license documents and applicable third-party notices. Verify BuildTools/MSIX nonlegal files are excluded and only expected runtime/projection output is distributed. Maintain explicit third-party terms and distributor/end-user protection obligations; generic MIT attribution alone is not sufficient. `license-check: true`, `vulnerability-check: true`, low-or-higher advisory failure, SCA/malware checks, locked restores, signed/DCO commits, required PR gates and release attestations remain enabled. Any reviewed version/license/payload-scope change requires renewed approval before the exclusion can be used.
|
||||
- **Expires:** 2027-04-30, or any reviewed package version/license/scope change, whichever occurs first. A classifier improvement alone does not make proprietary terms permissively licensed; remove or renew the record through the normal standards PR process.
|
||||
- **Owner:** the maintainer of `QRun-IO/qqq-print-agent`.
|
||||
- **Review date:** 2026-11-01.
|
||||
|
||||
### Exact package scope
|
||||
|
||||
| NuGet package | Version | Raw NUPKG SHA-256 |
|
||||
| --- | --- | --- |
|
||||
| [Microsoft.Windows.AI.MachineLearning](https://www.nuget.org/packages/Microsoft.Windows.AI.MachineLearning/2.1.74) | 2.1.74 | `691165FA3C07A04B752CBF4A07E93ED13A418E9DEA1EE89EB163D2225E2BA3AF` |
|
||||
| [Microsoft.Windows.SDK.BuildTools](https://www.nuget.org/packages/Microsoft.Windows.SDK.BuildTools/10.0.28000.2705) | 10.0.28000.2705 | `8BFDFB6CA2633F531CF80B5FA22512BA61A394D7988F0970DB83BAADC67929ED` |
|
||||
| [Microsoft.Windows.SDK.BuildTools.MSIX](https://www.nuget.org/packages/Microsoft.Windows.SDK.BuildTools.MSIX/1.7.251221100) | 1.7.251221100 | `E61C560805029F5AE15433BE52E153F3862B6FAAF725292638A9ECE592F7A060` |
|
||||
| [Microsoft.Windows.SDK.NET.Ref](https://www.nuget.org/packages/Microsoft.Windows.SDK.NET.Ref/10.0.19041.57) | 10.0.19041.57 | `90B18F2CF7D7A23F5B0BFE2B86D8DDA45881481F677CE36ECCAF6E9AB50B6B4B` |
|
||||
| [Microsoft.WindowsAppSDK](https://www.nuget.org/packages/Microsoft.WindowsAppSDK/2.5.1) | 2.5.1 | `2E26DE1253E2E2A288A75387523E2F8B402C05F117D42013DF9875887A70933A` |
|
||||
| [Microsoft.WindowsAppSDK.AI](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.AI/2.5.5) | 2.5.5 | `108143AD35B20C8DF7A885995CE4258E7C2E6BA74C9B60D2D819CBC46980B6FB` |
|
||||
| [Microsoft.WindowsAppSDK.Base](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.Base/2.0.4) | 2.0.4 | `E3E13478C4C80C59ED5F8F89542FE49A2985DAA484753E93A5858E90C2D46A4D` |
|
||||
| [Microsoft.WindowsAppSDK.DWrite](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.DWrite/2.1.0) | 2.1.0 | `91746ABD6A780B84001F9C465507830D5A24C79912BC4AA0D35938C71C1C6B5F` |
|
||||
| [Microsoft.WindowsAppSDK.Foundation](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.Foundation/2.3.12) | 2.3.12 | `CE04D01D68CB16B5DC85A5C9EFB8162DBA876BC2E5B766B5968979899D4EFA9E` |
|
||||
| [Microsoft.WindowsAppSDK.InteractiveExperiences](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.InteractiveExperiences/2.1.9) | 2.1.9 | `F9159D73E3E46717F159606E43B4ACF8336AAFF5BDD4D395FE3A93FDB41437AC` |
|
||||
| [Microsoft.WindowsAppSDK.ML](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.ML/2.1.94) | 2.1.94 | `549E92CD397B9D90EA9F08334DB6C79DFA78FC49B47BD3D26A53EB1DF2F21191` |
|
||||
| [Microsoft.WindowsAppSDK.Runtime](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.Runtime/2.5.1) | 2.5.1 | `2FBD27012291BE50CAFED6942306964281D37167AF6A6AF3887E1163EC5CF063` |
|
||||
| [Microsoft.WindowsAppSDK.Search](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.Search/2.5.5) | 2.5.5 | `B5876FE0091A6A14C34E46B5BC898621E80B5ED598F45574B90DEB84BF33F023` |
|
||||
| [Microsoft.WindowsAppSDK.Widgets](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.Widgets/2.0.5) | 2.0.5 | `7F1B585EF8F012784D2CB91B86E809814CECCAB74183A5979EF45DB141779CB5` |
|
||||
| [Microsoft.WindowsAppSDK.WinUI](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.WinUI/2.3.9) | 2.3.9 | `D36145BD6C0F46FC04E07FB4AF365D093FB42C39B625948D0EAD07A0B26FCE96` |
|
||||
|
||||
The 14 ordinary packages have identical versions and NuGet content hashes in the production app and test host. `Microsoft.Windows.SDK.NET.Ref` is an implicit SDK download dependency; its reviewed `lib/net8.0/Microsoft.Windows.SDK.NET.dll` and `lib/net8.0/WinRT.Runtime.dll` are legitimate runtime projections, not build tools. Packaging verification found these bytes unchanged in the app, CLI and service; the package's `Microsoft.Windows.UI.Xaml.dll` was absent. The package manifest and payload guard retain these distinctions rather than excluding every reference-pack file indiscriminately.
|
||||
|
||||
### License evidence and conditions
|
||||
|
||||
| Exact document | Packages | License byte SHA-256 |
|
||||
| --- | --- | --- |
|
||||
| [Windows App SDK license.txt](https://www.nuget.org/packages/Microsoft.WindowsAppSDK/2.5.1/License) | WindowsAppSDK, AI, Base, DWrite, Foundation, InteractiveExperiences, Runtime, Search, Widgets, WinUI | `5B11E6347756E40FE0274BC08C97F89201B94F0D50181A09A00F1F4740840501` |
|
||||
| [Windows App SDK and Windows ML license.txt](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.ML/2.1.94/License) | WindowsAppSDK.ML 2.1.94 | `656AAB74C15AA9F9964BCDCC993EB2755CBDB4822D5E0E3BC61D2E281897F758` |
|
||||
| [Windows ML Runtime license.txt](https://www.nuget.org/packages/Microsoft.Windows.AI.MachineLearning/2.1.74/License) | Windows.AI.MachineLearning 2.1.74 | `66395F8CB219087FAE2BD025010BD9076B736C14F03B48F20295471C0C376814` |
|
||||
| [Packaged SDK sdk_license.txt](https://www.nuget.org/packages/Microsoft.Windows.SDK.BuildTools.MSIX/1.7.251221100/License) | SDK.BuildTools.MSIX 1.7.251221100 | `A7A5C7E7FF998558983D6ACA2702117C328AEB0C6404D298CB275F5623C6FD13` |
|
||||
| [Official SDK license RTF](https://download.microsoft.com/download/0/F/F/0FF2B061-47DD-4F55-89B6-FD1D8C44F14D/sdk_license.rtf) | SDK.BuildTools and SDK.NET.Ref, whose nuspecs declare [WinSDKLicenseURL](https://aka.ms/WinSDKLicenseURL) | `DD07EB178E00C6BBA4148457FC00FF77CD4887EB521D504186FE59C9EC8BBE62` |
|
||||
|
||||
The 13 file-declared licenses were read from the exact package archives and their byte hashes checked. The two URL declarations resolved on 2026-10-31 to the official RTF above, labeled Windows 10 SDK / `WIN10SDK.RTM.AUG_2018_en-US`; that is the document supplied by these packages, not a guessed newer license. No full vendor-license copies are placed in this standards repository.
|
||||
|
||||
App SDK section 1 permits Windows development/testing; section 3 permits distribution of files placed with the application by the SDK, including self-contained deployment. Its requirements include significant application functionality, protective distributor/end-user terms, the stated indemnity obligations and unmodified-code exception, notices and restrictions on endorsement/standalone offerings. Both ML documents additionally retain their section 1(d) third-party-material/execution-provider terms. SDK sections 1(a)/1(c) cover development and build-server use; section 2 separately restricts redistributable/setup/utility code. These obligations and applicable third-party notices remain in force. The maintainer must fulfill the applicable redistribution/end-user protection requirements; this record is not a legal opinion or a waiver of vendor terms.
|
||||
|
||||
The paired guard passed 25 rejection tests and checked the actual package cache, 11 locks and 11 evaluated assets. An unexecuted local MSI (`F08FF820CB4F7EA3877554E27208441942C6AA4D8749AD5A0D852C21A14D814C`) was built and statically extracted: all 13 legal/manifest paths matched expected bytes, the two runtime projections matched their package bytes, and BuildTools/MSIX nonlegal payloads and other reference-pack bytes were excluded. This is technical packaging evidence, not a certification that every redistribution obligation is fulfilled or authorization to install/publish that local MSI.
|
||||
|
||||
Additional evidence: [resolved dependency-review failure](https://github.com/QRun-IO/qqq-print-agent/actions/runs/41259093183), [application change under review](https://github.com/QRun-IO/qqq-print-agent/pull/10), [pinned reusable workflow](https://github.com/KofTwentyTwo/standards/blob/fdf5ed029603c938615a32b1beca3628b8787945/.github/workflows/pr.yml), [version-ignoring package matcher](https://github.com/actions/dependency-review-action/blob/a1d282b36b6f3519aa1f3fc636f609c47dddb294/src/purl.ts). Publication and approval links must be added only when they exist.
|
||||
|
||||
## EX-0015
|
||||
|
||||
**uri-js 4.4.1 license classification in the QQQ Print build tooling**
|
||||
|
||||
- **Status:** Open upon merge of this record through the normal protected PR process.
|
||||
- **Requirements:** `K22-DEP-02`. Build-only tools may use any OSI-approved license. GitHub dependency review classifies the npm package `uri-js` 4.4.1 as `BSD-2-Clause AND BSD-2-Clause-Views`, and SPDX lists `BSD-2-Clause-Views` as not separately OSI-approved. This is a classification exception for one build-only tool dependency, not permission to ship it and not permission to admit any other non-allowlisted license. No vulnerability, malware, provenance, signing or branch-protection requirement is waived.
|
||||
- **Scope:** `QRun-IO/qqq-print-protocol` and `QRun-IO/qqq-print-agent` only; npm package `uri-js`, unmodified version **4.4.1**, lock integrity `sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==`, installed `LICENSE` SHA-256 `0AF366EFF4C01EC147C9C61EA9E8FFAD64A4294754C9D79355F3FD1B97CB2FB9`. It is a locked, `dev: true`, transitive dependency of the pinned ESLint 10.14.0 (through ajv 6), never a direct dependency and never a runtime dependency. `node_modules` is restored only in development and CI; the protocol release bundle names repository paths only and the agent installer is built from .NET projects. No other package, version, repository or modified copy is covered.
|
||||
- **Reason:** the pinned ESLint requires ajv 6, which requires `uri-js`; ESLint is required by the TypeScript standard for these repositories' owned tooling. The installed `LICENSE` (Copyright 2011 Gary Court) is the BSD-2-Clause conditions and disclaimer plus one sentence stating that the software's views and conclusions are the authors' and are not official policies; SPDX describes this as identical to BSD-2-Clause plus that sentence. It adds no restriction on use, copying, modification or redistribution. Replacing the resolver with a non-compatible library, or removing the package from the lock without an upstream ESLint change, would change reviewed tool behavior rather than resolve a policy problem.
|
||||
- **Risk:** the scanner and SPDX classification are not on the permissive allowlist; the package is old (OpenSSF Scorecard 2.4, below the threshold of 3, already reported as a warning). A package-name exclusion could hide a later version or license change because the pinned action ignores PURL versions. Dependency review reports no vulnerability for 4.4.1.
|
||||
- **Compensating controls:** each caller passes only `pkg:npm/uri-js` to `allow-dependencies-licenses`; every other package keeps its license check. `license-check: true`, `vulnerability-check: true`, low-or-higher advisory failure, development/runtime/unknown scopes, SCA/malware, Scorecard reporting and all required checks remain enabled. The required script-quality job in each repository fails unless the lock contains exactly one `uri-js`, version 4.4.1 with the integrity above, `dev: true`, not a direct dependency, required only by development packages; the installed `LICENSE` bytes hash to the value above; and the caller lists exactly `pkg:npm/uri-js` and no other npm package URL. The guard has rejection tests for another version, integrity, scope, consumer, duplicate copy, changed license bytes and a broadened caller list. Any version, license or scope change requires renewed approval before the exclusion can be used.
|
||||
- **Expires:** 2027-04-30, or any `uri-js` version, license or scope change, or an ESLint release that no longer requires it, whichever occurs first. Remove or renew through the normal standards PR process; do not broaden the exclusion silently.
|
||||
- **Owner:** the maintainer of `QRun-IO/qqq-print-protocol` and `QRun-IO/qqq-print-agent`.
|
||||
- **Review date:** 2027-01-31.
|
||||
|
||||
Evidence: failed `pr / dependency-review` runs on [qqq-print-protocol#16](https://github.com/QRun-IO/qqq-print-protocol/pull/16) and [qqq-print-agent#18](https://github.com/QRun-IO/qqq-print-agent/pull/18) (only this license finding remained), [SPDX BSD-2-Clause-Views](https://spdx.org/licenses/BSD-2-Clause-Views.html), [pinned reusable workflow](https://github.com/KofTwentyTwo/standards/blob/fdf5ed029603c938615a32b1beca3628b8787945/.github/workflows/pr.yml), [version-ignoring package matcher](https://github.com/actions/dependency-review-action/blob/a1d282b36b6f3519aa1f3fc636f609c47dddb294/src/purl.ts).
|
||||
Do not activate a caller exemption while this entry remains proposed.
|
||||
@@ -35,10 +35,6 @@ jobs:
|
||||
coverage-packages: "{{COVERAGE_PACKAGES}}"
|
||||
test-projects: |
|
||||
{{TEST_PROJECT}}
|
||||
# For Windows libraries, enable only after verified Azure OIDC configuration:
|
||||
# sign: true
|
||||
# signing-files: |
|
||||
# src/My.Library/bin/Release/net10.0/My.Library.dll
|
||||
|
||||
publish:
|
||||
name: publish (nuget.org)
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
# One-time owner setup:
|
||||
# - Environment `release` (Settings -> Environments) with a deployment rule that allows
|
||||
# only tags matching v*.
|
||||
# - Code signing: verify the existing Artifact Signing profile, configure repository
|
||||
# OIDC and its Certificate Profile Signer role, then add AZURE_CLIENT_ID,
|
||||
# AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, ARTIFACT_SIGNING_ENDPOINT,
|
||||
# ARTIFACT_SIGNING_ACCOUNT, ARTIFACT_SIGNING_PROFILE and ARTIFACT_SIGNING_SUBJECT
|
||||
# to the protected release environment. Enable sign and enumerate owned files.
|
||||
# See architecture/windows-release-signing.md; an Active profile alone is not proof.
|
||||
# - Code signing (when exception EX-0002 closes): add the environment variables
|
||||
# AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, TRUSTED_SIGNING_ENDPOINT,
|
||||
# TRUSTED_SIGNING_ACCOUNT, TRUSTED_SIGNING_PROFILE (a federated credential for this
|
||||
# repository's `release` environment on the Azure app registration), then set
|
||||
# `sign: true` below.
|
||||
# - winget (optional, stable releases only): the package must already exist in
|
||||
# microsoft/winget-pkgs. Submit it once by hand with
|
||||
# wingetcreate new <Setup.exe URL of the first stable release>
|
||||
@@ -52,13 +51,7 @@ jobs:
|
||||
test-projects: |
|
||||
{{TEST_PROJECT}}
|
||||
cli-project: "{{CLI_PROJECT}}" # remove for apps without a CLI
|
||||
sign: false # enable only after CI authentication and real signed-artifact verification
|
||||
# Required when sign is true; include all owned app/CLI binaries, not dependencies:
|
||||
# signing-files: |
|
||||
# app/MyApp.exe
|
||||
# app/MyApp.dll
|
||||
# app/MyApp.Core.dll
|
||||
# cli/myapp.exe
|
||||
sign: false # exception EX-0002: true once the Azure Trusted Signing identity is validated
|
||||
|
||||
# winget lives here, not in the shared workflow, because its token belongs to this
|
||||
# repository's `release` environment. It runs only for stable releases, and only when
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
#Requires -Version 7.4
|
||||
# Copyright (c) 2026 James Maes
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
BeforeAll `
|
||||
{
|
||||
$lines = Get-Content -LiteralPath (Join-Path $PSScriptRoot '../.github/workflows/release-velopack.yml')
|
||||
$start = [Array]::IndexOf($lines, " @'")
|
||||
$end = $start + 1
|
||||
while($end -lt $lines.Count -and -not $lines[$end].StartsWith(" '@ | Set-Content")) { $end++ }
|
||||
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'
|
||||
# GitHub's default pwsh shell prepends this statement to every inline run body.
|
||||
[IO.File]::WriteAllLines($script:Bootstrap, @('$ErrorActionPreference = "stop"') + @($lines[$dependencyStart..($dependencyEnd - 1)] | ForEach-Object { $_.Substring(10) }))
|
||||
$script:OldTool = $env:K22_VPK_SIGN_TOOL
|
||||
$env:K22_VPK_SIGN_TOOL = 'Invoke-WrapperTool'
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Simulates signing and verification without using credentials.
|
||||
#>
|
||||
function Invoke-WrapperTool
|
||||
{
|
||||
[CmdletBinding()]
|
||||
param([Parameter(ValueFromRemainingArguments)] [string[]] $ToolArguments)
|
||||
|
||||
$ToolCalls.Add($ToolArguments)
|
||||
$global:LASTEXITCODE = if($ToolArguments[0] -eq 'sign') { $SignExit } else { $VerifyExit }
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Describe 'The actual Velopack signing wrapper' `
|
||||
{
|
||||
BeforeEach `
|
||||
{
|
||||
$script:ToolCalls = [Collections.Generic.List[object]]::new()
|
||||
$script:SignExit = 0
|
||||
$script:VerifyExit = 0
|
||||
Mock Get-AuthenticodeSignature { [pscustomobject]@{ Status = 'NotSigned' } }
|
||||
}
|
||||
|
||||
It 'preserves a valid existing signature without signing again' `
|
||||
{
|
||||
Mock Get-AuthenticodeSignature { [pscustomobject]@{ Status = 'Valid' } }
|
||||
& $script:Wrapper -File 'a file.dll'
|
||||
$script:ToolCalls.Count | Should -Be 0
|
||||
}
|
||||
|
||||
It 'refuses an invalid existing signature' `
|
||||
{
|
||||
Mock Get-AuthenticodeSignature { [pscustomobject]@{ Status = 'HashMismatch' } }
|
||||
{ & $script:Wrapper -File 'a file.dll' } | Should -Throw '*invalid signature*'
|
||||
$script:ToolCalls.Count | Should -Be 0
|
||||
}
|
||||
|
||||
It 'stops on signing failure before verification' `
|
||||
{
|
||||
$script:SignExit = 1
|
||||
{ & $script:Wrapper -File 'a file.dll' } | Should -Throw '*Artifact Signing failed*'
|
||||
$script:ToolCalls.Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'stops when signature verification fails' `
|
||||
{
|
||||
$script:VerifyExit = 1
|
||||
{ & $script:Wrapper -File 'a file.dll' } | Should -Throw '*Signature verification failed*'
|
||||
$script:ToolCalls.Count | Should -Be 2
|
||||
}
|
||||
|
||||
It 'uses SHA256, RFC3161 timestamping and all-signature trust verification' `
|
||||
{
|
||||
& $script:Wrapper -File 'a file.dll'
|
||||
$script:ToolCalls.Count | Should -Be 2
|
||||
($script:ToolCalls[0] -join ' ') | Should -BeLike 'sign /fd SHA256 /tr http://timestamp.acs.microsoft.com /td SHA256* a file.dll'
|
||||
($script:ToolCalls[1] -join ' ') | Should -Be 'verify /pa /all /v a file.dll'
|
||||
}
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
#Requires -Version 7.4
|
||||
# Copyright (c) 2026 James Maes (KofTwentyTwo)
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
BeforeDiscovery `
|
||||
{
|
||||
Import-Module (Join-Path $PSScriptRoot 'WindowsSigning.psm1') -Force -Global
|
||||
}
|
||||
|
||||
Describe 'Owned binary catalog' `
|
||||
{
|
||||
BeforeEach `
|
||||
{
|
||||
$script:Root = Join-Path $TestDrive ([guid]::NewGuid().ToString('N'))
|
||||
New-Item -ItemType Directory -Path $script:Root | Out-Null
|
||||
$script:Catalog = Join-Path $script:Root 'catalog.txt'
|
||||
$script:Manifest = Join-Path $script:Root 'manifest.json'
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $script:Root 'Owned.dll')
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $script:Root 'Microsoft.dll')
|
||||
}
|
||||
|
||||
It 'includes only explicit owned names and inventories other binaries' `
|
||||
{
|
||||
New-WindowsSigningCatalog -Root $script:Root -Patterns @('Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest -PreserveOtherBinaries
|
||||
@(Get-Content -LiteralPath $script:Catalog) | Should -HaveCount 1
|
||||
Get-Content -LiteralPath $script:Catalog | Should -Be 'Owned.dll'
|
||||
$entries = @(Get-Content -LiteralPath $script:Manifest -Raw | ConvertFrom-Json)
|
||||
$entries | Should -HaveCount 2
|
||||
@($entries | Where-Object owned).relativePath | Should -Be 'Owned.dll'
|
||||
@($entries | Where-Object { -not $_.owned }).relativePath | Should -Be 'Microsoft.dll'
|
||||
}
|
||||
|
||||
It 'rejects a missing required binary rather than silently skipping signing' `
|
||||
{
|
||||
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Missing.exe') -CatalogPath $script:Catalog -ManifestPath $script:Manifest } |
|
||||
Should -Throw '*No binary matches*'
|
||||
}
|
||||
|
||||
It 'rejects parent traversal' `
|
||||
{
|
||||
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('../Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest } |
|
||||
Should -Throw '*relative paths*'
|
||||
}
|
||||
|
||||
It 'supports WhatIf without writing signing inputs' `
|
||||
{
|
||||
New-WindowsSigningCatalog -Root $script:Root -Patterns @('Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest -WhatIf
|
||||
Test-Path -LiteralPath $script:Catalog | Should -BeFalse
|
||||
Test-Path -LiteralPath $script:Manifest | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'rejects malformed owned PE input before writing a catalog' `
|
||||
{
|
||||
[IO.File]::WriteAllBytes((Join-Path $script:Root 'Owned.dll'), [byte[]]::new(128))
|
||||
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest } | Should -Throw '*Invalid published PE binary: Owned.dll*'
|
||||
Test-Path -LiteralPath $script:Catalog | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'rejects an already malformed preserved dependency' `
|
||||
{
|
||||
[IO.File]::WriteAllBytes((Join-Path $script:Root 'Microsoft.dll'), [byte[]]::new(128))
|
||||
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest -PreserveOtherBinaries } | Should -Throw '*Invalid published PE binary: Microsoft.dll*'
|
||||
Test-Path -LiteralPath $script:Manifest | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'does not treat package-format signing targets as PE images' `
|
||||
{
|
||||
Set-Content -LiteralPath (Join-Path $script:Root 'Setup.msi') -Value 'package fixture; signature validation is a later gate'
|
||||
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Setup.msi') -CatalogPath $script:Catalog -ManifestPath $script:Manifest } | Should -Not -Throw
|
||||
Get-Content -LiteralPath $script:Catalog | Should -Be 'Setup.msi'
|
||||
}
|
||||
|
||||
It 'ignores excluded binaries when dependency preservation is disabled' `
|
||||
{
|
||||
[IO.File]::WriteAllBytes((Join-Path $script:Root 'Microsoft.dll'), [byte[]]::new(128))
|
||||
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest } | Should -Not -Throw
|
||||
@(Get-Content -LiteralPath $script:Manifest -Raw | ConvertFrom-Json) | Should -HaveCount 1
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Signature and distributed-byte gates' `
|
||||
{
|
||||
InModuleScope WindowsSigning `
|
||||
{
|
||||
BeforeAll `
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Supplies deterministic SignTool results for rejection-path tests.
|
||||
#>
|
||||
function Invoke-ProbeSignTool
|
||||
{
|
||||
$global:LASTEXITCODE = $script:ProbeExitCode
|
||||
return $script:ProbeOutput
|
||||
}
|
||||
}
|
||||
|
||||
BeforeEach `
|
||||
{
|
||||
$script:ProbeExitCode = 0
|
||||
$script:ProbeOutput = 'Hash of file (sha256): 0123456789'
|
||||
$script:ProbeSignature = [pscustomobject]@{
|
||||
Status = 'Valid'
|
||||
SignerCertificate = [pscustomobject]@{ Subject = 'CN=Verified Publisher'; SubjectName = [pscustomobject]@{ Name = 'CN=Verified Publisher' } }
|
||||
TimeStamperCertificate = [pscustomobject]@{ Subject = 'CN=Timestamp Authority' }
|
||||
}
|
||||
Mock Get-AuthenticodeSignature { return $script:ProbeSignature }
|
||||
}
|
||||
|
||||
It 'accepts a trusted timestamped SHA256 signature for the verified subject' `
|
||||
{
|
||||
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Not -Throw
|
||||
}
|
||||
|
||||
It 'rejects a native verification failure' `
|
||||
{
|
||||
$script:ProbeExitCode = 1
|
||||
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*verification failed*'
|
||||
}
|
||||
|
||||
It 'rejects a different publisher' `
|
||||
{
|
||||
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Other Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*Unexpected publisher*'
|
||||
}
|
||||
|
||||
It 'rejects a missing timestamp' `
|
||||
{
|
||||
$script:ProbeSignature.TimeStamperCertificate = $null
|
||||
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*timestamped*'
|
||||
}
|
||||
|
||||
It 'rejects an untrusted certificate' `
|
||||
{
|
||||
$script:ProbeSignature.Status = 'NotTrusted'
|
||||
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*timestamped*'
|
||||
}
|
||||
|
||||
It 'rejects a non-SHA256 digest' `
|
||||
{
|
||||
$script:ProbeOutput = 'Hash of file (sha1): 0123456789'
|
||||
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*SHA256*'
|
||||
}
|
||||
|
||||
It 'rejects a SHA1 signature appended alongside a SHA256 signature' `
|
||||
{
|
||||
$script:ProbeOutput = "Hash of file (sha256): abc`nHash of file (sha1): def"
|
||||
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*SHA256*'
|
||||
}
|
||||
|
||||
It 'rejects modified third-party binaries' `
|
||||
{
|
||||
$root = Join-Path $TestDrive 'preserve'
|
||||
New-Item -ItemType Directory -Path $root | Out-Null
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Microsoft.dll')
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
|
||||
Set-Content -LiteralPath (Join-Path $root 'Microsoft.dll') -Value 'modified'
|
||||
{ Assert-WindowsSigningManifest -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*Third-party binary was modified*'
|
||||
}
|
||||
|
||||
It 'refuses to catalog another publisher signature for replacement' `
|
||||
{
|
||||
$root = Join-Path $TestDrive 'other-publisher'
|
||||
New-Item -ItemType Directory -Path $root | Out-Null
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
|
||||
$script:ProbeSignature.SignerCertificate.SubjectName.Name = 'CN=Other Publisher'
|
||||
{ New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath (Join-Path $root 'manifest.json') -ExpectedSubject 'CN=Verified Publisher' } | Should -Throw '*another publisher*'
|
||||
Test-Path -LiteralPath (Join-Path $root 'catalog.txt') | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'rejects changed owned bytes in the final package' `
|
||||
{
|
||||
$root = Join-Path $TestDrive 'changed'
|
||||
$assets = Join-Path $root 'assets'
|
||||
$packed = Join-Path $root 'packed'
|
||||
New-Item -ItemType Directory -Path $root, $assets, $packed | Out-Null
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest
|
||||
Set-Content -LiteralPath (Join-Path $packed 'Owned.dll') -Value 'modified after signing'
|
||||
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets 'Owned.1.0.0.nupkg'))
|
||||
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*Distributed binary changed*'
|
||||
}
|
||||
|
||||
It 'rejects an archive missing its required owned binary' `
|
||||
{
|
||||
$root = Join-Path $TestDrive 'missing'
|
||||
$assets = Join-Path $root 'assets'
|
||||
$packed = Join-Path $root 'packed'
|
||||
New-Item -ItemType Directory -Path $root, $assets, $packed | Out-Null
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest
|
||||
Set-Content -LiteralPath (Join-Path $packed 'README.txt') -Value 'no binary'
|
||||
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets 'Owned.1.0.0.nupkg'))
|
||||
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*Owned binary missing*'
|
||||
}
|
||||
|
||||
It 'rejects a missing dependency from <Archive>' -TestCases @(
|
||||
@{ Archive = 'Owned-Portable.zip'; Head = 'app' },
|
||||
@{ Archive = 'Owned-1.0.0-full.nupkg'; Head = 'app' },
|
||||
@{ Archive = 'Owned-cli-win-x64.zip'; Head = 'cli' }
|
||||
) `
|
||||
{
|
||||
param($Archive, $Head)
|
||||
$root = Join-Path $TestDrive ('missing-dependency-' + [guid]::NewGuid().ToString('N'))
|
||||
$publish = Join-Path $root 'publish'
|
||||
$assets = Join-Path $root 'assets'
|
||||
$packed = Join-Path $root 'packed'
|
||||
New-Item -ItemType Directory -Path "$publish/$Head", $assets, $packed | Out-Null
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/$Head/Owned.dll"
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/$Head/Microsoft.dll"
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $publish -Patterns @("$Head/Owned.dll") -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
|
||||
Copy-Item -LiteralPath "$publish/$Head/Owned.dll" -Destination $packed
|
||||
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets $Archive))
|
||||
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*Third-party binary missing*'
|
||||
}
|
||||
|
||||
It 'allows only the default excluded foreign runtime helpers to be omitted' `
|
||||
{
|
||||
$root = Join-Path $TestDrive 'excluded-runtime'
|
||||
$publish = Join-Path $root 'publish'
|
||||
$assets = Join-Path $root 'assets'
|
||||
$packed = Join-Path $root 'packed'
|
||||
New-Item -ItemType Directory -Path "$publish/app", $assets, $packed | Out-Null
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Owned.dll"
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Microsoft.dll"
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/createdump.exe"
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Owned.vshost.exe"
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $publish -Patterns @('app/Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
|
||||
Copy-Item -LiteralPath "$publish/app/Owned.dll", "$publish/app/Microsoft.dll" -Destination $packed
|
||||
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets 'Owned-Portable.zip'))
|
||||
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Not -Throw
|
||||
}
|
||||
|
||||
It 'rejects changed dependency bytes in the final archive' `
|
||||
{
|
||||
$root = Join-Path $TestDrive 'changed-dependency'
|
||||
$publish = Join-Path $root 'publish'
|
||||
$assets = Join-Path $root 'assets'
|
||||
$packed = Join-Path $root 'packed'
|
||||
New-Item -ItemType Directory -Path "$publish/app", $assets, $packed | Out-Null
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Owned.dll"
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Microsoft.dll"
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $publish -Patterns @('app/Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
|
||||
Copy-Item -LiteralPath "$publish/app/Owned.dll" -Destination $packed
|
||||
Set-Content -LiteralPath (Join-Path $packed 'Microsoft.dll') -Value 'changed'
|
||||
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets 'Owned-Portable.zip'))
|
||||
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*Distributed binary changed*'
|
||||
}
|
||||
|
||||
It 'accepts the exact signed bytes shipped in a package' `
|
||||
{
|
||||
$root = Join-Path $TestDrive 'valid'
|
||||
$assets = Join-Path $root 'assets'
|
||||
New-Item -ItemType Directory -Path $root, $assets | Out-Null
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest
|
||||
$packed = Join-Path $root 'packed'
|
||||
New-Item -ItemType Directory -Path $packed | Out-Null
|
||||
Copy-Item -LiteralPath (Join-Path $root 'Owned.dll') -Destination $packed
|
||||
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets 'Owned.1.0.0.nupkg'))
|
||||
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Not -Throw
|
||||
}
|
||||
|
||||
It 'verifies separate NuGet packages whose IDs share the core package prefix' `
|
||||
{
|
||||
$root = Join-Path $TestDrive 'prefix'
|
||||
$assets = Join-Path $root 'assets'
|
||||
New-Item -ItemType Directory -Path $root, $assets | Out-Null
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.Adapter.dll')
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll', 'Owned.Adapter.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest
|
||||
foreach($name in @('Owned', 'Owned.Adapter'))
|
||||
{
|
||||
$packed = Join-Path $root $name
|
||||
New-Item -ItemType Directory -Path $packed | Out-Null
|
||||
Copy-Item -LiteralPath (Join-Path $root "$name.dll") -Destination $packed
|
||||
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets "$name.1.0.0.nupkg"))
|
||||
}
|
||||
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Not -Throw
|
||||
}
|
||||
|
||||
It 'preserves same-name third-party satellite binaries in different culture directories' `
|
||||
{
|
||||
$root = Join-Path $TestDrive 'cultures'
|
||||
$publish = Join-Path $root 'publish'
|
||||
$assets = Join-Path $root 'assets'
|
||||
New-Item -ItemType Directory -Path $assets, "$publish/app/en", "$publish/app/fr" | Out-Null
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Owned.dll"
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/en/Library.resources.dll"
|
||||
[IO.File]::AppendAllText("$publish/app/en/Library.resources.dll", 'English')
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/fr/Library.resources.dll"
|
||||
[IO.File]::AppendAllText("$publish/app/fr/Library.resources.dll", 'French')
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $publish -Patterns @('app/Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
|
||||
[IO.Compression.ZipFile]::CreateFromDirectory("$publish/app", (Join-Path $assets 'Owned-Portable.zip'))
|
||||
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Velopack installer payload' `
|
||||
{
|
||||
BeforeEach `
|
||||
{
|
||||
$root = Join-Path $TestDrive ([guid]::NewGuid().ToString('N'))
|
||||
New-Item -ItemType Directory -Path $root | Out-Null
|
||||
$script:Installer = Join-Path $root 'Probe-Setup.exe'
|
||||
$script:FullPackage = Join-Path $root 'Probe-full.nupkg'
|
||||
$payload = [Text.Encoding]::UTF8.GetBytes('exact full package payload')
|
||||
[IO.File]::WriteAllBytes($script:FullPackage, $payload)
|
||||
Copy-Item -LiteralPath (Get-WindowsSignTool) -Destination $script:Installer
|
||||
$stream = [IO.File]::Open($script:Installer, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite)
|
||||
$reader = [Reflection.PortableExecutable.PEReader]::new($stream, [Reflection.PortableExecutable.PEStreamOptions]::LeaveOpen)
|
||||
try
|
||||
{
|
||||
$headerPosition = $reader.PEHeaders.SectionHeaders[0].PointerToRawData
|
||||
$packageOffset = $stream.Length
|
||||
$stream.Position = $headerPosition
|
||||
$stream.Write([BitConverter]::GetBytes($packageOffset))
|
||||
$stream.Write([BitConverter]::GetBytes([long] $payload.Length))
|
||||
$stream.Write([Convert]::FromHexString('94F0B17B6893E02937EB34EF53AAE7D42B54F5707EF5D6F57854983E5E94ED7D'))
|
||||
$stream.Position = $packageOffset
|
||||
$stream.Write($payload)
|
||||
}
|
||||
finally
|
||||
{
|
||||
$reader.Dispose()
|
||||
$stream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
It 'accepts the exact embedded full-package bytes' `
|
||||
{
|
||||
{ Assert-VelopackInstallerPayload -Installer $script:Installer -FullPackage $script:FullPackage } | Should -Not -Throw
|
||||
}
|
||||
|
||||
It 'rejects a different embedded payload of the same length' `
|
||||
{
|
||||
$bytes = [IO.File]::ReadAllBytes($script:FullPackage)
|
||||
$bytes[0] = 0
|
||||
[IO.File]::WriteAllBytes($script:FullPackage, $bytes)
|
||||
{ Assert-VelopackInstallerPayload -Installer $script:Installer -FullPackage $script:FullPackage } | Should -Throw '*different full package*'
|
||||
}
|
||||
|
||||
It 'rejects a different full-package length' `
|
||||
{
|
||||
[IO.File]::WriteAllText($script:FullPackage, 'short')
|
||||
{ Assert-VelopackInstallerPayload -Installer $script:Installer -FullPackage $script:FullPackage } | Should -Throw '*length differs*'
|
||||
}
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
#Requires -Version 7.4
|
||||
# Copyright (c) 2026 James Maes (KofTwentyTwo)
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves the newest installed x64 Windows SDK SignTool.
|
||||
#>
|
||||
function Get-WindowsSignTool
|
||||
{
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$sdk = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits/10/bin'
|
||||
$tools = @(Get-ChildItem -Path "$sdk/*/x64/signtool.exe" -File -ErrorAction SilentlyContinue |
|
||||
Sort-Object { [version] $_.Directory.Parent.Name } -Descending)
|
||||
if($tools.Count -eq 0) { throw 'Install the Windows SDK with x64 SignTool.' }
|
||||
return $tools[0].FullName
|
||||
}
|
||||
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds an explicit signing catalog and records other binaries for preservation checks.
|
||||
#>
|
||||
function New-WindowsSigningCatalog
|
||||
{
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string] $Root,
|
||||
[Parameter(Mandatory)] [string[]] $Patterns,
|
||||
[Parameter(Mandatory)] [string] $CatalogPath,
|
||||
[Parameter(Mandatory)] [string] $ManifestPath,
|
||||
[string] $ExpectedSubject = '',
|
||||
[switch] $PreserveOtherBinaries
|
||||
)
|
||||
|
||||
$rootPath = (Resolve-Path -LiteralPath $Root).Path
|
||||
$binaries = @(Get-ChildItem -LiteralPath $rootPath -File -Recurse |
|
||||
Where-Object Extension -In '.exe', '.dll', '.msi', '.msix')
|
||||
$owned = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
foreach($pattern in $Patterns)
|
||||
{
|
||||
$pattern = $pattern.Trim().Replace('\', '/')
|
||||
if([string]::IsNullOrWhiteSpace($pattern) -or $pattern.Split('/') -contains '..' -or [IO.Path]::IsPathRooted($pattern))
|
||||
{
|
||||
throw "Signing patterns must be nonempty relative paths or file names: '$pattern'."
|
||||
}
|
||||
$catalogMatches = @($binaries | Where-Object {
|
||||
$candidate = if($pattern.Contains('/')) { [IO.Path]::GetRelativePath($rootPath, $_.FullName).Replace('\', '/') } else { $_.Name }
|
||||
$candidate -like $pattern
|
||||
})
|
||||
if($catalogMatches.Count -eq 0) { throw "No binary matches required signing pattern '$pattern'." }
|
||||
foreach($file in $catalogMatches)
|
||||
{
|
||||
if($ExpectedSubject)
|
||||
{
|
||||
$expected = [Security.Cryptography.X509Certificates.X500DistinguishedName]::new($ExpectedSubject).Name
|
||||
$existing = Get-AuthenticodeSignature -LiteralPath $file.FullName
|
||||
if($null -ne $existing.SignerCertificate -and $existing.SignerCertificate.SubjectName.Name -cne $expected)
|
||||
{
|
||||
throw "Refusing to replace another publisher's signature on '$($file.FullName)'. Keep it outside the owned catalog."
|
||||
}
|
||||
if($existing.Status -notin 'Valid', 'NotSigned') { throw "Invalid existing signature on '$($file.FullName)'." }
|
||||
}
|
||||
$null = $owned.Add($file.FullName)
|
||||
}
|
||||
}
|
||||
foreach($file in $binaries | Where-Object { ($PreserveOtherBinaries -or $owned.Contains($_.FullName)) -and $_.Extension -in '.exe', '.dll' })
|
||||
{
|
||||
$stream = [IO.File]::OpenRead($file.FullName)
|
||||
try
|
||||
{
|
||||
$reader = [Reflection.PortableExecutable.PEReader]::new($stream)
|
||||
try
|
||||
{
|
||||
if($null -eq $reader.PEHeaders.PEHeader) { throw 'Missing executable header.' }
|
||||
}
|
||||
finally { $reader.Dispose() }
|
||||
}
|
||||
catch { throw "Invalid published PE binary: $($file.Name). $($_.Exception.Message)" }
|
||||
finally { $stream.Dispose() }
|
||||
}
|
||||
$entries = @($binaries | Where-Object { $PreserveOtherBinaries -or $owned.Contains($_.FullName) } | ForEach-Object {
|
||||
[ordered]@{
|
||||
path = $_.FullName
|
||||
relativePath = [IO.Path]::GetRelativePath($rootPath, $_.FullName).Replace('\', '/')
|
||||
owned = $owned.Contains($_.FullName)
|
||||
sha256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash
|
||||
}
|
||||
})
|
||||
$catalogDirectory = [IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($CatalogPath))
|
||||
if(-not $PSCmdlet.ShouldProcess("$CatalogPath; $ManifestPath", 'Write signing catalog and preservation manifest')) { return }
|
||||
New-Item -ItemType Directory -Force -Path $catalogDirectory | Out-Null
|
||||
@($owned | Sort-Object | ForEach-Object { [IO.Path]::GetRelativePath($catalogDirectory, $_) }) |
|
||||
Set-Content -LiteralPath $CatalogPath -Encoding utf8NoBOM
|
||||
ConvertTo-Json -InputObject $entries -Depth 5 | Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
|
||||
}
|
||||
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Requires all Authenticode signatures to validate, with the configured publisher and a timestamp.
|
||||
#>
|
||||
function Assert-WindowsSignature
|
||||
{
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string[]] $Files,
|
||||
[Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ExpectedSubject,
|
||||
[string] $SignTool = (Get-WindowsSignTool)
|
||||
)
|
||||
|
||||
if($Files.Count -eq 0) { throw 'No shipped binaries were supplied for verification.' }
|
||||
$expected = [Security.Cryptography.X509Certificates.X500DistinguishedName]::new($ExpectedSubject).Name
|
||||
foreach($file in $Files)
|
||||
{
|
||||
$output = (& $SignTool verify /pa /all /v $file 2>&1) -join "`n"
|
||||
Write-Output $output
|
||||
if($LASTEXITCODE -ne 0) { throw "SignTool verification failed for '$file'." }
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $file
|
||||
if($signature.Status -ne 'Valid' -or $null -eq $signature.SignerCertificate -or $null -eq $signature.TimeStamperCertificate)
|
||||
{
|
||||
throw "Missing valid timestamped Authenticode signature on '$file'."
|
||||
}
|
||||
if($signature.SignerCertificate.SubjectName.Name -cne $expected)
|
||||
{
|
||||
throw "Unexpected publisher on '$file': $($signature.SignerCertificate.Subject)."
|
||||
}
|
||||
$digests = @([regex]::Matches($output, '(?i)Hash of file \(([^)]+)\)') | ForEach-Object { $_.Groups[1].Value })
|
||||
if($digests.Count -eq 0 -or @($digests | Where-Object { $_ -ne 'sha256' }).Count -gt 0)
|
||||
{
|
||||
throw "SignTool did not report a SHA256 file digest for '$file'. Use the English Windows SDK verification output."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Verifies signed catalog entries and checks that other binary bytes were preserved.
|
||||
#>
|
||||
function Assert-WindowsSigningManifest
|
||||
{
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string] $ManifestPath,
|
||||
[Parameter(Mandatory)] [string] $ExpectedSubject,
|
||||
[string] $SignTool = (Get-WindowsSignTool)
|
||||
)
|
||||
|
||||
$entries = @(Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json)
|
||||
$owned = @($entries | Where-Object owned | ForEach-Object path)
|
||||
Assert-WindowsSignature -Files $owned -ExpectedSubject $ExpectedSubject -SignTool $SignTool
|
||||
foreach($entry in $entries | Where-Object { -not $_.owned })
|
||||
{
|
||||
if((Get-FileHash -LiteralPath $entry.path -Algorithm SHA256).Hash -ne $entry.sha256)
|
||||
{
|
||||
throw "Third-party binary was modified: $($entry.relativePath)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Checks signed binaries inside the final ZIP/NuGet assets and their bytes against the signed build.
|
||||
#>
|
||||
function Assert-WindowsDistribution
|
||||
{
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string] $Directory,
|
||||
[Parameter(Mandatory)] [string] $ManifestPath,
|
||||
[Parameter(Mandatory)] [string] $ExpectedSubject,
|
||||
[string] $SignTool = (Get-WindowsSignTool)
|
||||
)
|
||||
|
||||
$entries = @(Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json)
|
||||
$owned = @($entries | Where-Object owned)
|
||||
if($owned.Count -eq 0) { throw 'The signing manifest has no owned binaries.' }
|
||||
$assets = @(Get-ChildItem -LiteralPath $Directory -File)
|
||||
$archives = @($assets | Where-Object { $_.Extension -in '.zip', '.nupkg' -and $_.Name -notlike '*-delta.nupkg' })
|
||||
if($archives.Count -eq 0) { throw 'No final ZIP or full NuGet package was produced.' }
|
||||
$seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
foreach($archive in $archives)
|
||||
{
|
||||
$scratch = Join-Path ([IO.Path]::GetTempPath()) ('k22-signing-' + [guid]::NewGuid().ToString('N'))
|
||||
try
|
||||
{
|
||||
[IO.Compression.ZipFile]::ExtractToDirectory($archive.FullName, $scratch)
|
||||
$files = @(Get-ChildItem -LiteralPath $scratch -File -Recurse)
|
||||
$archiveSeen = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
foreach($entry in $entries)
|
||||
{
|
||||
$name = [IO.Path]::GetFileName($entry.path)
|
||||
$packageRelative = if($entry.relativePath.StartsWith('app/') -or $entry.relativePath.StartsWith('cli/')) { $entry.relativePath.Substring(4) } else { $name }
|
||||
$copies = @($files | Where-Object {
|
||||
$relative = [IO.Path]::GetRelativePath($scratch, $_.FullName).Replace('\', '/')
|
||||
$_.Name -eq $name -and ($relative -eq $packageRelative -or $relative.EndsWith('/' + $packageRelative, [StringComparison]::OrdinalIgnoreCase))
|
||||
})
|
||||
foreach($copy in $copies)
|
||||
{
|
||||
$hash = (Get-FileHash -LiteralPath $copy.FullName -Algorithm SHA256).Hash
|
||||
$expectedHash = if($entry.owned) { (Get-FileHash -LiteralPath $entry.path -Algorithm SHA256).Hash } else { $entry.sha256 }
|
||||
if($hash -ne $expectedHash)
|
||||
{
|
||||
# The portable archive also carries a generated launcher at its root.
|
||||
if($entry.owned -and $copy.Extension -eq '.exe' -and $copy.DirectoryName -eq $scratch -and $archive.Name -like '*-Portable.zip')
|
||||
{
|
||||
Assert-WindowsSignature -Files @($copy.FullName) -ExpectedSubject $ExpectedSubject -SignTool $SignTool
|
||||
continue
|
||||
}
|
||||
throw "Distributed binary changed in $($archive.Name): $name."
|
||||
}
|
||||
if($entry.owned)
|
||||
{
|
||||
Assert-WindowsSignature -Files @($copy.FullName) -ExpectedSubject $ExpectedSubject -SignTool $SignTool
|
||||
$null = $seen.Add($entry.path)
|
||||
}
|
||||
$null = $archiveSeen.Add($entry.path)
|
||||
}
|
||||
}
|
||||
$required = @($entries | Where-Object {
|
||||
if($archive.Name -like '*-cli-*.zip') { return $_.relativePath.StartsWith('cli/') }
|
||||
if($archive.Name -like '*-Portable.zip' -or $archive.Name -like '*-full.nupkg')
|
||||
{
|
||||
# Match Velopack's default exclusions for runtime helpers.
|
||||
if(-not $_.owned -and $_.relativePath -match '(?:^|/)createdump[^/]*|\.vshost\.') { return $false }
|
||||
return $_.relativePath.StartsWith('app/')
|
||||
}
|
||||
$packageId = [regex]::Escape([IO.Path]::GetFileNameWithoutExtension($_.path))
|
||||
return $_.owned -and $archive.Name -match "^$packageId\.[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?\.nupkg$"
|
||||
})
|
||||
foreach($entry in $required)
|
||||
{
|
||||
if(-not $archiveSeen.Contains($entry.path))
|
||||
{
|
||||
$kind = if($entry.owned) { 'Owned' } else { 'Third-party' }
|
||||
throw "$kind binary missing from $($archive.Name): $($entry.relativePath)."
|
||||
}
|
||||
}
|
||||
# Velopack creates these branded helpers before its signing hook.
|
||||
$updates = @($files | Where-Object { $_.Name -in 'Update.exe', 'Squirrel.exe' -or $_.Name -like '*_ExecutionStub.exe' } | ForEach-Object FullName)
|
||||
if($updates.Count -gt 0) { Assert-WindowsSignature -Files $updates -ExpectedSubject $ExpectedSubject -SignTool $SignTool }
|
||||
}
|
||||
finally
|
||||
{
|
||||
$temporaryRoot = [IO.Path]::TrimEndingDirectorySeparator([IO.Path]::GetFullPath([IO.Path]::GetTempPath()))
|
||||
if([IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($scratch)) -ne $temporaryRoot -or [IO.Path]::GetFileName($scratch) -notlike 'k22-signing-*')
|
||||
{
|
||||
throw 'Refusing to remove a directory outside the signing scratch location.'
|
||||
}
|
||||
if(Test-Path -LiteralPath $scratch) { Remove-Item -LiteralPath $scratch -Recurse -Force }
|
||||
}
|
||||
}
|
||||
foreach($entry in $owned)
|
||||
{
|
||||
if(-not $seen.Contains($entry.path)) { throw "Owned binary is missing from distributed archives: $($entry.relativePath)." }
|
||||
}
|
||||
$installers = @($assets | Where-Object Extension -In '.exe', '.msi', '.msix' | ForEach-Object FullName)
|
||||
if($installers.Count -gt 0) { Assert-WindowsSignature -Files $installers -ExpectedSubject $ExpectedSubject -SignTool $SignTool }
|
||||
foreach($setup in $assets | Where-Object Name -Like '*-Setup.exe')
|
||||
{
|
||||
$fullPackages = @($assets | Where-Object Name -Like '*-full.nupkg')
|
||||
if($fullPackages.Count -ne 1) { throw 'Require one current full package to verify the installer payload.' }
|
||||
Assert-VelopackInstallerPayload -Installer $setup.FullName -FullPackage $fullPackages[0].FullName
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Requires the signed Velopack installer to embed the exact verified full update package.
|
||||
#>
|
||||
function Assert-VelopackInstallerPayload
|
||||
{
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string] $Installer,
|
||||
[Parameter(Mandatory)] [string] $FullPackage
|
||||
)
|
||||
|
||||
# Velopack 1.2.161 SetupBundle: two little-endian Int64s followed by this marker.
|
||||
$marker = '94F0B17B6893E02937EB34EF53AAE7D42B54F5707EF5D6F57854983E5E94ED7D'
|
||||
$stream = [IO.File]::OpenRead($Installer)
|
||||
$reader = [Reflection.PortableExecutable.PEReader]::new($stream, [Reflection.PortableExecutable.PEStreamOptions]::LeaveOpen)
|
||||
$hasher = [Security.Cryptography.IncrementalHash]::CreateHash([Security.Cryptography.HashAlgorithmName]::SHA256)
|
||||
try
|
||||
{
|
||||
$offset = 0L
|
||||
$length = 0L
|
||||
$headersFound = 0
|
||||
foreach($section in $reader.PEHeaders.SectionHeaders)
|
||||
{
|
||||
[byte[]] $bytes = $reader.GetSectionData($section.VirtualAddress).GetContent()
|
||||
$position = [Convert]::ToHexString($bytes).IndexOf($marker, [StringComparison]::Ordinal)
|
||||
if($position -lt 32 -or $position % 2 -ne 0) { continue }
|
||||
$headersFound++
|
||||
$offset = [BitConverter]::ToInt64($bytes, $position / 2 - 16)
|
||||
$length = [BitConverter]::ToInt64($bytes, $position / 2 - 8)
|
||||
}
|
||||
if($headersFound -ne 1 -or $offset -le 0 -or $length -le 0 -or $offset -gt $stream.Length -or $length -gt $stream.Length - $offset)
|
||||
{
|
||||
throw 'The Velopack installer bundle header is missing, ambiguous or outside the file.'
|
||||
}
|
||||
if($length -ne (Get-Item -LiteralPath $FullPackage).Length) { throw 'Installer payload length differs from the full package.' }
|
||||
$stream.Position = $offset
|
||||
$buffer = [byte[]]::new(1048576)
|
||||
$remaining = $length
|
||||
while($remaining -gt 0)
|
||||
{
|
||||
$count = $stream.Read($buffer, 0, [int] [math]::Min($buffer.Length, $remaining))
|
||||
if($count -eq 0) { throw 'Unexpected end of the installer payload.' }
|
||||
$hasher.AppendData($buffer, 0, $count)
|
||||
$remaining -= $count
|
||||
}
|
||||
$hash = [Convert]::ToHexString($hasher.GetHashAndReset())
|
||||
if($hash -ne (Get-FileHash -LiteralPath $FullPackage -Algorithm SHA256).Hash) { throw 'Installer embeds a different full package.' }
|
||||
}
|
||||
finally
|
||||
{
|
||||
$hasher.Dispose()
|
||||
$reader.Dispose()
|
||||
$stream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Get-WindowsSignTool, New-WindowsSigningCatalog, Assert-WindowsSignature, Assert-WindowsSigningManifest, Assert-WindowsDistribution, Assert-VelopackInstallerPayload
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.2.1
|
||||
1.0.0
|
||||
Reference in new issue
Block a user