Skip to content

Commit d9ce286

Browse files
Yogesh Prajapaticlaude
andcommitted
build(signing): sign Windows installers with self-signed certificate
CI Windows job now decodes a PFX from the WIN_CSC_LINK_BASE64 secret and passes CSC_LINK + CSC_KEY_PASSWORD to electron-builder, so the NSIS installer and zipped exe are Authenticode-signed. The step is gated on the secret being present, so the workflow stays green if signing is ever turned off. Adds: - scripts/generate-signing-cert.ps1: regenerate / rotate the cert - build/Notepp-CodeSigning.cer: public cert, users can pre-trust it - features/SIGNING.md: setup, rotation, and user-side trust runbook SmartScreen still warns on first install (self-signed cert chain doesn't reach a trusted CA), but the installer now shows the publisher name, is tamper-evident, and motivated users can import the public cert into Trusted Publishers to silence SmartScreen permanently. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent f890071 commit d9ce286

5 files changed

Lines changed: 255 additions & 2 deletions

File tree

.github/workflows/release.yml

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,36 @@ jobs:
8888
- name: Build whiteboard iframe bundle
8989
run: npm run build:wb
9090

91+
# Decode the self-signed Windows code-signing PFX out of repo secrets
92+
# so electron-builder can sign the NSIS installer + .exe. If the
93+
# secret is missing the step is a no-op and the build ships unsigned
94+
# — keeps the workflow green before the cert is provisioned.
95+
# See scripts/generate-signing-cert.ps1 and features/SIGNING.md.
96+
- name: Decode Windows signing certificate (Windows only)
97+
if: matrix.os == 'windows-latest' && secrets.WIN_CSC_LINK_BASE64 != ''
98+
shell: pwsh
99+
env:
100+
WIN_CSC_LINK_BASE64: ${{ secrets.WIN_CSC_LINK_BASE64 }}
101+
run: |
102+
$pfxPath = Join-Path $env:RUNNER_TEMP "notepp-codesign.pfx"
103+
[System.IO.File]::WriteAllBytes(
104+
$pfxPath,
105+
[System.Convert]::FromBase64String($env:WIN_CSC_LINK_BASE64)
106+
)
107+
"CSC_LINK=$pfxPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
108+
Write-Host "Decoded signing PFX to $pfxPath"
109+
91110
- name: Build & publish installer (electron-builder publishes to GitHub release)
92111
run: npx electron-builder ${{ matrix.target_flag }} --publish always
93112
env:
94113
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
95-
# Skip Mac code-signing in CI — we ship unsigned builds for now.
96-
# Users on macOS will need to right-click → Open the first time.
114+
# Windows signing: CSC_LINK is set by the decode step above when
115+
# WIN_CSC_LINK_BASE64 is configured. CSC_KEY_PASSWORD comes
116+
# straight from the secret. If either is empty, electron-builder
117+
# silently skips signing (= unsigned build, current behaviour).
118+
CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
119+
# Skip Mac code-signing in CI — self-signed certs are useless on
120+
# macOS (Gatekeeper requires an Apple Developer cert + notarization).
97121
CSC_IDENTITY_AUTO_DISCOVERY: false
98122

99123
- name: Upload platform artifacts (for the release job)

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,9 @@ src/excalidraw-fonts/
1414

1515
.claude/
1616

17+
# Code-signing private key — generated by scripts/generate-signing-cert.ps1.
18+
# The public .cer in build/ IS committed; the .pfx and anything else under
19+
# .signing/ MUST stay local.
20+
.signing/
21+
*.pfx
22+

build/Notepp-CodeSigning.cer

1.01 KB
Binary file not shown.

features/SIGNING.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Windows Code Signing (self-signed)
2+
3+
Note++ Windows installers are signed in CI with a self-signed code-signing
4+
certificate. This document explains the one-time setup, how CI consumes the
5+
cert, and what users see.
6+
7+
> Self-signed certs **do not** bypass SmartScreen — the cert chain doesn't
8+
> reach a trusted CA, so Windows still warns "unrecognized app". What signing
9+
> does buy you:
10+
>
11+
> - Installer shows **Yogesh Prajapati** as publisher instead of "Unknown
12+
> publisher".
13+
> - Artifacts are tamper-evident — modification breaks the signature.
14+
> - Users who manually trust the public `.cer` (one-time import into Trusted
15+
> Publishers) get zero SmartScreen prompts on future installs.
16+
> - When we eventually buy a real OV/EV cert, the wiring is already in place.
17+
18+
---
19+
20+
## Files in this repo
21+
22+
| Path | Role | Committed? |
23+
|---|---|---|
24+
| `scripts/generate-signing-cert.ps1` | Creates a new self-signed cert, exports PFX + CER, prints CI secret values | Yes |
25+
| `build/Notepp-CodeSigning.cer` | Public certificate. Users can import this into Trusted Publishers to silence SmartScreen | Yes (after first run) |
26+
| `.signing/Notepp-CodeSigning.pfx` | **Private** key. Generated locally, never committed | No (`.gitignore`) |
27+
28+
## One-time setup
29+
30+
1. From a Windows PowerShell prompt at the repo root:
31+
32+
```powershell
33+
pwsh -File scripts/generate-signing-cert.ps1
34+
```
35+
36+
The script prompts for a PFX password, generates a 5-year SHA-256
37+
code-signing cert in `Cert:\CurrentUser\My`, exports both the PFX and CER
38+
to `.signing/`, and copies the public CER into `build/`.
39+
40+
2. The script prints two values when it finishes. Add both as GitHub Actions
41+
secrets at <https://github.com/YogeshPraj/Note-/settings/secrets/actions>:
42+
43+
| Secret name | Value |
44+
|---|---|
45+
| `WIN_CSC_LINK_BASE64` | Base64 of the PFX (printed by the script) |
46+
| `WIN_CSC_KEY_PASSWORD` | The PFX password you chose |
47+
48+
3. Commit the public cert and push:
49+
50+
```bash
51+
git add build/Notepp-CodeSigning.cer
52+
git commit -m "build(signing): add public code-signing certificate"
53+
git push
54+
```
55+
56+
4. Tag a release (`vX.Y.Z`). The Windows runner decodes the PFX, electron-builder
57+
sees `CSC_LINK` + `CSC_KEY_PASSWORD`, and signs `notepp-win-x64.exe`
58+
automatically.
59+
60+
## How CI consumes the cert
61+
62+
`.github/workflows/release.yml`, Windows job:
63+
64+
1. `Decode Windows signing certificate` step writes the base64 secret to a
65+
`.pfx` under `$RUNNER_TEMP` and exports its path as `CSC_LINK` via
66+
`$GITHUB_ENV`.
67+
2. The `electron-builder` step picks up `CSC_LINK` + `CSC_KEY_PASSWORD`
68+
automatically — no `package.json` change required.
69+
3. If `WIN_CSC_LINK_BASE64` isn't set, the decode step is skipped and
70+
electron-builder ships an unsigned build. The workflow stays green.
71+
72+
macOS and Linux runners ignore these secrets — see "Why not Mac/Linux?" below.
73+
74+
## Verifying a signed build
75+
76+
After downloading `notepp-win-x64.exe` from a release:
77+
78+
```powershell
79+
Get-AuthenticodeSignature .\notepp-win-x64.exe
80+
```
81+
82+
`Status` should be `Valid`, signer should be `CN=Yogesh Prajapati`. (Windows
83+
will list it as `UnknownError` if the cert isn't yet in any trust store on
84+
the verifying machine — that's expected for self-signed.)
85+
86+
## Users: silencing SmartScreen permanently (optional)
87+
88+
A motivated user can pre-trust the cert once:
89+
90+
1. Download `Notepp-CodeSigning.cer` from the repo's `build/` folder.
91+
2. Double-click → **Install Certificate****Local Machine****Place all
92+
certificates in the following store****Trusted Publishers** → Finish.
93+
3. From then on, every signed Note++ installer launches without the
94+
"Windows protected your PC" dialog.
95+
96+
Document this in the GitHub release notes if you want users to discover it.
97+
98+
## Rotating the cert
99+
100+
The cert is valid for 5 years. To rotate:
101+
102+
1. Re-run `scripts/generate-signing-cert.ps1` (it creates a brand-new cert).
103+
2. Update both GitHub secrets with the new values it prints.
104+
3. Replace `build/Notepp-CodeSigning.cer` with the new public cert and
105+
commit.
106+
4. Note: `electron-updater` verifies that an update's signing publisher
107+
matches the currently-installed publisher. As long as the new cert has
108+
the same `CN`, auto-updates from old installs keep working.
109+
110+
## Why not Mac/Linux?
111+
112+
- **macOS** — Gatekeeper validates against an Apple-issued Developer ID cert
113+
chain. A self-signed cert is no better than no cert; both produce the same
114+
"unidentified developer" prompt. Mac signing requires a paid Apple
115+
Developer account ($99/yr) + notarization.
116+
- **Linux**`.deb` and AppImage don't have a desktop-level signing model
117+
comparable to Authenticode. The closest equivalent is a detached GPG
118+
signature alongside the artifact, which we can add later if there's
119+
demand.

scripts/generate-signing-cert.ps1

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# ---------------------------------------------------------------------------
2+
# generate-signing-cert.ps1
3+
#
4+
# Creates a self-signed code-signing certificate for Note++, exports it as
5+
# a password-protected PFX (private key, NEVER commit) and a CER (public,
6+
# safe to commit), and prints the values you need to upload as GitHub
7+
# Actions secrets so CI can sign Windows installers automatically.
8+
#
9+
# Run from PowerShell (no admin required for CurrentUser store):
10+
# pwsh -File scripts/generate-signing-cert.ps1
11+
#
12+
# Optional parameters:
13+
# -Subject Common name on the cert. Defaults to "CN=Yogesh Prajapati".
14+
# -OutDir Where to write the .pfx + .cer. Defaults to ./.signing/.
15+
# -ValidYears Cert validity. Defaults to 5.
16+
#
17+
# After running:
18+
# 1. Commit build/Notepp-CodeSigning.cer (the public cert, copied by
19+
# this script).
20+
# 2. In the GitHub repo: Settings -> Secrets and variables -> Actions
21+
# -> New repository secret. Add the two secrets the script prints:
22+
# WIN_CSC_LINK_BASE64 (base64 of the .pfx)
23+
# WIN_CSC_KEY_PASSWORD (the PFX password)
24+
# 3. Tag and push a release. The Windows runner will pick the secrets
25+
# up automatically via release.yml.
26+
# ---------------------------------------------------------------------------
27+
28+
[CmdletBinding()]
29+
param(
30+
[string] $Subject = "CN=Yogesh Prajapati",
31+
[string] $OutDir = (Join-Path $PSScriptRoot "..\.signing"),
32+
[int] $ValidYears = 5
33+
)
34+
35+
$ErrorActionPreference = "Stop"
36+
37+
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
38+
$OutDir = (Resolve-Path -LiteralPath (New-Item -ItemType Directory -Path $OutDir -Force)).Path
39+
$buildDir = Join-Path $repoRoot "build"
40+
if (-not (Test-Path $buildDir)) { New-Item -ItemType Directory -Path $buildDir | Out-Null }
41+
42+
$pfxPath = Join-Path $OutDir "Notepp-CodeSigning.pfx"
43+
$cerPath = Join-Path $OutDir "Notepp-CodeSigning.cer"
44+
$cerRepo = Join-Path $buildDir "Notepp-CodeSigning.cer"
45+
46+
Write-Host ""
47+
Write-Host "Generating self-signed code-signing certificate" -ForegroundColor Cyan
48+
Write-Host " Subject : $Subject"
49+
Write-Host " Valid for : $ValidYears years"
50+
Write-Host " Output dir : $OutDir"
51+
Write-Host ""
52+
53+
$pwd = Read-Host -Prompt "Choose a PFX password (you will need this for the GitHub secret)" -AsSecureString
54+
if ($pwd.Length -eq 0) { throw "Password cannot be empty." }
55+
56+
$cert = New-SelfSignedCertificate `
57+
-Type CodeSigningCert `
58+
-Subject $Subject `
59+
-KeyUsage DigitalSignature `
60+
-KeyAlgorithm RSA `
61+
-KeyLength 3072 `
62+
-HashAlgorithm SHA256 `
63+
-CertStoreLocation "Cert:\CurrentUser\My" `
64+
-NotAfter (Get-Date).AddYears($ValidYears) `
65+
-FriendlyName "Note++ Code Signing"
66+
67+
Write-Host "Created cert. Thumbprint: $($cert.Thumbprint)" -ForegroundColor Green
68+
69+
Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $pwd | Out-Null
70+
Export-Certificate -Cert $cert -FilePath $cerPath | Out-Null
71+
Copy-Item -Path $cerPath -Destination $cerRepo -Force
72+
73+
Write-Host ""
74+
Write-Host "Wrote:"
75+
Write-Host " $pfxPath (private key, DO NOT COMMIT)"
76+
Write-Host " $cerPath (public cert)"
77+
Write-Host " $cerRepo (public cert, copied into build/ for the repo)"
78+
Write-Host ""
79+
80+
$pfxBytes = [System.IO.File]::ReadAllBytes($pfxPath)
81+
$b64 = [System.Convert]::ToBase64String($pfxBytes)
82+
83+
$pwdPlain = [System.Net.NetworkCredential]::new("", $pwd).Password
84+
85+
Write-Host "---------- GitHub Actions secrets ----------" -ForegroundColor Yellow
86+
Write-Host "Add these two repository secrets at:"
87+
Write-Host " https://github.com/YogeshPraj/Note-/settings/secrets/actions"
88+
Write-Host ""
89+
Write-Host "Secret name : WIN_CSC_LINK_BASE64"
90+
Write-Host "Secret value:"
91+
Write-Host $b64
92+
Write-Host ""
93+
Write-Host "Secret name : WIN_CSC_KEY_PASSWORD"
94+
Write-Host "Secret value: $pwdPlain"
95+
Write-Host "--------------------------------------------"
96+
Write-Host ""
97+
Write-Host "Next steps:" -ForegroundColor Cyan
98+
Write-Host " 1. Add both secrets in the GitHub repo settings (link above)."
99+
Write-Host " 2. git add build/Notepp-CodeSigning.cer && git commit"
100+
Write-Host " 3. Tag and push a release. The Windows runner will sign automatically."
101+
Write-Host ""
102+
Write-Host "Verify locally (optional):"
103+
Write-Host " Get-AuthenticodeSignature path\to\notepp-win-x64.exe"
104+
Write-Host ""

0 commit comments

Comments
 (0)