Skip to content

Commit 046978c

Browse files
committed
feat(installer): Add Windows installer and code signing functionality
Add Inno Setup installation script and PowerShell signing script Update the CI process to include installer building and code signing Add a certificate file exclusion rule in .gitignore
1 parent 5aa627b commit 046978c

5 files changed

Lines changed: 356 additions & 0 deletions

File tree

.github/workflows/release-installer.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,72 @@ jobs:
4848
- name: Publish win-arm64
4949
run: dotnet publish DevTools.csproj -c Release -r win-arm64 --self-contained true /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true /p:PublishTrimmed=false -o publish\win-arm64
5050

51+
- name: Decode signing certificate
52+
if: env.CERTIFICATE_BASE64 != ''
53+
env:
54+
CERTIFICATE_BASE64: ${{ secrets.CERTIFICATE_BASE64 }}
55+
run: |
56+
$certPath = Join-Path $env:RUNNER_TEMP "cert.pfx"
57+
[System.IO.File]::WriteAllBytes($certPath, [System.Convert]::FromBase64String($env:CERTIFICATE_BASE64))
58+
echo "CERT_PATH=$certPath" >> $env:GITHUB_ENV
59+
shell: pwsh
60+
61+
- name: Sign executables
62+
if: env.CERT_PATH != ''
63+
env:
64+
CERT_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
65+
run: |
66+
$signtool = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" | Sort-Object { $_.VersionInfo.FileVersion } -Descending | Select-Object -First 1
67+
68+
$exeFiles = @(
69+
"publish\win-x86\DevTools.exe",
70+
"publish\win-x64\DevTools.exe",
71+
"publish\win-arm64\DevTools.exe"
72+
)
73+
74+
foreach ($exe in $exeFiles) {
75+
Write-Host "Signing: $exe"
76+
& $signtool.FullName sign /f $env:CERT_PATH /p $env:CERT_PASSWORD /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 $exe
77+
if ($LASTEXITCODE -ne 0) {
78+
Write-Host "Warning: Failed to sign $exe"
79+
}
80+
}
81+
shell: pwsh
82+
83+
- name: Build Installers
84+
run: |
85+
New-Item -ItemType Directory -Force -Path installer
86+
87+
$version = "${{ steps.get_version.outputs.VERSION }}"
88+
89+
Write-Host "Building x64 installer..."
90+
& iscc scripts/installer.iss /DAppVersion="$version" /DArchitecture="x64" /DOutputDir="installer" /DSourceDir="publish\win-x64"
91+
92+
Write-Host "Building x86 installer..."
93+
& iscc scripts/installer.iss /DAppVersion="$version" /DArchitecture="x86" /DOutputDir="installer" /DSourceDir="publish\win-x86"
94+
95+
Write-Host "Building arm64 installer..."
96+
& iscc scripts/installer.iss /DAppVersion="$version" /DArchitecture="arm64" /DOutputDir="installer" /DSourceDir="publish\win-arm64"
97+
shell: pwsh
98+
99+
- name: Sign installers
100+
if: env.CERT_PATH != ''
101+
env:
102+
CERT_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
103+
run: |
104+
$signtool = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" | Sort-Object { $_.VersionInfo.FileVersion } -Descending | Select-Object -First 1
105+
106+
$installerFiles = Get-ChildItem -Path "installer\*.exe"
107+
108+
foreach ($installer in $installerFiles) {
109+
Write-Host "Signing: $($installer.FullName)"
110+
& $signtool.FullName sign /f $env:CERT_PATH /p $env:CERT_PASSWORD /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 $installer.FullName
111+
if ($LASTEXITCODE -ne 0) {
112+
Write-Host "Warning: Failed to sign $($installer.FullName)"
113+
}
114+
}
115+
shell: pwsh
116+
51117
- name: Rename executables
52118
run: |
53119
Rename-Item -Path publish\win-x86\DevTools.exe -NewName DevTools-win-x86.exe
@@ -63,5 +129,6 @@ jobs:
63129
publish/win-x86/DevTools-win-x86.exe
64130
publish/win-x64/DevTools-win-x64.exe
65131
publish/win-arm64/DevTools-win-arm64.exe
132+
installer/*.exe
66133
env:
67134
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ Thumbs.db
116116
*.cache
117117
*.pdb
118118

119+
# Code signing certificates (DO NOT COMMIT)
120+
*.pfx
121+
*.p12
122+
build/certs/
123+
119124
# Node
120125
node_modules/
121126

scripts/create-cert.ps1

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
param(
2+
[string]$CertificatePassword = "DevTools2024!",
3+
[string]$CertName = "DevTools Code Signing",
4+
[string]$OutputPath = ".\build\certs"
5+
)
6+
7+
$ErrorActionPreference = "Stop"
8+
9+
Write-Host "=== DevTools Self-Signed Certificate Creator ===" -ForegroundColor Cyan
10+
11+
$pfxPath = Join-Path $OutputPath "DevToolsCodeSigning.pfx"
12+
$cerPath = Join-Path $OutputPath "DevToolsCodeSigning.cer"
13+
14+
if (-not (Test-Path $OutputPath)) {
15+
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
16+
Write-Host "Created output directory: $OutputPath" -ForegroundColor Green
17+
}
18+
19+
Write-Host "`nStep 1: Creating self-signed certificate..." -ForegroundColor Yellow
20+
21+
$cert = New-SelfSignedCertificate `
22+
-Subject "CN=$CertName" `
23+
-Type CodeSigningCert `
24+
-KeyUsage DigitalSignature `
25+
-FriendlyName "$CertName" `
26+
-CertStoreLocation "Cert:\CurrentUser\My" `
27+
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3,1.3.6.1.4.1.311.10.3.13") `
28+
-KeyExportPolicy Exportable `
29+
-KeyLength 2048 `
30+
-KeyAlgorithm RSA `
31+
-HashAlgorithm SHA256 `
32+
-NotAfter (Get-Date).AddYears(10)
33+
34+
Write-Host "Certificate created with thumbprint: $($cert.Thumbprint)" -ForegroundColor Green
35+
36+
Write-Host "`nStep 2: Exporting certificate to PFX file..." -ForegroundColor Yellow
37+
38+
$securePassword = ConvertTo-SecureString -String $CertificatePassword -Force -AsPlainText
39+
Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $securePassword | Out-Null
40+
Write-Host "PFX exported to: $pfxPath" -ForegroundColor Green
41+
42+
Write-Host "`nStep 3: Exporting public certificate..." -ForegroundColor Yellow
43+
Export-Certificate -Cert $cert -FilePath $cerPath -Type CERT | Out-Null
44+
Write-Host "Public cert exported to: $cerPath" -ForegroundColor Green
45+
46+
Write-Host "`nStep 4: Adding certificate to Trusted Publishers..." -ForegroundColor Yellow
47+
48+
$trustedPublishers = Get-ChildItem -Path "Cert:\CurrentUser\TrustedPublisher" -ErrorAction SilentlyContinue
49+
$alreadyTrusted = $trustedPublishers | Where-Object { $_.Thumbprint -eq $cert.Thumbprint }
50+
51+
if (-not $alreadyTrusted) {
52+
$cert | Export-Certificate -FilePath (Join-Path $OutputPath "temp.cer") -Type CERT -Force | Out-Null
53+
Import-Certificate -FilePath (Join-Path $OutputPath "temp.cer") -CertStoreLocation "Cert:\CurrentUser\TrustedPublisher" | Out-Null
54+
Remove-Item (Join-Path $OutputPath "temp.cer") -Force
55+
Write-Host "Certificate added to Trusted Publishers" -ForegroundColor Green
56+
} else {
57+
Write-Host "Certificate already in Trusted Publishers" -ForegroundColor Yellow
58+
}
59+
60+
Write-Host "`nStep 5: Adding certificate to Root CA (for local trust)..." -ForegroundColor Yellow
61+
62+
$rootCerts = Get-ChildItem -Path "Cert:\CurrentUser\Root" -ErrorAction SilentlyContinue
63+
$alreadyInRoot = $rootCerts | Where-Object { $_.Thumbprint -eq $cert.Thumbprint }
64+
65+
if (-not $alreadyInRoot) {
66+
$cert | Export-Certificate -FilePath (Join-Path $OutputPath "temp.cer") -Type CERT -Force | Out-Null
67+
Import-Certificate -FilePath (Join-Path $OutputPath "temp.cer") -CertStoreLocation "Cert:\CurrentUser\Root" | Out-Null
68+
Remove-Item (Join-Path $OutputPath "temp.cer") -Force
69+
Write-Host "Certificate added to Trusted Root Certification Authorities" -ForegroundColor Green
70+
} else {
71+
Write-Host "Certificate already in Root CA store" -ForegroundColor Yellow
72+
}
73+
74+
Write-Host @"
75+
76+
=== Certificate Creation Complete ===
77+
78+
Certificate Details:
79+
Subject: $($cert.Subject)
80+
Thumbprint: $($cert.Thumbprint)
81+
Valid From: $($cert.NotBefore)
82+
Valid To: $($cert.NotAfter)
83+
84+
Files Created:
85+
PFX (Private): $pfxPath
86+
CER (Public): $cerPath
87+
Password: $CertificatePassword
88+
89+
IMPORTANT:
90+
- Keep the PFX file and password secure!
91+
- The certificate is now trusted on THIS computer only
92+
- On other computers, users need to install the CER file to:
93+
1. Trusted Publishers
94+
2. Trusted Root Certification Authorities
95+
96+
"@ -ForegroundColor Cyan
97+
98+
return @{
99+
PfxPath = $pfxPath
100+
CerPath = $cerPath
101+
Thumbprint = $cert.Thumbprint
102+
Password = $CertificatePassword
103+
}

scripts/installer.iss

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#define AppName "DevTools"
2+
#define AppPublisher "DevTools"
3+
#define AppURL "https://github.com/user/devtools"
4+
#define AppExeName "DevTools.exe"
5+
6+
[Setup]
7+
AppId={{8B5F3C7A-1D2E-4F6B-9C3A-5E7D8F1A2B3C}
8+
AppName={#AppName}
9+
AppVersion={#AppVersion}
10+
AppVerName={#AppName} {#AppVersion}
11+
AppPublisher={#AppPublisher}
12+
AppPublisherURL={#AppURL}
13+
AppSupportURL={#AppURL}
14+
AppUpdatesURL={#AppURL}
15+
DefaultDirName={autopf}\{#AppName}
16+
DefaultGroupName={#AppName}
17+
AllowNoIcons=yes
18+
OutputDir={#OutputDir}
19+
OutputBaseFilename={#AppName}-{#Architecture}-Setup-{#AppVersion}
20+
Compression=lzma2/ultra64
21+
SolidCompression=yes
22+
WizardStyle=modern
23+
PrivilegesRequired=admin
24+
UninstallDisplayIcon={app}\{#AppExeName}
25+
UninstallDisplayName={#AppName}
26+
VersionInfoVersion={#AppVersion}
27+
VersionInfoCompany={#AppPublisher}
28+
VersionInfoDescription={#AppName} Installer
29+
VersionInfoCopyright=Copyright (C) 2024
30+
VersionInfoProductName={#AppName}
31+
VersionInfoProductVersion={#AppVersion}
32+
33+
#if "x64" == "{#Architecture}"
34+
ArchitecturesAllowed=x64compatible
35+
ArchitecturesInstallIn64BitMode=x64compatible
36+
#elif "arm64" == "{#Architecture}"
37+
ArchitecturesAllowed=arm64
38+
ArchitecturesInstallIn64BitMode=arm64
39+
#else
40+
ArchitecturesAllowed=x86
41+
#endif
42+
43+
[Languages]
44+
Name: "english"; MessagesFile: "compiler:Default.isl"
45+
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
46+
47+
[Tasks]
48+
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
49+
50+
[Files]
51+
Source: "{#SourceDir}\{#AppExeName}"; DestDir: "{app}"; Flags: ignoreversion
52+
Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
53+
54+
[Icons]
55+
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"
56+
Name: "{group}\{cm:ProgramOnTheWeb,{#AppName}}"; Filename: "{#AppURL}"
57+
Name: "{group}\{cm:UninstallProgram,{#AppName}}"; Filename: "{uninstallexe}"
58+
Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon
59+
60+
[Run]
61+
Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
62+
63+
[UninstallDelete]
64+
Type: filesandordirs; Name: "{app}"

scripts/sign-exe.ps1

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
param(
2+
[string]$ExePath,
3+
[string]$PfxPath = ".\build\certs\DevToolsCodeSigning.pfx",
4+
[string]$Password = "DevTools2024!",
5+
[string]$TimestampServer = "http://timestamp.digicert.com"
6+
)
7+
8+
$ErrorActionPreference = "Stop"
9+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
10+
$projectRoot = (Get-Item $scriptDir).Parent.FullName
11+
12+
Write-Host "=== DevTools Code Signing ===" -ForegroundColor Cyan
13+
14+
if (-not $ExePath) {
15+
Write-Host "Error: ExePath parameter is required" -ForegroundColor Red
16+
Write-Host "Usage: .\sign-exe.ps1 -ExePath <path-to-exe>" -ForegroundColor Yellow
17+
exit 1
18+
}
19+
20+
if (-not ([System.IO.Path]::IsPathRooted($ExePath))) {
21+
$ExePath = Join-Path $projectRoot $ExePath
22+
}
23+
24+
if (-not ([System.IO.Path]::IsPathRooted($PfxPath))) {
25+
$PfxPath = Join-Path $projectRoot $PfxPath
26+
}
27+
28+
if (-not (Test-Path $ExePath)) {
29+
Write-Host "Error: File not found: $ExePath" -ForegroundColor Red
30+
exit 1
31+
}
32+
33+
if (-not (Test-Path $PfxPath)) {
34+
Write-Host "Error: Certificate not found: $PfxPath" -ForegroundColor Red
35+
Write-Host "Please run create-cert.ps1 first to create the certificate." -ForegroundColor Yellow
36+
exit 1
37+
}
38+
39+
Write-Host "Signing: $ExePath" -ForegroundColor Yellow
40+
Write-Host "Certificate: $PfxPath" -ForegroundColor Yellow
41+
42+
$signtool = $null
43+
44+
$sdkPaths = @(
45+
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe",
46+
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.0.22000.0\x64\signtool.exe",
47+
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe",
48+
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.0.18362.0\x64\signtool.exe"
49+
)
50+
51+
foreach ($path in $sdkPaths) {
52+
if (Test-Path $path) {
53+
$signtool = $path
54+
break
55+
}
56+
}
57+
58+
if (-not $signtool) {
59+
$signtool = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter "signtool.exe" -ErrorAction SilentlyContinue |
60+
Where-Object { $_.FullName -match "\\x64\\" } |
61+
Sort-Object { $_.FullName } -Descending |
62+
Select-Object -ExpandProperty FullName -First 1
63+
}
64+
65+
if (-not $signtool) {
66+
Write-Host @"
67+
68+
========================================
69+
Windows SDK Not Found!
70+
========================================
71+
72+
signtool.exe is required for code signing.
73+
74+
Please install Windows SDK:
75+
1. Download from: https://aka.ms/winsdk
76+
2. Or run: winget install Microsoft.WindowsSDK.10.0
77+
3. Select "Windows Software Development Kit" during installation
78+
79+
After installation, run this script again.
80+
81+
"@ -ForegroundColor Red
82+
exit 1
83+
}
84+
85+
Write-Host "Using signtool: $signtool" -ForegroundColor Gray
86+
87+
$result = & $signtool sign /f $PfxPath /p $Password /tr $TimestampServer /td SHA256 /fd SHA256 $ExePath 2>&1
88+
89+
if ($LASTEXITCODE -eq 0) {
90+
Write-Host "`nSigning successful!" -ForegroundColor Green
91+
92+
Write-Host "`nVerifying signature..." -ForegroundColor Cyan
93+
$verifyResult = & $signtool verify /pa $ExePath 2>&1
94+
Write-Host $verifyResult
95+
96+
Write-Host @"
97+
98+
========================================
99+
Signature Applied Successfully!
100+
========================================
101+
102+
The executable is now signed with your certificate.
103+
104+
Note: Since this is a self-signed certificate:
105+
- It will be trusted on THIS computer (certificate installed to Trusted Publishers)
106+
- On other computers, users need to install the .cer file first
107+
108+
Files:
109+
Signed EXE: $ExePath
110+
Public Cert: $($PfxPath -replace '\.pfx$', '.cer')
111+
112+
"@ -ForegroundColor Green
113+
} else {
114+
Write-Host "`nSigning failed!" -ForegroundColor Red
115+
Write-Host $result
116+
exit 1
117+
}

0 commit comments

Comments
 (0)