|
11 | 11 | runs-on: windows-latest |
12 | 12 | permissions: |
13 | 13 | contents: write |
| 14 | + id-token: write # required for the OIDC federated credential used by azure/login |
| 15 | + |
| 16 | + env: |
| 17 | + SIGNING_ENABLED: ${{ vars.AZURE_SIGNING_ENDPOINT != '' && 'true' || 'false' }} |
14 | 18 |
|
15 | 19 | steps: |
16 | 20 | - name: Checkout |
@@ -89,6 +93,139 @@ jobs: |
89 | 93 | /t:LANCommander_Interposer_Plugin_CDKey |
90 | 94 | /p:Configuration=Release /p:Platform=x86 |
91 | 95 |
|
| 96 | + # ── Authenticode signing (Azure Artifact Signing) ──────────────────────── |
| 97 | + # |
| 98 | + # Signs every PE output under Release\ in place, before any packaging step |
| 99 | + # runs. The redistributable payload ZIP and the LCX are both assembled by |
| 100 | + # copying out of Release\ further down, so signing here also covers the |
| 101 | + # copies embedded in those packages. The ZIP and LCX containers themselves |
| 102 | + # are not PE files and cannot be Authenticode signed. |
| 103 | + # |
| 104 | + # Required repository secrets (OIDC federated credential): |
| 105 | + # AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID |
| 106 | + # |
| 107 | + # Required repository variables: |
| 108 | + # AZURE_SIGNING_ENDPOINT e.g. https://eus.codesigning.azure.net |
| 109 | + # AZURE_SIGNING_ACCOUNT_NAME |
| 110 | + # AZURE_SIGNING_CERT_PROFILE_NAME |
| 111 | + # |
| 112 | + # Leaving AZURE_SIGNING_ENDPOINT unset disables signing: every step below is |
| 113 | + # skipped and the job still produces a complete (unsigned) release, so forks |
| 114 | + # without the Azure setup keep working. |
| 115 | + # |
| 116 | + # NOTE: if these secrets and variables are scoped to a GitHub *environment* |
| 117 | + # rather than the repository, they resolve empty here and signing is silently |
| 118 | + # skipped. Add a matching `environment:` key to this job in that case — but be |
| 119 | + # aware that gates the entire release job, not just signing. |
| 120 | + - name: Report signing configuration |
| 121 | + shell: pwsh |
| 122 | + run: | |
| 123 | + if ($env:SIGNING_ENABLED -eq 'true') { |
| 124 | + Write-Host "Signing release binaries" |
| 125 | + } else { |
| 126 | + Write-Host "::warning::AZURE_SIGNING_ENDPOINT is not set. Release binaries will be unsigned." |
| 127 | + } |
| 128 | +
|
| 129 | + - name: Collect Files to Sign |
| 130 | + id: targets |
| 131 | + if: env.SIGNING_ENABLED == 'true' |
| 132 | + shell: pwsh |
| 133 | + run: | |
| 134 | + $ErrorActionPreference = 'Stop' |
| 135 | +
|
| 136 | + # Every PE the release ships. LANCommander.Interposer.asi is a renamed |
| 137 | + # DLL — signtool dispatches on PE format, not file extension. |
| 138 | + $patterns = @( |
| 139 | + 'LANCommander.Interposer.dll' |
| 140 | + 'LANCommander.Interposer.Injector.exe' |
| 141 | + 'LANCommander.Interposer.Plugin.CDKey.dll' |
| 142 | + 'LANCommander.Interposer.asi' |
| 143 | + 'version.dll' |
| 144 | + 'dinput8.dll' |
| 145 | + ) |
| 146 | +
|
| 147 | + # The signing action expects absolute paths. Each build configuration |
| 148 | + # writes to its own subdirectory (Injector\, Proxy\, ProxyDInput8\, |
| 149 | + # ASI\, CDKey\), so the search has to recurse. |
| 150 | + $files = Get-ChildItem -Path 'Release' -Recurse -File -Include $patterns | |
| 151 | + Select-Object -ExpandProperty FullName | |
| 152 | + Sort-Object -Unique |
| 153 | +
|
| 154 | + if (-not $files) { |
| 155 | + throw "No files under Release\ matched the signing patterns. Refusing to sign nothing." |
| 156 | + } |
| 157 | +
|
| 158 | + Write-Host "Signing $($files.Count) file(s):" |
| 159 | + $files | ForEach-Object { Write-Host " $($_.Substring($PWD.Path.Length + 1))" } |
| 160 | +
|
| 161 | + # Persist the exact list so the verification step checks what was signed |
| 162 | + # rather than re-deriving it. |
| 163 | + $manifest = Join-Path $env:RUNNER_TEMP 'sign-targets.txt' |
| 164 | + $files | Set-Content -Path $manifest -Encoding UTF8 |
| 165 | +
|
| 166 | + "files<<SIGNTARGETS" | Out-File -FilePath $env:GITHUB_OUTPUT -Append |
| 167 | + $files | Out-File -FilePath $env:GITHUB_OUTPUT -Append |
| 168 | + "SIGNTARGETS" | Out-File -FilePath $env:GITHUB_OUTPUT -Append |
| 169 | +
|
| 170 | + # The signing action documents a .NET 8 runtime prerequisite, which is not |
| 171 | + # guaranteed to be present on every windows-latest image revision. |
| 172 | + - name: Setup .NET |
| 173 | + if: env.SIGNING_ENABLED == 'true' |
| 174 | + uses: actions/setup-dotnet@v4 |
| 175 | + with: |
| 176 | + dotnet-version: '8.0.x' |
| 177 | + |
| 178 | + - name: Azure Login |
| 179 | + if: env.SIGNING_ENABLED == 'true' |
| 180 | + uses: azure/login@v3 |
| 181 | + with: |
| 182 | + client-id: ${{ secrets.AZURE_CLIENT_ID }} |
| 183 | + tenant-id: ${{ secrets.AZURE_TENANT_ID }} |
| 184 | + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} |
| 185 | + |
| 186 | + - name: Sign Files |
| 187 | + if: env.SIGNING_ENABLED == 'true' |
| 188 | + uses: azure/artifact-signing-action@v2 |
| 189 | + with: |
| 190 | + endpoint: ${{ vars.AZURE_SIGNING_ENDPOINT }} |
| 191 | + signing-account-name: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} |
| 192 | + certificate-profile-name: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} |
| 193 | + files: ${{ steps.targets.outputs.files }} |
| 194 | + file-digest: SHA256 |
| 195 | + timestamp-rfc3161: http://timestamp.acs.microsoft.com |
| 196 | + timestamp-digest: SHA256 |
| 197 | + # Pin authentication to the session established by azure/login rather than |
| 198 | + # letting DefaultAzureCredential fall through a chain of unconfigured options. |
| 199 | + exclude-environment-credential: true |
| 200 | + exclude-azure-cli-credential: false |
| 201 | + |
| 202 | + - name: Verify Signatures |
| 203 | + if: env.SIGNING_ENABLED == 'true' |
| 204 | + shell: pwsh |
| 205 | + run: | |
| 206 | + $ErrorActionPreference = 'Stop' |
| 207 | +
|
| 208 | + $manifest = Join-Path $env:RUNNER_TEMP 'sign-targets.txt' |
| 209 | + $failed = @() |
| 210 | +
|
| 211 | + foreach ($path in Get-Content -Path $manifest) { |
| 212 | + $signature = Get-AuthenticodeSignature -FilePath $path |
| 213 | + $relative = $path.Substring($PWD.Path.Length + 1) |
| 214 | +
|
| 215 | + if ($signature.Status -ne 'Valid') { |
| 216 | + $failed += "$relative : $($signature.Status) - $($signature.StatusMessage)" |
| 217 | + } elseif (-not $signature.TimeStamperCertificate) { |
| 218 | + $failed += "$relative : signed but not timestamped" |
| 219 | + } else { |
| 220 | + Write-Host "$relative : Valid ($($signature.SignerCertificate.Subject))" |
| 221 | + } |
| 222 | + } |
| 223 | +
|
| 224 | + if ($failed) { |
| 225 | + $failed | ForEach-Object { Write-Host "::error::$_" } |
| 226 | + throw "$($failed.Count) file(s) failed signature verification." |
| 227 | + } |
| 228 | +
|
92 | 229 | - name: Create release ZIPs |
93 | 230 | shell: pwsh |
94 | 231 | run: | |
|
0 commit comments