mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Tauri sign fixes for security alerts (#6122)
This commit is contained in:
@@ -21,6 +21,14 @@ on:
|
|||||||
- windows
|
- windows
|
||||||
- macos
|
- macos
|
||||||
- linux
|
- linux
|
||||||
|
sign:
|
||||||
|
description: "Code sign the binaries (requires signing secrets)"
|
||||||
|
required: false
|
||||||
|
default: "true"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- "true"
|
||||||
|
- "false"
|
||||||
release:
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
@@ -144,6 +152,9 @@ jobs:
|
|||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: frontend/package-lock.json
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Install Task
|
||||||
|
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||||
|
|
||||||
- name: Build JAR
|
- name: Build JAR
|
||||||
run: ./gradlew build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
run: ./gradlew build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||||
env:
|
env:
|
||||||
@@ -219,88 +230,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.3.1
|
||||||
|
|
||||||
- name: Build Java backend with JLink
|
- name: Install Task
|
||||||
working-directory: ./
|
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
chmod +x ./gradlew
|
|
||||||
echo "🔧 Building Stirling-PDF JAR..."
|
|
||||||
./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
|
||||||
|
|
||||||
# Find the built JAR
|
- name: Prepare desktop build
|
||||||
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
|
run: task desktop:prepare
|
||||||
echo "✅ Built JAR: $STIRLING_JAR"
|
|
||||||
|
|
||||||
# Create Tauri directories
|
|
||||||
mkdir -p ./frontend/src-tauri/libs
|
|
||||||
mkdir -p ./frontend/src-tauri/runtime
|
|
||||||
|
|
||||||
# Copy JAR to Tauri libs
|
|
||||||
cp "$STIRLING_JAR" ./frontend/src-tauri/libs/
|
|
||||||
echo "✅ JAR copied to Tauri libs"
|
|
||||||
|
|
||||||
# Analyze JAR dependencies for jlink modules
|
|
||||||
echo "🔍 Analyzing JAR dependencies..."
|
|
||||||
if command -v jdeps &> /dev/null; then
|
|
||||||
DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "")
|
|
||||||
if [ -n "$DETECTED_MODULES" ]; then
|
|
||||||
echo "📋 jdeps detected modules: $DETECTED_MODULES"
|
|
||||||
MODULES="$DETECTED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
|
||||||
else
|
|
||||||
echo "⚠️ jdeps analysis failed, using predefined modules"
|
|
||||||
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "⚠️ jdeps not available, using predefined modules"
|
|
||||||
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create custom JRE with jlink
|
|
||||||
echo "🔧 Creating custom JRE with jlink..."
|
|
||||||
echo "📋 Using modules: $MODULES"
|
|
||||||
|
|
||||||
# Remove any existing JRE
|
|
||||||
rm -rf ./frontend/src-tauri/runtime/jre
|
|
||||||
|
|
||||||
# Create the custom JRE
|
|
||||||
jlink \
|
|
||||||
--add-modules "$MODULES" \
|
|
||||||
--strip-debug \
|
|
||||||
--compress=2 \
|
|
||||||
--no-header-files \
|
|
||||||
--no-man-pages \
|
|
||||||
--output ./frontend/src-tauri/runtime/jre
|
|
||||||
|
|
||||||
if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then
|
|
||||||
echo "❌ Failed to create JLink runtime"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test the bundled runtime
|
|
||||||
if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then
|
|
||||||
RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1)
|
|
||||||
echo "✅ Custom JRE created successfully: $RUNTIME_VERSION"
|
|
||||||
else
|
|
||||||
echo "❌ Custom JRE executable not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Calculate runtime size
|
|
||||||
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
|
|
||||||
echo "📊 Custom JRE size: $RUNTIME_SIZE"
|
|
||||||
env:
|
env:
|
||||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||||
DISABLE_ADDITIONAL_FEATURES: true
|
DISABLE_ADDITIONAL_FEATURES: true
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
|
||||||
run: task frontend:install
|
|
||||||
|
|
||||||
# DigiCert KeyLocker Setup (Cloud HSM)
|
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||||
- name: Setup DigiCert KeyLocker
|
- name: Setup DigiCert KeyLocker
|
||||||
id: digicert-setup
|
id: digicert-setup
|
||||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||||
uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
|
uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
|
||||||
env:
|
env:
|
||||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||||
@@ -310,7 +254,7 @@ jobs:
|
|||||||
SM_HOST: ${{ secrets.SM_HOST }}
|
SM_HOST: ${{ secrets.SM_HOST }}
|
||||||
|
|
||||||
- name: Setup DigiCert KeyLocker Certificate
|
- name: Setup DigiCert KeyLocker Certificate
|
||||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
Write-Host "Setting up DigiCert KeyLocker environment..."
|
Write-Host "Setting up DigiCert KeyLocker environment..."
|
||||||
@@ -345,7 +289,7 @@ jobs:
|
|||||||
|
|
||||||
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
|
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
|
||||||
- name: Import Windows Code Signing Certificate
|
- name: Import Windows Code Signing Certificate
|
||||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||||
env:
|
env:
|
||||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||||
@@ -376,7 +320,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
- name: Import Apple Developer Certificate
|
- name: Import Apple Developer Certificate
|
||||||
if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master')
|
if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
||||||
env:
|
env:
|
||||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
@@ -397,7 +341,7 @@ jobs:
|
|||||||
rm certificate.p12
|
rm certificate.p12
|
||||||
|
|
||||||
- name: Verify Certificate
|
- name: Verify Certificate
|
||||||
if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master')
|
if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
||||||
run: |
|
run: |
|
||||||
echo "Verifying Apple Developer Certificate..."
|
echo "Verifying Apple Developer Certificate..."
|
||||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
@@ -408,6 +352,62 @@ jobs:
|
|||||||
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
|
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
|
||||||
echo "Certificate imported successfully."
|
echo "Certificate imported successfully."
|
||||||
|
|
||||||
|
# Pre-flight: verify smctl can talk to DigiCert and sync cert before we sign.
|
||||||
|
# Mirrors the setup from working public Tauri+KeyLocker repos (Labric, Meetily).
|
||||||
|
# Without this, signCommand failures are opaque (Tauri captures but drops
|
||||||
|
# smctl's stderr) - running these loudly surfaces auth/env/keypair issues.
|
||||||
|
- name: Preflight smctl
|
||||||
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||||
|
run: |
|
||||||
|
& smctl healthcheck
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl healthcheck failed"; exit 1 }
|
||||||
|
& smctl keypair ls
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl keypair ls failed"; exit 1 }
|
||||||
|
& smctl windows certsync --keypair-alias "$env:KEYPAIR_ALIAS"
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] smctl windows certsync returned non-zero - continuing" }
|
||||||
|
Write-Host "[SUCCESS] smctl preflight passed"
|
||||||
|
|
||||||
|
# Write platform-specific Tauri config that adds signCommand for Windows.
|
||||||
|
# Tauri auto-merges tauri.windows.conf.json with tauri.conf.json (RFC 7396).
|
||||||
|
# Tauri calls this command on every binary BEFORE bundling into the MSI,
|
||||||
|
# substituting %1 with the file path.
|
||||||
|
#
|
||||||
|
# Why OBJECT form (cmd + args) instead of string:
|
||||||
|
# Tauri's string-form parser does a naive split(' ') with no shell/quote handling.
|
||||||
|
# Args with spaces or quote characters get mangled. The object form passes each
|
||||||
|
# arg directly to Rust's Command::arg which handles Windows CreateProcess quoting.
|
||||||
|
#
|
||||||
|
# Why --keypair-alias instead of --fingerprint:
|
||||||
|
# --fingerprint requires smctl windows certsync to have synced the cert to the
|
||||||
|
# Windows cert store first. --keypair-alias goes direct through PKCS11 and works
|
||||||
|
# without certsync. All real-world working Tauri+smctl examples use this flag.
|
||||||
|
#
|
||||||
|
# smctl reads SM_HOST, SM_API_KEY, SM_CLIENT_CERT_FILE, SM_CLIENT_CERT_PASSWORD
|
||||||
|
# from env (set by prior DigiCert setup step). No --config-file needed.
|
||||||
|
- name: Configure Windows code signing
|
||||||
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||||
|
run: |
|
||||||
|
cat > ./frontend/src-tauri/tauri.windows.conf.json <<EOF
|
||||||
|
{
|
||||||
|
"bundle": {
|
||||||
|
"windows": {
|
||||||
|
"signCommand": {
|
||||||
|
"cmd": "smctl",
|
||||||
|
"args": ["sign", "--keypair-alias", "${KEYPAIR_ALIAS}", "--input", "%1", "--verbose"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
echo "Generated tauri.windows.conf.json (alias masked):"
|
||||||
|
sed "s/${KEYPAIR_ALIAS}/***/g" ./frontend/src-tauri/tauri.windows.conf.json
|
||||||
|
|
||||||
- name: Build Tauri app
|
- name: Build Tauri app
|
||||||
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
|
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
|
||||||
env:
|
env:
|
||||||
@@ -424,108 +424,92 @@ jobs:
|
|||||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }}
|
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }}
|
||||||
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
|
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
|
||||||
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
|
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
|
||||||
# Only enable Windows signing in Tauri when on release or V2-master
|
# DigiCert KeyLocker env vars consumed by smctl during signCommand
|
||||||
SIGN: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
|
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
|
||||||
CI: true
|
CI: true
|
||||||
with:
|
with:
|
||||||
projectPath: ./frontend
|
projectPath: ./frontend
|
||||||
tauriScript: npx tauri
|
tauriScript: npx tauri
|
||||||
args: ${{ matrix.args }}
|
args: ${{ matrix.args }}
|
||||||
|
|
||||||
# Sign with DigiCert KeyLocker (post-build)
|
# Verify the MSI (outer wrapper users download) AND the inner exe extracted
|
||||||
- name: Sign Windows binaries with DigiCert KeyLocker
|
# from it (what actually gets installed and what AV scans). We don't check
|
||||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
|
# target/.../release/stirling-pdf.exe - that's Tauri's intermediate build
|
||||||
|
# artifact. Tauri signs a COPY when bundling into the MSI and leaves the raw
|
||||||
|
# cargo output unsigned, so checking it produces false negatives.
|
||||||
|
- name: Verify Windows Code Signature
|
||||||
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
Write-Host "=== DigiCert KeyLocker Signing ==="
|
$allSigned = $true
|
||||||
|
|
||||||
# Test smctl connectivity first
|
# Check MSI installer (outer wrapper - what users download)
|
||||||
Write-Host "Testing smctl connection..."
|
|
||||||
$healthCheck = & smctl healthcheck 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) {
|
|
||||||
Write-Host "[SUCCESS] Connected to DigiCert KeyLocker"
|
|
||||||
} else {
|
|
||||||
Write-Host "[ERROR] Failed to connect to DigiCert KeyLocker"
|
|
||||||
Write-Host $healthCheck
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# Sync certificates to Windows certificate store
|
|
||||||
Write-Host "Syncing certificates to Windows certificate store..."
|
|
||||||
$syncOutput = & smctl windows certsync 2>&1
|
|
||||||
Write-Host "Cert sync result: $syncOutput"
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# Find only the files we need to sign
|
|
||||||
$filesToSign = @()
|
|
||||||
|
|
||||||
# Main application executable
|
|
||||||
$mainExe = Get-ChildItem -Path "./frontend/src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "stirling-pdf.exe" -File -ErrorAction SilentlyContinue
|
|
||||||
if ($mainExe) { $filesToSign += $mainExe }
|
|
||||||
|
|
||||||
# MSI installer
|
|
||||||
$msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File
|
$msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File
|
||||||
$filesToSign += $msiFiles
|
if ($msiFiles.Count -eq 0) {
|
||||||
|
Write-Host "[ERROR] No MSI found under target/"
|
||||||
if ($filesToSign.Count -eq 0) {
|
|
||||||
Write-Host "[ERROR] No files found to sign"
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
foreach ($msi in $msiFiles) {
|
||||||
Write-Host "Found $($filesToSign.Count) files to sign:"
|
$sig = Get-AuthenticodeSignature -FilePath $msi.FullName
|
||||||
foreach ($f in $filesToSign) { Write-Host " - $($f.Name)" }
|
Write-Host "MSI: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
||||||
Write-Host ""
|
if ($sig.Status -ne "Valid") {
|
||||||
|
Write-Host "[ERROR] MSI is not signed"
|
||||||
$signedCount = 0
|
$allSigned = $false
|
||||||
foreach ($file in $filesToSign) {
|
|
||||||
Write-Host "Signing: $($file.Name)"
|
|
||||||
|
|
||||||
# Get PKCS11 config file path
|
|
||||||
$pkcs11Config = $env:PKCS11_CONFIG
|
|
||||||
if (-not $pkcs11Config) {
|
|
||||||
Write-Host "[ERROR] PKCS11_CONFIG environment variable not set"
|
|
||||||
exit 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Using PKCS11 config: $pkcs11Config"
|
|
||||||
|
|
||||||
# Try signing with certificate fingerprint first (if available)
|
|
||||||
$fingerprint = "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}"
|
|
||||||
if ($fingerprint -and $fingerprint -ne "") {
|
|
||||||
Write-Host "Attempting to sign with certificate fingerprint..."
|
|
||||||
$output = & smctl sign --fingerprint "$fingerprint" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
|
|
||||||
$exitCode = $LASTEXITCODE
|
|
||||||
} else {
|
|
||||||
Write-Host "No fingerprint provided, using keypair alias..."
|
|
||||||
$output = & smctl sign --keypair-alias "${{ secrets.SM_KEYPAIR_ALIAS }}" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
|
|
||||||
$exitCode = $LASTEXITCODE
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Exit code: $exitCode"
|
|
||||||
Write-Host "Output: $output"
|
|
||||||
|
|
||||||
if ($output -match "FAILED" -or $output -match "error" -or $output -match "Error") {
|
|
||||||
Write-Host "[ERROR] Signing failed for $($file.Name)"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($exitCode -ne 0) {
|
|
||||||
Write-Host "[ERROR] Failed to sign $($file.Name)"
|
|
||||||
Write-Host "Full error output:"
|
|
||||||
Write-Host $output
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$signedCount++
|
|
||||||
Write-Host "[SUCCESS] Signed: $($file.Name)"
|
|
||||||
Write-Host ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "=== Summary ==="
|
# Extract MSI and verify the inner exe (the file that actually gets installed).
|
||||||
Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully"
|
# This is the critical check - AV flags the installed exe at runtime.
|
||||||
|
$msi = $msiFiles[0].FullName
|
||||||
|
$extractDir = Join-Path $env:RUNNER_TEMP "msi-verify"
|
||||||
|
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
|
||||||
|
$proc = Start-Process msiexec.exe -ArgumentList '/a', $msi, '/qn', "TARGETDIR=$extractDir" -Wait -PassThru -NoNewWindow
|
||||||
|
if ($proc.ExitCode -eq 0) {
|
||||||
|
$innerExe = Get-ChildItem -Path $extractDir -Filter "stirling-pdf.exe" -Recurse -File | Select-Object -First 1
|
||||||
|
if ($innerExe) {
|
||||||
|
$sig = Get-AuthenticodeSignature -FilePath $innerExe.FullName
|
||||||
|
Write-Host "Inner EXE (from MSI): Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
||||||
|
if ($sig.Status -ne "Valid") {
|
||||||
|
Write-Host "[ERROR] Inner exe extracted from MSI is NOT signed - AV will flag this at runtime"
|
||||||
|
$allSigned = $false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "[ERROR] Could not find stirling-pdf.exe inside MSI"
|
||||||
|
$allSigned = $false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "[ERROR] Failed to extract MSI for verification (exit code: $($proc.ExitCode))"
|
||||||
|
$allSigned = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $allSigned) {
|
||||||
|
Write-Host "[ERROR] Signature verification failed"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "[SUCCESS] MSI and installed exe are properly signed"
|
||||||
|
|
||||||
|
# Dump smctl log files on failure. Tauri's signCommand captures smctl output
|
||||||
|
# but drops stderr when the command exits non-zero, making failures opaque.
|
||||||
|
# The real errors live in smctl's log files - surface them here for debugging.
|
||||||
|
- name: Dump smctl logs on failure
|
||||||
|
if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$logDir = "$env:USERPROFILE\.signingmanager\logs"
|
||||||
|
if (Test-Path $logDir) {
|
||||||
|
Get-ChildItem $logDir | ForEach-Object {
|
||||||
|
Write-Host "=== $($_.FullName) ==="
|
||||||
|
Get-Content $_.FullName -Tail 200
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "smctl log directory not found at $logDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rename + Upload: use always() so artifacts are still collected when verify
|
||||||
|
# fails - we need them to manually inspect what actually came out of the build.
|
||||||
- name: Rename artifacts
|
- name: Rename artifacts
|
||||||
|
if: always() && steps.digicert-setup.conclusion != 'failure'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./dist
|
mkdir -p ./dist
|
||||||
@@ -533,7 +517,8 @@ jobs:
|
|||||||
|
|
||||||
# Find and rename artifacts based on platform
|
# Find and rename artifacts based on platform
|
||||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||||
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
|
# Only ship the MSI installer on Windows. The loose exe and WiX toolset exes
|
||||||
|
# are not the user-facing installer - the MSI contains the signed inner exe.
|
||||||
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
|
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
|
||||||
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
|
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
|
||||||
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||||
@@ -544,6 +529,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
|
if: always() && steps.digicert-setup.conclusion != 'failure'
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: Stirling-PDF-${{ matrix.name }}
|
name: Stirling-PDF-${{ matrix.name }}
|
||||||
|
|||||||
@@ -260,6 +260,38 @@ jobs:
|
|||||||
echo "Available tools:"
|
echo "Available tools:"
|
||||||
ls -la /usr/bin/hd* || echo "No hd* tools found"
|
ls -la /usr/bin/hd* || echo "No hd* tools found"
|
||||||
|
|
||||||
|
- name: Preflight smctl
|
||||||
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||||
|
run: |
|
||||||
|
& smctl healthcheck
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl healthcheck failed"; exit 1 }
|
||||||
|
& smctl keypair ls
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl keypair ls failed"; exit 1 }
|
||||||
|
& smctl windows certsync --keypair-alias "$env:KEYPAIR_ALIAS"
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] smctl windows certsync returned non-zero - continuing" }
|
||||||
|
|
||||||
|
- name: Configure Windows code signing
|
||||||
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||||
|
run: |
|
||||||
|
cat > ./frontend/src-tauri/tauri.windows.conf.json <<EOF
|
||||||
|
{
|
||||||
|
"bundle": {
|
||||||
|
"windows": {
|
||||||
|
"signCommand": {
|
||||||
|
"cmd": "smctl",
|
||||||
|
"args": ["sign", "--keypair-alias", "${KEYPAIR_ALIAS}", "--input", "%1", "--verbose"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Build Tauri app
|
- name: Build Tauri app
|
||||||
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
|
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
|
||||||
env:
|
env:
|
||||||
@@ -276,173 +308,13 @@ jobs:
|
|||||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }}
|
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }}
|
||||||
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
|
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
|
||||||
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
|
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
|
||||||
# Only enable Windows signing in Tauri when on main
|
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
|
||||||
SIGN: ${{ github.ref == 'refs/heads/main' && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
|
|
||||||
CI: true
|
CI: true
|
||||||
with:
|
with:
|
||||||
projectPath: ./frontend
|
projectPath: ./frontend
|
||||||
tauriScript: npx tauri
|
tauriScript: npx tauri
|
||||||
args: ${{ matrix.args }}
|
args: ${{ matrix.args }}
|
||||||
|
|
||||||
# Sign with DigiCert KeyLocker (post-build)
|
|
||||||
- name: Sign Windows binaries with DigiCert KeyLocker
|
|
||||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
Write-Host "=== DigiCert KeyLocker Signing ==="
|
|
||||||
|
|
||||||
# Test smctl connectivity first
|
|
||||||
Write-Host "Testing smctl connection..."
|
|
||||||
$healthCheck = & smctl healthcheck 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) {
|
|
||||||
Write-Host "[SUCCESS] Connected to DigiCert KeyLocker"
|
|
||||||
} else {
|
|
||||||
Write-Host "[ERROR] Failed to connect to DigiCert KeyLocker"
|
|
||||||
Write-Host $healthCheck
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# Sync certificates to Windows certificate store
|
|
||||||
Write-Host "Syncing certificates to Windows certificate store..."
|
|
||||||
$syncOutput = & smctl windows certsync 2>&1
|
|
||||||
Write-Host "Cert sync result: $syncOutput"
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# List available certificates and check if they have certificates attached
|
|
||||||
Write-Host "Checking for available certificates..."
|
|
||||||
$certList = & smctl keypair ls 2>&1
|
|
||||||
Write-Host "Keypair list output:"
|
|
||||||
Write-Host $certList
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# Parse the output to check certificate status
|
|
||||||
$lines = $certList -split "`n"
|
|
||||||
$foundKeypair = $false
|
|
||||||
$hasCertificate = $false
|
|
||||||
|
|
||||||
foreach ($line in $lines) {
|
|
||||||
if ($line -match "${{ secrets.SM_KEYPAIR_ALIAS }}") {
|
|
||||||
$foundKeypair = $true
|
|
||||||
Write-Host "[SUCCESS] Found keypair in list"
|
|
||||||
|
|
||||||
# Check if this line has certificate info (not just empty spaces after alias)
|
|
||||||
$parts = $line -split "\s+"
|
|
||||||
if ($parts.Count -gt 2 -and $parts[1] -ne "" -and $parts[1] -ne "CERTIFICATE") {
|
|
||||||
$hasCertificate = $true
|
|
||||||
Write-Host "[SUCCESS] Certificate is associated with keypair"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $foundKeypair) {
|
|
||||||
Write-Host "[ERROR] Keypair not found: ${{ secrets.SM_KEYPAIR_ALIAS }}"
|
|
||||||
Write-Host "Available keypairs are listed above"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Please verify:"
|
|
||||||
Write-Host " 1. Keypair alias is correct in GitHub secret"
|
|
||||||
Write-Host " 2. API key has access to this keypair"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $hasCertificate) {
|
|
||||||
Write-Host "[ERROR] No certificate associated with keypair"
|
|
||||||
Write-Host "This usually means:"
|
|
||||||
Write-Host " 1. Certificate not yet synced to KeyLocker (run sync manually)"
|
|
||||||
Write-Host " 2. Certificate is pending approval"
|
|
||||||
Write-Host " 3. Certificate needs to be attached to the keypair"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Try running in DigiCert ONE portal:"
|
|
||||||
Write-Host " smctl keypair sync"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "[SUCCESS] Certificate check passed"
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# Find only the files we need to sign (not build scripts)
|
|
||||||
$filesToSign = @()
|
|
||||||
|
|
||||||
# Main application executable
|
|
||||||
$mainExe = Get-ChildItem -Path "./frontend/src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "stirling-pdf.exe" -File -ErrorAction SilentlyContinue
|
|
||||||
if ($mainExe) { $filesToSign += $mainExe }
|
|
||||||
|
|
||||||
# MSI installer
|
|
||||||
$msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File
|
|
||||||
$filesToSign += $msiFiles
|
|
||||||
|
|
||||||
if ($filesToSign.Count -eq 0) {
|
|
||||||
Write-Host "[ERROR] No files found to sign"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Found $($filesToSign.Count) files to sign:"
|
|
||||||
foreach ($f in $filesToSign) { Write-Host " - $($f.Name)" }
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
$signedCount = 0
|
|
||||||
foreach ($file in $filesToSign) {
|
|
||||||
Write-Host "Signing: $($file.Name)"
|
|
||||||
|
|
||||||
# Get PKCS11 config file path (set by DigiCert action)
|
|
||||||
$pkcs11Config = $env:PKCS11_CONFIG
|
|
||||||
if (-not $pkcs11Config) {
|
|
||||||
Write-Host "[ERROR] PKCS11_CONFIG environment variable not set"
|
|
||||||
Write-Host "DigiCert KeyLocker action may not have run correctly"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Using PKCS11 config: $pkcs11Config"
|
|
||||||
|
|
||||||
# Try signing with certificate fingerprint first (if available)
|
|
||||||
$fingerprint = "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}"
|
|
||||||
if ($fingerprint -and $fingerprint -ne "") {
|
|
||||||
Write-Host "Attempting to sign with certificate fingerprint..."
|
|
||||||
$output = & smctl sign --fingerprint "$fingerprint" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
|
|
||||||
$exitCode = $LASTEXITCODE
|
|
||||||
} else {
|
|
||||||
Write-Host "No fingerprint provided, using keypair alias..."
|
|
||||||
# Use smctl to sign with keypair alias
|
|
||||||
$output = & smctl sign --keypair-alias "${{ secrets.SM_KEYPAIR_ALIAS }}" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
|
|
||||||
$exitCode = $LASTEXITCODE
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Exit code: $exitCode"
|
|
||||||
Write-Host "Output: $output"
|
|
||||||
|
|
||||||
# Check if output contains "FAILED" even with exit code 0
|
|
||||||
if ($output -match "FAILED" -or $output -match "error" -or $output -match "Error") {
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "[ERROR] Signing failed for $($file.Name)"
|
|
||||||
Write-Host "[ERROR] smctl returned success but output indicates failure"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Possible issues:"
|
|
||||||
Write-Host " 1. Certificate not fully synced to KeyLocker (wait a few minutes)"
|
|
||||||
Write-Host " 2. Incorrect keypair alias"
|
|
||||||
Write-Host " 3. API key lacks signing permissions"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Please verify in DigiCert ONE portal:"
|
|
||||||
Write-Host " - Certificate status is 'Issued' (not Pending)"
|
|
||||||
Write-Host " - Keypair status is 'Online'"
|
|
||||||
Write-Host " - 'Can sign' is set to 'Yes'"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($exitCode -ne 0) {
|
|
||||||
Write-Host "[ERROR] Failed to sign $($file.Name)"
|
|
||||||
Write-Host "Full error output:"
|
|
||||||
Write-Host $output
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$signedCount++
|
|
||||||
Write-Host "[SUCCESS] Signed: $($file.Name)"
|
|
||||||
Write-Host ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "=== Summary ==="
|
|
||||||
Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully"
|
|
||||||
|
|
||||||
- name: Verify notarization (macOS only)
|
- name: Verify notarization (macOS only)
|
||||||
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
||||||
run: |
|
run: |
|
||||||
@@ -466,7 +338,8 @@ jobs:
|
|||||||
|
|
||||||
# Find and rename artifacts based on platform
|
# Find and rename artifacts based on platform
|
||||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||||
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
|
# Only ship the MSI installer. The loose exe and WiX toolset exes
|
||||||
|
# are not the user-facing installer - the MSI contains the signed inner exe.
|
||||||
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
|
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
|
||||||
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
|
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
|
||||||
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||||
@@ -475,64 +348,69 @@ jobs:
|
|||||||
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Verify the MSI AND the inner exe extracted from it are signed.
|
||||||
|
# The inner exe is what gets installed on users' machines and what AV scans.
|
||||||
- name: Verify Windows Code Signature
|
- name: Verify Windows Code Signature
|
||||||
if: matrix.platform == 'windows-latest' && github.ref == 'refs/heads/main'
|
if: matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main'
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
Write-Host "Verifying Windows code signatures..."
|
$allSigned = $true
|
||||||
|
|
||||||
$exePath = "./dist/Stirling-PDF-${{ matrix.name }}.exe"
|
|
||||||
$msiPath = "./dist/Stirling-PDF-${{ matrix.name }}.msi"
|
$msiPath = "./dist/Stirling-PDF-${{ matrix.name }}.msi"
|
||||||
|
|
||||||
$allSigned = $true
|
# Check MSI (outer wrapper)
|
||||||
$usingKeyLocker = "${{ env.SM_API_KEY }}" -ne ""
|
|
||||||
$usingPfx = "${{ env.WINDOWS_CERTIFICATE }}" -ne ""
|
|
||||||
|
|
||||||
# Check EXE signature
|
|
||||||
if (Test-Path $exePath) {
|
|
||||||
$exeSig = Get-AuthenticodeSignature -FilePath $exePath
|
|
||||||
Write-Host "EXE Signature Status: $($exeSig.Status)"
|
|
||||||
Write-Host "EXE Signer: $($exeSig.SignerCertificate.Subject)"
|
|
||||||
Write-Host "EXE Timestamp: $($exeSig.TimeStamperCertificate.NotAfter)"
|
|
||||||
|
|
||||||
if ($exeSig.Status -ne "Valid") {
|
|
||||||
Write-Host "[WARNING] EXE is not properly signed (Status: $($exeSig.Status))"
|
|
||||||
if ($usingKeyLocker -or $usingPfx) {
|
|
||||||
Write-Host "[ERROR] Certificate was provided but signing failed"
|
|
||||||
$allSigned = $false
|
|
||||||
} else {
|
|
||||||
Write-Host "[INFO] Building unsigned binary (no certificate provided)"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Host "[SUCCESS] EXE is properly signed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check MSI signature
|
|
||||||
if (Test-Path $msiPath) {
|
if (Test-Path $msiPath) {
|
||||||
$msiSig = Get-AuthenticodeSignature -FilePath $msiPath
|
$sig = Get-AuthenticodeSignature -FilePath $msiPath
|
||||||
Write-Host "MSI Signature Status: $($msiSig.Status)"
|
Write-Host "MSI: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
||||||
Write-Host "MSI Signer: $($msiSig.SignerCertificate.Subject)"
|
if ($sig.Status -ne "Valid") {
|
||||||
Write-Host "MSI Timestamp: $($msiSig.TimeStamperCertificate.NotAfter)"
|
Write-Host "[ERROR] MSI is not signed"
|
||||||
|
$allSigned = $false
|
||||||
|
}
|
||||||
|
|
||||||
if ($msiSig.Status -ne "Valid") {
|
# Extract MSI and verify inner exe
|
||||||
Write-Host "[WARNING] MSI is not properly signed (Status: $($msiSig.Status))"
|
$extractDir = Join-Path $env:RUNNER_TEMP "msi-verify"
|
||||||
if ($usingKeyLocker -or $usingPfx) {
|
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
|
||||||
Write-Host "[ERROR] Certificate was provided but signing failed"
|
$proc = Start-Process msiexec.exe -ArgumentList '/a', $msiPath, '/qn', "TARGETDIR=$extractDir" -Wait -PassThru -NoNewWindow
|
||||||
$allSigned = $false
|
if ($proc.ExitCode -eq 0) {
|
||||||
|
$innerExe = Get-ChildItem -Path $extractDir -Filter "stirling-pdf.exe" -Recurse -File | Select-Object -First 1
|
||||||
|
if ($innerExe) {
|
||||||
|
$sig = Get-AuthenticodeSignature -FilePath $innerExe.FullName
|
||||||
|
Write-Host "Inner EXE: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
||||||
|
if ($sig.Status -ne "Valid") {
|
||||||
|
Write-Host "[ERROR] Inner exe is NOT signed - AV will flag this at runtime"
|
||||||
|
$allSigned = $false
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[INFO] Building unsigned binary (no certificate provided)"
|
Write-Host "[ERROR] Could not find stirling-pdf.exe inside MSI"
|
||||||
|
$allSigned = $false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[SUCCESS] MSI is properly signed"
|
Write-Host "[ERROR] MSI extraction failed (exit code: $($proc.ExitCode))"
|
||||||
|
$allSigned = $false
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "[ERROR] MSI not found at $msiPath"
|
||||||
|
$allSigned = $false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($usingKeyLocker -or $usingPfx) -and -not $allSigned) {
|
if (-not $allSigned) {
|
||||||
Write-Host "[ERROR] Code signing verification failed"
|
Write-Host "[ERROR] Signature verification failed"
|
||||||
exit 1
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "[SUCCESS] MSI and inner exe are properly signed"
|
||||||
|
|
||||||
|
- name: Dump smctl logs on failure
|
||||||
|
if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$logDir = "$env:USERPROFILE\.signingmanager\logs"
|
||||||
|
if (Test-Path $logDir) {
|
||||||
|
Get-ChildItem $logDir | ForEach-Object {
|
||||||
|
Write-Host "=== $($_.FullName) ==="
|
||||||
|
Get-Content $_.FullName -Tail 200
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Write-Host "[SUCCESS] Code signature verification completed"
|
Write-Host "smctl log directory not found at $logDir"
|
||||||
}
|
}
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
|
|||||||
Reference in New Issue
Block a user