mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
feat(desktop): add Windows code signing
This commit is contained in:
146
desktop/electron-builder.win.js
Normal file
146
desktop/electron-builder.win.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Dynamic electron-builder config for WINDOWS code signing.
|
||||
*
|
||||
* Mirrors electron-builder.js (which handles mac.binaries) but for Windows.
|
||||
* It wires the Racent remote code-signing CLI into electron-builder so that
|
||||
* every produced .exe (the app launcher + the NSIS installer) is signed in the
|
||||
* cloud — the EV private key never leaves the CA's HSM, satisfying the post-2023
|
||||
* "private key must live in hardware" rule.
|
||||
*
|
||||
* Two signing passes are needed:
|
||||
* 1. win.signtoolOptions.sign (customSign) — electron-builder calls this for
|
||||
* each artifact it produces (app .exe, uninstaller, NSIS installer).
|
||||
* 2. afterPack — signs the PyInstaller backend (cowagent-backend.exe) that is
|
||||
* copied in via extraResources. electron-builder's sign hook only touches
|
||||
* its OWN outputs, so the nested backend exe must be signed here, BEFORE
|
||||
* the installer is assembled, so the signed backend ends up inside it.
|
||||
*
|
||||
* VENDOR PRIVACY: the CLI path and all credentials come from env vars only.
|
||||
* Nothing in this file (or the public workflow) names the reseller, so a public
|
||||
* repo never leaks which signing vendor we use.
|
||||
*
|
||||
* DRY-RUN / SKIP: when SIGNTOOL_CERT_CODE is absent we skip signing entirely
|
||||
* (unsigned dev/dry builds keep working). When COW_SIGN_DRY_RUN=1 we pass
|
||||
* --dry-run so the WHOLE pipeline can be validated in CI with a self-signed
|
||||
* cert, WITHOUT a real certificate and WITHOUT consuming any signing quota.
|
||||
*/
|
||||
const { execFileSync } = require('child_process')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const config = require('./package.json').build
|
||||
|
||||
// Absolute path to the Racent signtool CLI on the runner. Injected by CI so
|
||||
// this file never hardcodes a vendor download URL. e.g. C:\signtool\signtool.exe
|
||||
const SIGNTOOL = process.env.SIGNTOOL_PATH || ''
|
||||
const ACCESS_KEY = process.env.SIGNTOOL_ACCESS_KEY || ''
|
||||
const ACCESS_SECRET = process.env.SIGNTOOL_ACCESS_SECRET || ''
|
||||
const CERT_CODE = process.env.SIGNTOOL_CERT_CODE || ''
|
||||
// Dry-run validates the pipeline with a self-signed cert (no quota, no real
|
||||
// cert needed). Any truthy value enables it.
|
||||
const DRY_RUN = !!process.env.COW_SIGN_DRY_RUN
|
||||
|
||||
// RFC3161 timestamp server for SHA256. Microsoft's is reliable from CI runners
|
||||
// worldwide; overridable via env if needed.
|
||||
const TIMESTAMP = process.env.SIGNTOOL_TIMESTAMP || 'http://timestamp.acs.microsoft.com'
|
||||
|
||||
// Signing is possible when we have the CLI plus either a real cert code or
|
||||
// explicit dry-run mode (dry-run accepts placeholder credentials).
|
||||
function canSign() {
|
||||
if (!SIGNTOOL || !fs.existsSync(SIGNTOOL)) return false
|
||||
if (DRY_RUN) return true
|
||||
return !!(ACCESS_KEY && ACCESS_SECRET && CERT_CODE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a single file in place using the Racent CLI. The CLI writes to a
|
||||
* separate --out path (it refuses to overwrite an existing file), so we sign to
|
||||
* a temp file and atomically move it back over the original.
|
||||
*/
|
||||
function signFile(filePath) {
|
||||
const tmpOut = `${filePath}.signed`
|
||||
// Remove a stale temp from a previous failed run (CLI errors if --out exists).
|
||||
try {
|
||||
if (fs.existsSync(tmpOut)) fs.rmSync(tmpOut)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
const args = [
|
||||
'sign',
|
||||
...(DRY_RUN ? ['--dry-run'] : []),
|
||||
`--access-key=${ACCESS_KEY}`,
|
||||
`--access-secret=${ACCESS_SECRET}`,
|
||||
`--cert-code=${CERT_CODE}`,
|
||||
`--file=${filePath}`,
|
||||
`--out=${tmpOut}`,
|
||||
'--sha1=false',
|
||||
'--sha2=true',
|
||||
'--timestamp-rfc3161',
|
||||
TIMESTAMP,
|
||||
]
|
||||
|
||||
// Never print credentials: log only the file being signed.
|
||||
console.log(`[win-sign] signing ${path.basename(filePath)}${DRY_RUN ? ' (dry-run)' : ''}`)
|
||||
execFileSync(SIGNTOOL, args, { stdio: ['ignore', 'inherit', 'inherit'] })
|
||||
|
||||
if (!fs.existsSync(tmpOut)) {
|
||||
throw new Error(`[win-sign] signed output not produced for ${filePath}`)
|
||||
}
|
||||
// Replace the original with the signed copy.
|
||||
fs.rmSync(filePath)
|
||||
fs.renameSync(tmpOut, filePath)
|
||||
}
|
||||
|
||||
// electron-builder calls this for each artifact it generates (app exe, NSIS
|
||||
// installer, uninstaller). Signature: (configuration) => void, where
|
||||
// configuration.path is the file to sign.
|
||||
async function customSign(configuration) {
|
||||
if (!canSign()) {
|
||||
console.warn('[win-sign] signing skipped (no signtool/credentials)')
|
||||
return
|
||||
}
|
||||
signFile(configuration.path)
|
||||
}
|
||||
|
||||
// Recursively collect .exe/.dll under the packed backend dir so nested native
|
||||
// binaries get signed too (Defender flags PyInstaller bundles most often).
|
||||
function collectSignables(dir) {
|
||||
const out = []
|
||||
const walk = (d) => {
|
||||
for (const name of fs.readdirSync(d)) {
|
||||
const full = path.join(d, name)
|
||||
const st = fs.lstatSync(full)
|
||||
if (st.isSymbolicLink()) continue
|
||||
if (st.isDirectory()) {
|
||||
walk(full)
|
||||
continue
|
||||
}
|
||||
if (/\.(exe|dll)$/i.test(name)) out.push(full)
|
||||
}
|
||||
}
|
||||
walk(dir)
|
||||
return out
|
||||
}
|
||||
|
||||
// Sign the PyInstaller backend BEFORE the installer is built, so the signed
|
||||
// backend is what gets packaged. context.appOutDir is the unpacked app dir.
|
||||
async function afterPack(context) {
|
||||
if (process.platform !== 'win32') return
|
||||
if (!canSign()) return
|
||||
const backendDir = path.join(context.appOutDir, 'resources', 'backend', 'cowagent-backend')
|
||||
if (!fs.existsSync(backendDir)) {
|
||||
console.warn(`[win-sign] backend dir not found, skipping backend signing: ${backendDir}`)
|
||||
return
|
||||
}
|
||||
const files = collectSignables(backendDir)
|
||||
console.log(`[win-sign] signing ${files.length} backend binaries`)
|
||||
for (const f of files) signFile(f)
|
||||
}
|
||||
|
||||
// Extend the base config: attach the sign hook + afterPack. Only meaningful on
|
||||
// Windows builds (this config is only passed via --config on the win matrix leg).
|
||||
config.win = { ...config.win, signtoolOptions: { sign: customSign, signingHashAlgorithms: ['sha256'] } }
|
||||
config.afterPack = afterPack
|
||||
|
||||
module.exports = config
|
||||
Reference in New Issue
Block a user