diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81301da5..2924d34a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -295,6 +295,44 @@ jobs: set PATH=C:\Qt\5.15.2\msvc2019_64\bin;%PATH% nmake check TESTARGS="-maxwarnings 100000" + - name: Upload executables for signing + if: github.repository == 'YACReader/yacreader' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop') + uses: actions/upload-artifact@v4 + id: upload_executables + with: + name: windows-x64-executables-unsigned-${{ needs.initialization.outputs.build_number }} + path: | + release64/YACReader.exe + release64/YACReaderLibrary.exe + release64/YACReaderLibraryServer.exe + + - name: Sign executables with SignPath + if: github.repository == 'YACReader/yacreader' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop') + uses: signpath/github-action-submit-signing-request@v1 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} + project-slug: 'yacreader' + signing-policy-slug: 'release-signing' + artifact-configuration-slug: 'zipped-files' + github-artifact-id: ${{ steps.upload_executables.outputs.artifact-id }} + wait-for-completion: true + output-artifact-directory: release64/signed + + - name: Replace with signed executables + if: github.repository == 'YACReader/yacreader' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop') + shell: pwsh + run: | + Write-Host "=== Replacing executables with signed versions ===" + Get-ChildItem -Path "release64/signed" -Filter "*.exe" | ForEach-Object { + $destPath = "release64/$($_.Name)" + Write-Host "Moving signed: $($_.Name) -> $destPath" + Move-Item -Path $_.FullName -Destination $destPath -Force + Write-Host " Moved successfully" + } + Remove-Item -Path "release64/signed" -Recurse -Force -ErrorAction SilentlyContinue + Write-Host "Signed executables are ready for installer creation" + - name: Create installer shell: cmd working-directory: ci/win @@ -328,7 +366,7 @@ jobs: organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} project-slug: 'yacreader' signing-policy-slug: 'release-signing' - artifact-configuration-slug: 'windows-installer' + artifact-configuration-slug: 'zipped-files' github-artifact-id: ${{ steps.upload_unsigned.outputs.artifact-id }} wait-for-completion: true output-artifact-directory: ci/win/Output/signed @@ -421,6 +459,44 @@ jobs: set PATH=C:\Qt\6.3.1\msvc2019_64\bin;%PATH% nmake check TESTARGS="-maxwarnings 100000" + - name: Upload executables for signing + if: github.repository == 'YACReader/yacreader' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop') + uses: actions/upload-artifact@v4 + id: upload_executables + with: + name: windows-x64-qt6-executables-unsigned-${{ needs.initialization.outputs.build_number }} + path: | + release64/YACReader.exe + release64/YACReaderLibrary.exe + release64/YACReaderLibraryServer.exe + + - name: Sign executables with SignPath + if: github.repository == 'YACReader/yacreader' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop') + uses: signpath/github-action-submit-signing-request@v1 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} + project-slug: 'yacreader' + signing-policy-slug: 'release-signing' + artifact-configuration-slug: 'zipped-files' + github-artifact-id: ${{ steps.upload_executables.outputs.artifact-id }} + wait-for-completion: true + output-artifact-directory: release64/signed + + - name: Replace with signed executables + if: github.repository == 'YACReader/yacreader' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop') + shell: pwsh + run: | + Write-Host "=== Replacing executables with signed versions ===" + Get-ChildItem -Path "release64/signed" -Filter "*.exe" | ForEach-Object { + $destPath = "release64/$($_.Name)" + Write-Host "Moving signed: $($_.Name) -> $destPath" + Move-Item -Path $_.FullName -Destination $destPath -Force + Write-Host " Moved successfully" + } + Remove-Item -Path "release64/signed" -Recurse -Force -ErrorAction SilentlyContinue + Write-Host "Signed executables are ready for installer creation" + - name: Create installer shell: cmd working-directory: ci/win @@ -454,7 +530,7 @@ jobs: organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} project-slug: 'yacreader' signing-policy-slug: 'release-signing' - artifact-configuration-slug: 'windows-installer-qt6' + artifact-configuration-slug: 'zipped-files' github-artifact-id: ${{ steps.upload_unsigned.outputs.artifact-id }} wait-for-completion: true output-artifact-directory: ci/win/Output/signed @@ -531,6 +607,44 @@ jobs: set PATH=C:\Qt\5.15.2\msvc2019\bin;%PATH% nmake check TESTARGS="-maxwarnings 100000" + - name: Upload executables for signing + if: github.repository == 'YACReader/yacreader' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop') + uses: actions/upload-artifact@v4 + id: upload_executables + with: + name: windows-x86-executables-unsigned-${{ needs.initialization.outputs.build_number }} + path: | + release/YACReader.exe + release/YACReaderLibrary.exe + release/YACReaderLibraryServer.exe + + - name: Sign executables with SignPath + if: github.repository == 'YACReader/yacreader' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop') + uses: signpath/github-action-submit-signing-request@v1 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} + project-slug: 'yacreader' + signing-policy-slug: 'release-signing' + artifact-configuration-slug: 'zipped-files' + github-artifact-id: ${{ steps.upload_executables.outputs.artifact-id }} + wait-for-completion: true + output-artifact-directory: release/signed + + - name: Replace with signed executables + if: github.repository == 'YACReader/yacreader' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop') + shell: pwsh + run: | + Write-Host "=== Replacing executables with signed versions ===" + Get-ChildItem -Path "release/signed" -Filter "*.exe" | ForEach-Object { + $destPath = "release/$($_.Name)" + Write-Host "Moving signed: $($_.Name) -> $destPath" + Move-Item -Path $_.FullName -Destination $destPath -Force + Write-Host " Moved successfully" + } + Remove-Item -Path "release/signed" -Recurse -Force -ErrorAction SilentlyContinue + Write-Host "Signed executables are ready for installer creation" + - name: Create installer shell: cmd working-directory: ci/win @@ -564,7 +678,7 @@ jobs: organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} project-slug: 'yacreader' signing-policy-slug: 'release-signing' - artifact-configuration-slug: 'windows-installer-x86' + artifact-configuration-slug: 'zipped-files' github-artifact-id: ${{ steps.upload_unsigned.outputs.artifact-id }} wait-for-completion: true output-artifact-directory: ci/win/Output/signed diff --git a/ci/win/build_installer.iss b/ci/win/build_installer.iss index 350d2e8c..ef9880c5 100644 --- a/ci/win/build_installer.iss +++ b/ci/win/build_installer.iss @@ -15,6 +15,7 @@ SetupIconFile=setup.ico UninstallDisplayIcon=uninstall.ico ArchitecturesInstallIn64BitMode=x64 ArchitecturesAllowed=x64 +SignTool=custom_signtool [Registry] Root: HKCR; SubKey: .cbz; ValueType: string; ValueData: Comic Book (zip); Flags: uninsdeletekey; Tasks: File_association diff --git a/ci/win/build_installer_qt6.iss b/ci/win/build_installer_qt6.iss index 7f6bb39b..263c12a3 100644 --- a/ci/win/build_installer_qt6.iss +++ b/ci/win/build_installer_qt6.iss @@ -15,6 +15,7 @@ SetupIconFile=setup.ico UninstallDisplayIcon=uninstall.ico ArchitecturesInstallIn64BitMode=x64 ArchitecturesAllowed=x64 +SignTool=custom_signtool [Registry] Root: HKCR; SubKey: .cbz; ValueType: string; ValueData: Comic Book (zip); Flags: uninsdeletekey; Tasks: File_association diff --git a/ci/win/create_installer.cmd b/ci/win/create_installer.cmd index 65025292..fe94d3f8 100644 --- a/ci/win/create_installer.cmd +++ b/ci/win/create_installer.cmd @@ -59,9 +59,9 @@ if "%1"=="x86" ( echo "iscc start" if "%4"=="qt6" ( - iscc /DVERSION=%VERSION% /DPLATFORM=%1 /DCOMPRESSED_ARCHIVE_BACKEND=%2 /DBUILD_NUMBER=%3 build_installer_qt6.iss || exit /b + iscc /DVERSION=%VERSION% /DPLATFORM=%1 /DCOMPRESSED_ARCHIVE_BACKEND=%2 /DBUILD_NUMBER=%3 /Scustom_signtool="powershell.exe -ExecutionPolicy Bypass -File sign_tool.ps1 $f" build_installer_qt6.iss || exit /b ) else ( - iscc /DVERSION=%VERSION% /DPLATFORM=%1 /DCOMPRESSED_ARCHIVE_BACKEND=%2 /DBUILD_NUMBER=%3 build_installer.iss || exit /b + iscc /DVERSION=%VERSION% /DPLATFORM=%1 /DCOMPRESSED_ARCHIVE_BACKEND=%2 /DBUILD_NUMBER=%3 /Scustom_signtool="powershell.exe -ExecutionPolicy Bypass -File sign_tool.ps1 $f" build_installer.iss || exit /b ) echo "iscc done!" diff --git a/ci/win/sign_tool.ps1 b/ci/win/sign_tool.ps1 new file mode 100644 index 00000000..b3e634c0 --- /dev/null +++ b/ci/win/sign_tool.ps1 @@ -0,0 +1,181 @@ +# PowerShell script for signing uninstaller during Inno Setup process +param( + [Parameter(Mandatory=$true)] + [string]$FilePath +) + +# Get the filename from the full path +$fileName = Split-Path $FilePath -Leaf +Write-Host "SignTool called for file: $fileName" +Write-Host "Full file path: $FilePath" + +# Validate the file exists +if (-not (Test-Path $FilePath)) { + Write-Error "File not found: $FilePath" + exit 1 +} + +# Only sign the uninstaller - all other executables are pre-signed +if ($fileName -eq "unins000.exe") { + Write-Host "Signing uninstaller: $fileName" + + # Check if we're in a CI environment (GitHub Actions) + $isCI = $env:GITHUB_ACTIONS -eq "true" + + if ($isCI) { + Write-Host "Running in CI environment - calling SignPath API" + Write-Host "File path: $FilePath" + Write-Host "File size: $((Get-Item $FilePath).Length) bytes" + + # Get required environment variables (these should be set as GitHub secrets) + $organizationId = $env:SIGNPATH_ORGANIZATION_ID + $projectSlug = $env:SIGNPATH_PROJECT_SLUG + $signingPolicySlug = $env:SIGNPATH_SIGNING_POLICY_SLUG + $apiToken = $env:SIGNPATH_API_TOKEN + + # Validate required environment variables are set + if (-not $organizationId) { + Write-Error "SIGNPATH_ORGANIZATION_ID environment variable not set" + exit 1 + } + if (-not $projectSlug) { + Write-Error "SIGNPATH_PROJECT_SLUG environment variable not set" + exit 1 + } + if (-not $signingPolicySlug) { + Write-Error "SIGNPATH_SIGNING_POLICY_SLUG environment variable not set" + exit 1 + } + if (-not $apiToken) { + Write-Error "SIGNPATH_API_TOKEN environment variable not set" + exit 1 + } + + # Initialize temp directory variable for proper cleanup + $tempDir = $null + + try { + # Create a temporary directory for the signing process + $tempDir = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "signpath_$(Get-Random)") -Force + $tempFile = Join-Path $tempDir "unins000.exe" + + Write-Host "Created temp directory: $tempDir" + + # Copy file to temp location with error handling + Copy-Item $FilePath $tempFile -Force + Write-Host "Copied file to temp location: $tempFile" + Write-Host "Copied file to temp location: $tempFile" + + Write-Host "Uploading uninstaller to SignPath for signing..." + + # Create the API request + $uri = "https://app.signpath.io/api/v1/$organizationId/SigningRequests" + + # Prepare the form data + $form = @{ + 'ProjectSlug' = $projectSlug + 'SigningPolicySlug' = $signingPolicySlug + 'ArtifactConfigurationSlug' = 'executable-file' + 'Description' = "Signing uninstaller for YACReader build $(Get-Date -Format 'yyyy-MM-dd HH:mm')" + } + + # Prepare headers (API token should never be logged) + $headers = @{ + 'Authorization' = "Bearer $apiToken" + } + + # Upload and sign with error handling + Write-Host "Submitting signing request..." + $response = Invoke-RestMethod -Uri $uri -Method Post -Form $form -InFile $tempFile -Headers $headers + + # Validate response has required fields + if (-not $response.signingRequestId) { + Write-Error "SignPath API response missing signingRequestId field" + exit 1 + } + + $signingRequestId = $response.signingRequestId + Write-Host "Signing request submitted with ID: $signingRequestId" + + # Poll for completion + $statusUri = "https://app.signpath.io/api/v1/$organizationId/SigningRequests/$signingRequestId" + $maxWaitTime = 300 # 5 minutes max + $waitTime = 0 + $pollInterval = 10 + + do { + Start-Sleep $pollInterval + $waitTime += $pollInterval + + Write-Host "Checking signing status... ($waitTime/$maxWaitTime seconds)" + $status = Invoke-RestMethod -Uri $statusUri -Headers $headers + + # Validate status response + if (-not $status.status) { + Write-Error "SignPath status response missing status field" + exit 1 + } + + if ($status.status -eq "Completed") { + Write-Host "Signing completed successfully!" + break + } elseif ($status.status -eq "Failed") { + $description = if ($status.description) { $status.description } else { "Unknown error" } + Write-Error "Signing failed: $description" + exit 1 + } elseif ($status.status -eq "Denied") { + $description = if ($status.description) { $status.description } else { "Request denied" } + Write-Error "Signing was denied: $description" + exit 1 + } + + } while ($waitTime -lt $maxWaitTime) + + if ($waitTime -ge $maxWaitTime) { + Write-Error "Signing timed out after $maxWaitTime seconds" + exit 1 + } + + # Download the signed file + Write-Host "Downloading signed uninstaller..." + $downloadUri = "https://app.signpath.io/api/v1/$organizationId/SigningRequests/$signingRequestId/SignedArtifact" + + # Download to temp location first, then replace original + $signedTempFile = Join-Path $tempDir "unins000_signed.exe" + Invoke-RestMethod -Uri $downloadUri -Headers $headers -OutFile $signedTempFile + + # Verify the signed file exists and has content + if (-not (Test-Path $signedTempFile) -or (Get-Item $signedTempFile).Length -eq 0) { + Write-Error "Downloaded signed file is missing or empty" + exit 1 + } + + # Replace the original file with the signed version + Copy-Item $signedTempFile $FilePath -Force + Write-Host "Uninstaller signed successfully and replaced at: $FilePath" + + # Clean up temp directory + Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue + Write-Host "Cleaned up temporary files" + + } catch { + Write-Error "SignPath API call failed: $($_.Exception.Message)" + Write-Host "Error details: $($_.Exception.ToString())" + + # Clean up temp directory on error + if ($tempDir -and (Test-Path $tempDir)) { + Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue + Write-Host "Cleaned up temp directory after error" + } + exit 1 + } + + } else { + Write-Host "Running locally - skipping uninstaller signing" + Write-Host "Set GITHUB_ACTIONS=true to enable SignPath integration" + exit 0 + } +} else { + Write-Host "Skipping file: $fileName (not the uninstaller)" + exit 0 +} \ No newline at end of file