diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dcd14b8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,188 @@ +# Builds, signs and packages a release. +# +# SignPath Foundation signs only what a trusted build system built from this +# repository, so this workflow is the only way a signed build can exist. A +# local `cmake --build --preset release` + Inno compile still works for +# testing, but what it produces cannot be signed. +# +# Signing happens in two rounds, because the installer contains the +# executables and so has to be compiled around the signed copies: +# +# 1. build -> sign the three .exes (artifact configuration "exes") +# 2. compile the installer around them -> sign Setup.exe ("installer") +# +# Under release-signing, each round waits for a manual approval in SignPath, +# so a release takes two approvals. test-signing needs none. +# +# Triggers: +# * a pushed tag vX.Y -> release-signing, then a DRAFT GitHub release +# with the signed installer attached +# * Run workflow (manual) -> test-signing by default; the installer is +# left as a workflow artifact, no release made +# +# Configuration, under Settings > Secrets and variables > Actions: +# secret SIGNPATH_API_TOKEN API token of the SignPath CI user +# variable SIGNPATH_ORGANIZATION_ID +# variable SIGNPATH_PROJECT_SLUG +# +# Artifact configurations must exist in SignPath with the slugs "exes" and +# "installer"; their contents are versioned in .signpath/. + +name: Release + +on: + push: + tags: ['v*'] + workflow_dispatch: + inputs: + signing-policy: + description: SignPath signing policy + type: choice + options: [test-signing, release-signing] + default: test-signing + +permissions: + contents: write # create the draft release + actions: read # lets SignPath fetch the uploaded artifacts + +env: + SIGNING_POLICY: ${{ startsWith(github.ref, 'refs/tags/') && 'release-signing' || inputs.signing-policy || 'test-signing' }} + BUILD_DIR: build\release\Release + VIGEM_FILE: ViGEmBus_1.22.0_x64_x86_arm64.exe + VIGEM_URL: https://github.com/nefarius/ViGEmBus/releases/download/v1.22.0/ViGEmBus_1.22.0_x64_x86_arm64.exe + VIGEM_SHA256: 89220a7865076b342892f98865f3499fb7c4cfd673159e89d352c360fd014c6a + +jobs: + release: + runs-on: windows-latest + # Covers two waits for a human to approve in SignPath. + timeout-minutes: 300 + defaults: + run: + shell: pwsh + + steps: + - uses: actions/checkout@v5 + + # A tag that disagrees with the script would publish an installer under + # one version that installs another. The script already refuses an exe + # whose version differs from its own; this closes the other side. + - name: Check tag matches installer version + if: startsWith(github.ref, 'refs/tags/') + run: | + $iss = Get-Content resources\InnoInstallerScript.iss -Raw + if ($iss -notmatch '#define MyAppVersion "([^"]+)"') { throw 'MyAppVersion not found in the installer script' } + $scriptVersion = $Matches[1] + $tagVersion = '${{ github.ref_name }}'.TrimStart('v') + if ($tagVersion -ne $scriptVersion) { + throw "Tag ${{ github.ref_name }} does not match MyAppVersion $scriptVersion" + } + + - name: Build + run: | + cmake --preset release + cmake --build --preset release + + # ---- Round one: the executables ------------------------------------- + + - name: Stage unsigned executables + run: | + New-Item -ItemType Directory staging\exes | Out-Null + foreach ($exe in 'SteamlessController.exe', 'SteamlessDeviceCycle.exe', 'ViGEmBusProbe.exe') { + Copy-Item "$env:BUILD_DIR\$exe" staging\exes\ + } + + - name: Upload unsigned executables + id: upload-exes + uses: actions/upload-artifact@v7 + with: + name: unsigned-exes + path: staging\exes\ + + - name: Sign executables + uses: signpath/github-action-submit-signing-request@v3 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ env.SIGNING_POLICY }} + artifact-configuration-slug: exes + github-artifact-id: ${{ steps.upload-exes.outputs.artifact-id }} + wait-for-completion: true + wait-for-completion-timeout-in-seconds: 7200 + output-artifact-directory: signed\exes + + # Inno reads from BUILD_DIR, so the signed copies replace the unsigned + # ones there rather than the script learning a second source directory. + - name: Put signed executables in place + run: Copy-Item signed\exes\*.exe $env:BUILD_DIR\ -Force + + # ---- Round two: the installer --------------------------------------- + + # Not in the repository (*.exe is gitignored). Pinned by hash so the + # installer can only ever carry the exact binary nefarius published. + - name: Fetch ViGEmBus installer + run: | + Invoke-WebRequest $env:VIGEM_URL -OutFile "resources\$env:VIGEM_FILE" + $hash = (Get-FileHash "resources\$env:VIGEM_FILE" -Algorithm SHA256).Hash + if ($hash -ne $env:VIGEM_SHA256) { throw "ViGEmBus hash mismatch: got $hash" } + + - name: Compile installer + run: | + $iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" + if (-not (Test-Path $iscc)) { + choco install innosetup -y --no-progress + if ($LASTEXITCODE -ne 0) { throw 'Inno Setup install failed' } + } + & $iscc resources\InnoInstallerScript.iss + if ($LASTEXITCODE -ne 0) { throw "ISCC failed with exit code $LASTEXITCODE" } + + - name: Upload unsigned installer + id: upload-installer + uses: actions/upload-artifact@v7 + with: + name: unsigned-installer + path: Output\SteamlessController-Setup.exe + + - name: Sign installer + uses: signpath/github-action-submit-signing-request@v3 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ env.SIGNING_POLICY }} + artifact-configuration-slug: installer + github-artifact-id: ${{ steps.upload-installer.outputs.artifact-id }} + wait-for-completion: true + wait-for-completion-timeout-in-seconds: 7200 + output-artifact-directory: signed\installer + + # Test certificates do not chain to a trusted root, so only a + # release-signed build can be held to this. + - name: Verify signatures + if: env.SIGNING_POLICY == 'release-signing' + run: | + $files = @(Get-ChildItem signed\exes\*.exe) + @(Get-Item signed\installer\SteamlessController-Setup.exe) + foreach ($file in $files) { + $sig = Get-AuthenticodeSignature $file.FullName + "{0}: {1} ({2})" -f $file.Name, $sig.Status, $sig.SignerCertificate.Subject + if ($sig.Status -ne 'Valid') { throw "$($file.Name) is not validly signed" } + } + + - name: Upload signed installer + uses: actions/upload-artifact@v7 + with: + name: SteamlessController-Setup-${{ env.SIGNING_POLICY }} + path: signed\installer\SteamlessController-Setup.exe + + # A draft, so the notes get written and the installer tried before + # anyone can download it. + - name: Create draft release + if: startsWith(github.ref, 'refs/tags/') + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create '${{ github.ref_name }}' signed\installer\SteamlessController-Setup.exe ` + --draft --verify-tag ` + --title 'SteamlessController-${{ github.ref_name }}' ` + --notes 'Release notes to follow.' diff --git a/.signpath/artifact-configurations/exes.xml b/.signpath/artifact-configurations/exes.xml new file mode 100644 index 0000000..5587b0f --- /dev/null +++ b/.signpath/artifact-configurations/exes.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/.signpath/artifact-configurations/installer.xml b/.signpath/artifact-configurations/installer.xml new file mode 100644 index 0000000..5ed732f --- /dev/null +++ b/.signpath/artifact-configurations/installer.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/README.md b/README.md index fc4c534..d4a17bd 100644 --- a/README.md +++ b/README.md @@ -113,3 +113,21 @@ saved identity can take. ## Third-party - [ViGEmClient](https://github.com/nefarius/ViGEmClient) — MIT License, built from source as a static library + +## Code signing policy + +Free code signing provided by [SignPath.io](https://about.signpath.io), certificate by [SignPath Foundation](https://signpath.org). + +Releases are built and signed by the [Release workflow](.github/workflows/release.yml) from this repository's source. Only this project's own executables and installer are signed. The bundled ViGEmBus installer is signed by its own publisher and passes through unchanged. + +Team roles: + +- Committers and reviewers: [Dylan Deverill](https://github.com/ddeverill) +- Approvers: [Dylan Deverill](https://github.com/ddeverill) + +### Privacy + +SteamlessController sends no data about you or your system anywhere. It makes two kinds of network request: + +- The controller remapping window loads its fonts from Google Fonts, which receives the usual details of a web request, such as your IP address. +- If the ViGEmBus driver is missing, clicking the notification that says so opens its download page in your browser.