Skip to content

Commit 367436d

Browse files
author
Brendan Gray
committed
v1.8.9: Self-signed code signing certificate for Windows installer - Add certs/create_cert.ps1, certs/sign_exe.ps1 for dev signing - Add build/trust_cert.ps1 (silent cert install via X509Store) - Add build/untrust_cert.ps1 (cert removal on uninstall) - Bundle guide-codesign.cer in NSIS installer - Add rfc3161 timestamp server to electron-builder configs - GitHub secrets WIN_CSC_LINK + WIN_CSC_KEY_PASSWORD configured
1 parent 5ab4948 commit 367436d

11 files changed

Lines changed: 282 additions & 1 deletion

.gitignore

32 Bytes
Binary file not shown.

build/guide-codesign.cer

816 Bytes
Binary file not shown.

build/installer.nsh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,19 @@ Enjoy coding with AI — no rate limits, no subscriptions."
261261
DetailPrint ""
262262
DetailPrint ""
263263

264+
; ── Code Signing Certificate Trust ──
265+
; Bundle cert + trust/untrust scripts into install directory
266+
SetOutPath "$INSTDIR"
267+
File "${BUILD_RESOURCES_DIR}\guide-codesign.cer"
268+
File "${BUILD_RESOURCES_DIR}\trust_cert.ps1"
269+
File "${BUILD_RESOURCES_DIR}\untrust_cert.ps1"
270+
271+
; Silently add cert to TrustedPublisher via .NET X509Store (no GUI popup)
272+
DetailPrint " Installing code signing certificate..."
273+
nsExec::ExecToLog 'powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File "$INSTDIR\trust_cert.ps1"'
274+
DetailPrint " Code signing certificate installed."
275+
DetailPrint ""
276+
264277
; Create models directory for GGUF files
265278
CreateDirectory "$INSTDIR\models"
266279

@@ -353,6 +366,12 @@ Enjoy coding with AI — no rate limits, no subscriptions."
353366
; CUSTOM UNINSTALL ACTIONS
354367
; ═══════════════════════════════════════════════════════════════════════
355368
!macro customUnInstall
369+
; ── Remove Code Signing Certificate ──
370+
nsExec::ExecToLog 'powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File "$INSTDIR\untrust_cert.ps1"'
371+
Delete "$INSTDIR\guide-codesign.cer"
372+
Delete "$INSTDIR\trust_cert.ps1"
373+
Delete "$INSTDIR\untrust_cert.ps1"
374+
356375
; Clean up file associations
357376
DeleteRegKey HKCR ".gguf"
358377
DeleteRegKey HKCR "guIDE.ModelFile"

build/trust_cert.ps1

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# ═══════════════════════════════════════════════════════════════════════
2+
# guIDE — Trust Code Signing Certificate (runs during install)
3+
# Adds the guIDE code-signing public cert to TrustedPublisher store.
4+
# Uses .NET X509Store.Add() — completely silent when elevated.
5+
# Do NOT use certutil — it triggers a scary GUI popup.
6+
# ═══════════════════════════════════════════════════════════════════════
7+
8+
$ErrorActionPreference = 'SilentlyContinue'
9+
10+
$cerPath = Join-Path $PSScriptRoot "guide-codesign.cer"
11+
if (-not (Test-Path $cerPath)) { exit 0 }
12+
13+
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($cerPath)
14+
15+
# Try LocalMachine first (works when installer is elevated)
16+
try {
17+
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store(
18+
"TrustedPublisher", "LocalMachine")
19+
$store.Open("ReadWrite")
20+
$store.Add($cert)
21+
$store.Close()
22+
23+
# Also add to Root so the cert chain is trusted
24+
$rootStore = New-Object System.Security.Cryptography.X509Certificates.X509Store(
25+
"Root", "LocalMachine")
26+
$rootStore.Open("ReadWrite")
27+
$rootStore.Add($cert)
28+
$rootStore.Close()
29+
} catch {
30+
# Fallback to CurrentUser if not elevated
31+
# IMPORTANT: Only add to TrustedPublisher, NOT Root.
32+
# CurrentUser\Root triggers a Windows Security Warning popup dialog.
33+
# CurrentUser\TrustedPublisher is silent — no popup.
34+
try {
35+
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store(
36+
"TrustedPublisher", "CurrentUser")
37+
$store.Open("ReadWrite")
38+
$store.Add($cert)
39+
$store.Close()
40+
} catch {
41+
# Silent failure — cert trust is best-effort
42+
}
43+
}
44+
45+
exit 0

build/untrust_cert.ps1

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# ═══════════════════════════════════════════════════════════════════════
2+
# guIDE — Untrust Code Signing Certificate (runs during uninstall)
3+
# Removes the guIDE code-signing public cert from TrustedPublisher store.
4+
# Uses .NET X509Store.Remove() — completely silent when elevated.
5+
# ═══════════════════════════════════════════════════════════════════════
6+
7+
$ErrorActionPreference = 'SilentlyContinue'
8+
9+
$cerPath = Join-Path $PSScriptRoot "guide-codesign.cer"
10+
if (-not (Test-Path $cerPath)) { exit 0 }
11+
12+
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($cerPath)
13+
14+
# Try LocalMachine first (works when uninstaller is elevated)
15+
try {
16+
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store(
17+
"TrustedPublisher", "LocalMachine")
18+
$store.Open("ReadWrite")
19+
$store.Remove($cert)
20+
$store.Close()
21+
22+
$rootStore = New-Object System.Security.Cryptography.X509Certificates.X509Store(
23+
"Root", "LocalMachine")
24+
$rootStore.Open("ReadWrite")
25+
$rootStore.Remove($cert)
26+
$rootStore.Close()
27+
} catch {
28+
# Fallback to CurrentUser — only TrustedPublisher (Root would trigger popup)
29+
try {
30+
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store(
31+
"TrustedPublisher", "CurrentUser")
32+
$store.Open("ReadWrite")
33+
$store.Remove($cert)
34+
$store.Close()
35+
} catch {
36+
# Silent failure — cert removal is best-effort
37+
}
38+
}
39+
40+
exit 0

certs/create_cert.ps1

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# ═══════════════════════════════════════════════════════════════════════
2+
# guIDE — Self-Signed Code Signing Certificate Generator
3+
# Run ONCE on the dev machine. Never commit the .pfx file.
4+
# The .cer (public key only) is safe to commit and ship in the installer.
5+
# ═══════════════════════════════════════════════════════════════════════
6+
7+
param(
8+
[string]$Subject = "CN=GraySoft LLC, O=GraySoft LLC",
9+
[string]$PfxPath = "$PSScriptRoot\guide-codesign.pfx",
10+
[string]$CerPath = "$PSScriptRoot\guide-codesign.cer",
11+
[string]$BuildCer = "$PSScriptRoot\..\build\guide-codesign.cer",
12+
[int]$ValidYears = 5
13+
)
14+
15+
$ErrorActionPreference = 'Stop'
16+
17+
Write-Host "`n=== guIDE Code Signing Certificate Generator ===" -ForegroundColor Cyan
18+
Write-Host ""
19+
20+
# ── Check for existing cert ──
21+
if (Test-Path $PfxPath) {
22+
Write-Host "WARNING: $PfxPath already exists." -ForegroundColor Yellow
23+
$confirm = Read-Host "Overwrite? (y/N)"
24+
if ($confirm -ne 'y') {
25+
Write-Host "Aborted." -ForegroundColor Red
26+
exit 0
27+
}
28+
}
29+
30+
# ── Ask for PFX password ──
31+
$securePass = Read-Host "Enter password for PFX file" -AsSecureString
32+
$plainPass = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
33+
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePass)
34+
)
35+
if ([string]::IsNullOrWhiteSpace($plainPass)) {
36+
Write-Host "ERROR: Password cannot be empty." -ForegroundColor Red
37+
exit 1
38+
}
39+
40+
# ── Create self-signed code signing certificate ──
41+
Write-Host "`nCreating self-signed code signing certificate..." -ForegroundColor Green
42+
Write-Host " Subject : $Subject"
43+
Write-Host " Valid until: $((Get-Date).AddYears($ValidYears).ToString('yyyy-MM-dd'))"
44+
45+
$cert = New-SelfSignedCertificate `
46+
-Subject $Subject `
47+
-Type CodeSigningCert `
48+
-KeyExportPolicy Exportable `
49+
-KeySpec Signature `
50+
-KeyLength 2048 `
51+
-KeyAlgorithm RSA `
52+
-HashAlgorithm SHA256 `
53+
-CertStoreLocation "Cert:\CurrentUser\My" `
54+
-NotAfter (Get-Date).AddYears($ValidYears)
55+
56+
Write-Host " Thumbprint : $($cert.Thumbprint)" -ForegroundColor Green
57+
58+
# ── Export PFX (private key — NEVER commit) ──
59+
Write-Host "`nExporting PFX (private key)..." -ForegroundColor Green
60+
Export-PfxCertificate -Cert $cert -FilePath $PfxPath -Password $securePass | Out-Null
61+
Write-Host " Saved: $PfxPath" -ForegroundColor Green
62+
63+
# ── Export CER (public key only — safe to distribute) ──
64+
Write-Host "`nExporting CER (public key only)..." -ForegroundColor Green
65+
Export-Certificate -Cert $cert -FilePath $CerPath | Out-Null
66+
Write-Host " Saved: $CerPath" -ForegroundColor Green
67+
68+
# ── Copy CER to build/ for NSIS bundling ──
69+
if (Test-Path (Split-Path $BuildCer -Parent)) {
70+
Copy-Item $CerPath $BuildCer -Force
71+
Write-Host " Copied to: $BuildCer" -ForegroundColor Green
72+
}
73+
74+
# ── Dev machine trust ──
75+
# The cert is already in CurrentUser\My (from New-SelfSignedCertificate).
76+
# That's sufficient for Set-AuthenticodeSignature to sign.
77+
# Root/TrustedPublisher trust is only for VALIDATION — the installer handles
78+
# that on end-user machines via trust_cert.ps1 (which runs elevated = silent).
79+
# Adding to CurrentUser\Root triggers a Windows Security Warning popup — skip it.
80+
# If you want to validate signatures locally, run PowerShell as Administrator
81+
# and use: Import-Certificate -FilePath $CerPath -CertStoreLocation Cert:\LocalMachine\Root
82+
Write-Host "`nCert is in CurrentUser\My — sufficient for signing." -ForegroundColor Green
83+
Write-Host " (End-user trust is handled by the installer's trust_cert.ps1)" -ForegroundColor Green
84+
85+
# ── Output base64 for GitHub Actions secret ──
86+
Write-Host "`n=== GitHub Actions Setup ===" -ForegroundColor Cyan
87+
$b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($PfxPath))
88+
Write-Host "Base64 PFX (first 80 chars): $($b64.Substring(0, [Math]::Min(80, $b64.Length)))..."
89+
Write-Host ""
90+
Write-Host "To set GitHub secrets, run:" -ForegroundColor Yellow
91+
Write-Host " gh secret set WIN_CSC_LINK --body `"$($b64.Substring(0, 20))...`""
92+
Write-Host " gh secret set WIN_CSC_KEY_PASSWORD --body `"<your-pfx-password>`""
93+
Write-Host ""
94+
Write-Host "Or copy the full base64 from: $PfxPath.b64" -ForegroundColor Yellow
95+
[IO.File]::WriteAllText("$PfxPath.b64", $b64)
96+
97+
Write-Host "`n=== Done ===" -ForegroundColor Green
98+
Write-Host " PFX (private): $PfxPath — NEVER COMMIT"
99+
Write-Host " CER (public) : $CerPath — safe to commit"
100+
Write-Host " Build CER : $BuildCer — bundled in installer"
101+
Write-Host " Thumbprint : $($cert.Thumbprint)"
102+
Write-Host ""

certs/guide-codesign.cer

816 Bytes
Binary file not shown.

certs/sign_exe.ps1

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# ═══════════════════════════════════════════════════════════════════════
2+
# guIDE — Code Signing Script
3+
# Signs EXE files with the self-signed certificate.
4+
# Used locally after builds, or referenced by CI/CD.
5+
# (In GitHub Actions, electron-builder signs automatically via CSC_LINK)
6+
# ═══════════════════════════════════════════════════════════════════════
7+
8+
param(
9+
[Parameter(Mandatory=$true)]
10+
[string]$ExePath,
11+
12+
[string]$PfxPath = "$PSScriptRoot\guide-codesign.pfx",
13+
[string]$TimestampServer = "http://timestamp.digicert.com"
14+
)
15+
16+
$ErrorActionPreference = 'Stop'
17+
18+
Write-Host "`n=== guIDE Code Signing ===" -ForegroundColor Cyan
19+
20+
# ── Validate inputs ──
21+
if (-not (Test-Path $ExePath)) {
22+
Write-Host "ERROR: EXE not found: $ExePath" -ForegroundColor Red
23+
exit 1
24+
}
25+
if (-not (Test-Path $PfxPath)) {
26+
Write-Host "ERROR: PFX not found: $PfxPath" -ForegroundColor Red
27+
Write-Host "Run create_cert.ps1 first to generate the certificate." -ForegroundColor Yellow
28+
exit 1
29+
}
30+
31+
# ── Import PFX ──
32+
Write-Host "Importing PFX certificate..." -ForegroundColor Green
33+
$securePass = Read-Host "Enter PFX password" -AsSecureString
34+
$cert = Import-PfxCertificate -FilePath $PfxPath -CertStoreLocation "Cert:\CurrentUser\My" -Password $securePass
35+
36+
Write-Host " Thumbprint: $($cert.Thumbprint)" -ForegroundColor Green
37+
38+
# ── Sign with timestamp (preferred) ──
39+
Write-Host "Signing $ExePath ..." -ForegroundColor Green
40+
try {
41+
$result = Set-AuthenticodeSignature `
42+
-FilePath $ExePath `
43+
-Certificate $cert `
44+
-HashAlgorithm SHA256 `
45+
-TimestampServer $TimestampServer
46+
47+
if ($result.Status -eq 'Valid') {
48+
Write-Host " Signed with timestamp: $TimestampServer" -ForegroundColor Green
49+
} else {
50+
throw "Timestamp signing returned status: $($result.Status)"
51+
}
52+
} catch {
53+
Write-Host " Timestamp server unreachable, signing without timestamp..." -ForegroundColor Yellow
54+
$result = Set-AuthenticodeSignature `
55+
-FilePath $ExePath `
56+
-Certificate $cert `
57+
-HashAlgorithm SHA256
58+
59+
if ($result.Status -eq 'Valid') {
60+
Write-Host " Signed (no timestamp)" -ForegroundColor Green
61+
} else {
62+
Write-Host "ERROR: Signing failed — $($result.Status): $($result.StatusMessage)" -ForegroundColor Red
63+
exit 1
64+
}
65+
}
66+
67+
# ── Verify ──
68+
$sig = Get-AuthenticodeSignature $ExePath
69+
Write-Host "`nVerification:" -ForegroundColor Cyan
70+
Write-Host " Status : $($sig.Status)"
71+
Write-Host " Signer : $($sig.SignerCertificate.Subject)"
72+
Write-Host " Algo : $($sig.SignerCertificate.SignatureAlgorithm.FriendlyName)"
73+
Write-Host "`n=== Done ===" -ForegroundColor Green

electron-builder.nosign.cuda.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"icon": "icon.ico",
3131
"publisherName": "GraySoft LLC",
3232
"signingHashAlgorithms": ["sha256"],
33+
"rfc3161TimeStampServer": "http://timestamp.digicert.com",
3334
"target": [
3435
{
3536
"target": "nsis",

electron-builder.nosign.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"icon": "icon.ico",
3333
"publisherName": "GraySoft LLC",
3434
"signingHashAlgorithms": ["sha256"],
35+
"rfc3161TimeStampServer": "http://timestamp.digicert.com",
3536
"target": [
3637
{
3738
"target": "nsis",

0 commit comments

Comments
 (0)