feat(HAI-080): add code signing for macOS and Windows release binaries
- Add macOS signing script with codesign, notarization, and stapling support - Add Windows signing script using signtool with PFX certificate - Integrate signing steps into release and test-release workflows - Add signing workflow tests and verification coverage - Add CODE_SIGNING.md documentation and update README
This commit is contained in:
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
@@ -69,6 +69,42 @@ jobs:
|
||||
- name: Build standalone binary
|
||||
run: pnpm --filter hai build:exe -- --target ${{ matrix.target }}
|
||||
|
||||
- name: Rename binary with platform and arch
|
||||
run: |
|
||||
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH="x64" ;;
|
||||
aarch64) ARCH="arm64" ;;
|
||||
esac
|
||||
cp packages/cli/dist/hai packages/cli/dist/hai-${PLATFORM}-${ARCH}
|
||||
|
||||
# Code signing steps — activate when cross-platform matrix is in place (HAI-079)
|
||||
- name: Sign macOS binaries
|
||||
if: runner.os == 'macOS'
|
||||
env:
|
||||
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
|
||||
run: |
|
||||
for binary in packages/cli/dist/hai-darwin-*; do
|
||||
[ -f "$binary" ] && bash scripts/sign-macos.sh "$binary"
|
||||
done
|
||||
|
||||
- name: Sign Windows binaries
|
||||
if: runner.os == 'Windows'
|
||||
env:
|
||||
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
Get-ChildItem packages/cli/dist/hai-windows-*.exe | ForEach-Object {
|
||||
& .\scripts\sign-windows.ps1 $_.FullName
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Generate checksum (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
|
||||
37
.github/workflows/test-release.yml
vendored
37
.github/workflows/test-release.yml
vendored
@@ -49,6 +49,43 @@ jobs:
|
||||
- name: Build standalone binary
|
||||
run: pnpm --filter hai build:exe -- --target ${{ matrix.target }}
|
||||
|
||||
- name: Rename binary with platform and arch
|
||||
run: |
|
||||
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH="x64" ;;
|
||||
aarch64) ARCH="arm64" ;;
|
||||
esac
|
||||
cp packages/cli/dist/hai packages/cli/dist/hai-${PLATFORM}-${ARCH}
|
||||
|
||||
# Code signing steps — activate when cross-platform matrix is in place (HAI-079)
|
||||
# Signing is skipped gracefully when secrets are not configured
|
||||
- name: Sign macOS binaries
|
||||
if: runner.os == 'macOS' && env.APPLE_CERTIFICATE_BASE64 != ''
|
||||
env:
|
||||
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
|
||||
run: |
|
||||
for binary in packages/cli/dist/hai-darwin-*; do
|
||||
[ -f "$binary" ] && bash scripts/sign-macos.sh "$binary"
|
||||
done
|
||||
|
||||
- name: Sign Windows binaries
|
||||
if: runner.os == 'Windows' && env.WINDOWS_CERTIFICATE_BASE64 != ''
|
||||
env:
|
||||
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
Get-ChildItem packages/cli/dist/hai-windows-*.exe | ForEach-Object {
|
||||
& .\scripts\sign-windows.ps1 $_.FullName
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Generate checksum (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
|
||||
@@ -214,6 +214,8 @@ Download the latest binary from the [GitHub Releases](../../releases) page. Each
|
||||
| macOS x64 (Intel) | `hai-darwin-x64` | `macos-13` |
|
||||
| Windows x64 | `hai-windows-x64.exe` | `windows-latest` |
|
||||
|
||||
macOS and Windows binaries are **code-signed** to avoid OS security warnings (Gatekeeper/SmartScreen). See [docs/CODE_SIGNING.md](docs/CODE_SIGNING.md) for setup details.
|
||||
|
||||
### Triggering a release
|
||||
|
||||
Releases are automated via [changesets](https://github.com/changesets/changesets). See [RELEASING.md](./RELEASING.md) for the full workflow.
|
||||
|
||||
143
docs/CODE_SIGNING.md
Normal file
143
docs/CODE_SIGNING.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Code Signing Setup Guide
|
||||
|
||||
This document explains how to configure code signing for `hai` release binaries so they don't trigger OS security warnings on macOS (Gatekeeper) or Windows (SmartScreen).
|
||||
|
||||
## Overview
|
||||
|
||||
The release workflow automatically signs binaries when the appropriate secrets are configured:
|
||||
|
||||
- **macOS**: Codesign with hardened runtime + Apple notarization
|
||||
- **Windows**: Authenticode signing with timestamp
|
||||
- **Linux**: No signing (no standard code signing requirement for Linux CLI tools)
|
||||
|
||||
Signing is **optional** — if secrets are not configured, the build succeeds and signing steps are skipped.
|
||||
|
||||
## Required GitHub Secrets
|
||||
|
||||
### macOS Signing
|
||||
|
||||
| Secret | Description |
|
||||
|--------|-------------|
|
||||
| `APPLE_CERTIFICATE_BASE64` | Base64-encoded `.p12` Developer ID Application certificate |
|
||||
| `APPLE_CERTIFICATE_PASSWORD` | Password used when exporting the `.p12` certificate |
|
||||
| `APPLE_IDENTITY` | Signing identity string (e.g., `Developer ID Application: Your Name (TEAMID)`) |
|
||||
| `APPLE_ID` | Apple ID email address used for notarization |
|
||||
| `APPLE_TEAM_ID` | Apple Developer Team ID (10-character alphanumeric) |
|
||||
| `APPLE_APP_PASSWORD` | App-specific password for notarization |
|
||||
|
||||
### Windows Signing
|
||||
|
||||
| Secret | Description |
|
||||
|--------|-------------|
|
||||
| `WINDOWS_CERTIFICATE_BASE64` | Base64-encoded `.pfx` Authenticode code signing certificate |
|
||||
| `WINDOWS_CERTIFICATE_PASSWORD` | Password for the `.pfx` certificate |
|
||||
|
||||
## macOS Setup Instructions
|
||||
|
||||
### 1. Obtain a Developer ID Application Certificate
|
||||
|
||||
1. Enroll in the [Apple Developer Program](https://developer.apple.com/programs/)
|
||||
2. In Xcode or the Apple Developer portal, create a **Developer ID Application** certificate
|
||||
3. Export the certificate from Keychain Access as a `.p12` file with a password
|
||||
|
||||
### 2. Encode the Certificate as Base64
|
||||
|
||||
```bash
|
||||
base64 -i certificate.p12 | pbcopy
|
||||
```
|
||||
|
||||
Paste the result as the `APPLE_CERTIFICATE_BASE64` secret.
|
||||
|
||||
### 3. Find Your Team ID
|
||||
|
||||
Your Team ID is visible at [developer.apple.com/account](https://developer.apple.com/account) under Membership Details. It's a 10-character alphanumeric string (e.g., `ABC1234DEF`).
|
||||
|
||||
### 4. Create an App-Specific Password
|
||||
|
||||
1. Go to [appleid.apple.com](https://appleid.apple.com/)
|
||||
2. Sign in and navigate to **Sign-In and Security** → **App-Specific Passwords**
|
||||
3. Generate a new password and label it (e.g., "hai notarization")
|
||||
4. Use this as the `APPLE_APP_PASSWORD` secret
|
||||
|
||||
### 5. Determine Your Signing Identity
|
||||
|
||||
The signing identity looks like:
|
||||
```
|
||||
Developer ID Application: Your Name (TEAMID)
|
||||
```
|
||||
|
||||
You can find it by running:
|
||||
```bash
|
||||
security find-identity -v -p codesigning
|
||||
```
|
||||
|
||||
## Windows Setup Instructions
|
||||
|
||||
### 1. Obtain an Authenticode Code Signing Certificate
|
||||
|
||||
Purchase a code signing certificate from a trusted Certificate Authority:
|
||||
- DigiCert
|
||||
- Sectigo (Comodo)
|
||||
- GlobalSign
|
||||
- SSL.com
|
||||
|
||||
### 2. Export as `.pfx`
|
||||
|
||||
Export the certificate with its private key as a `.pfx` (PKCS#12) file. Set a strong password.
|
||||
|
||||
### 3. Encode the Certificate as Base64
|
||||
|
||||
```powershell
|
||||
[Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx")) | Set-Clipboard
|
||||
```
|
||||
|
||||
Or on Linux/macOS:
|
||||
```bash
|
||||
base64 -i certificate.pfx
|
||||
```
|
||||
|
||||
Paste the result as the `WINDOWS_CERTIFICATE_BASE64` secret.
|
||||
|
||||
## How Signing Works in the Release Flow
|
||||
|
||||
1. A tag push (`v*`) triggers the release workflow
|
||||
2. Each platform job builds the standalone binary
|
||||
3. **macOS jobs**: `scripts/sign-macos.sh` runs codesign + notarization
|
||||
4. **Windows jobs**: `scripts/sign-windows.ps1` runs Authenticode signing
|
||||
5. Checksums are generated **after** signing (so they match the signed binaries)
|
||||
6. Signed binaries and checksums are uploaded to the GitHub Release
|
||||
|
||||
The test-release workflow (`workflow_dispatch`) includes the same signing steps but guards them with secret-availability checks — signing is skipped if secrets are not configured.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### macOS: "The signature of the binary is invalid"
|
||||
|
||||
- Ensure the certificate is a **Developer ID Application** certificate (not Developer ID Installer or Mac App Distribution)
|
||||
- Check that the certificate hasn't expired
|
||||
- Verify the base64 encoding is correct: `echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode | file -`
|
||||
|
||||
### macOS: Notarization fails with "Invalid credentials"
|
||||
|
||||
- Verify `APPLE_ID` is your Apple ID email
|
||||
- Verify `APPLE_APP_PASSWORD` is an app-specific password (not your Apple ID password)
|
||||
- Verify `APPLE_TEAM_ID` matches the team that issued the certificate
|
||||
|
||||
### macOS: Notarization fails with "The software is not signed"
|
||||
|
||||
- Ensure the `--options runtime` flag is used during codesign (hardened runtime is required for notarization)
|
||||
- The `sign-macos.sh` script handles this automatically
|
||||
|
||||
### Windows: "signtool not found"
|
||||
|
||||
- `signtool.exe` is included in the Windows SDK, which is pre-installed on GitHub Actions Windows runners
|
||||
- For local testing, install the Windows SDK or Visual Studio Build Tools
|
||||
|
||||
### Windows: "The specified PFX password is not correct"
|
||||
|
||||
- Double-check the `WINDOWS_CERTIFICATE_PASSWORD` secret matches the password used when exporting the `.pfx`
|
||||
|
||||
### Signing step skipped
|
||||
|
||||
- In the test-release workflow, signing is intentionally skipped when secrets are not configured
|
||||
- Verify the secrets are set at the repository level in **Settings → Secrets and variables → Actions**
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { readFileSync, accessSync, constants } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { parse } from "yaml";
|
||||
|
||||
@@ -203,3 +203,100 @@ describe("Test Release workflow (.github/workflows/test-release.yml)", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Code signing — Release workflow", () => {
|
||||
let content: string;
|
||||
|
||||
beforeAll(() => {
|
||||
const result = loadWorkflow("release.yml");
|
||||
content = result.content;
|
||||
});
|
||||
|
||||
it("contains macOS signing step referencing sign-macos.sh", () => {
|
||||
expect(content).toContain("sign-macos.sh");
|
||||
});
|
||||
|
||||
it("contains Windows signing step referencing sign-windows.ps1", () => {
|
||||
expect(content).toContain("sign-windows.ps1");
|
||||
});
|
||||
|
||||
it("macOS signing step is conditioned on runner.os", () => {
|
||||
expect(content).toMatch(/if:.*runner\.os\s*==\s*'macOS'/);
|
||||
});
|
||||
|
||||
it("Windows signing step is conditioned on runner.os", () => {
|
||||
expect(content).toMatch(/if:.*runner\.os\s*==\s*'Windows'/);
|
||||
});
|
||||
|
||||
it("references all required Apple secrets", () => {
|
||||
const requiredSecrets = [
|
||||
"APPLE_CERTIFICATE_BASE64",
|
||||
"APPLE_CERTIFICATE_PASSWORD",
|
||||
"APPLE_ID",
|
||||
"APPLE_TEAM_ID",
|
||||
"APPLE_APP_PASSWORD",
|
||||
];
|
||||
for (const secret of requiredSecrets) {
|
||||
expect(content).toContain(`secrets.${secret}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("references Windows signing secrets", () => {
|
||||
expect(content).toContain("secrets.WINDOWS_CERTIFICATE_BASE64");
|
||||
expect(content).toContain("secrets.WINDOWS_CERTIFICATE_PASSWORD");
|
||||
});
|
||||
|
||||
it("checksums step comes after signing steps", () => {
|
||||
const signMacosIndex = content.indexOf("sign-macos.sh");
|
||||
const signWindowsIndex = content.indexOf("sign-windows.ps1");
|
||||
const checksumIndex = content.indexOf("Generate checksums");
|
||||
expect(signMacosIndex).toBeLessThan(checksumIndex);
|
||||
expect(signWindowsIndex).toBeLessThan(checksumIndex);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Code signing — Test-release workflow", () => {
|
||||
let content: string;
|
||||
|
||||
beforeAll(() => {
|
||||
const result = loadWorkflow("test-release.yml");
|
||||
content = result.content;
|
||||
});
|
||||
|
||||
it("has macOS signing step with secret-availability guard", () => {
|
||||
expect(content).toContain("sign-macos.sh");
|
||||
expect(content).toMatch(/if:.*APPLE_CERTIFICATE_BASE64\s*!=\s*''/);
|
||||
});
|
||||
|
||||
it("has Windows signing step with secret-availability guard", () => {
|
||||
expect(content).toContain("sign-windows.ps1");
|
||||
expect(content).toMatch(/if:.*WINDOWS_CERTIFICATE_BASE64\s*!=\s*''/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Code signing — Scripts", () => {
|
||||
const scriptsDir = join(workspaceRoot, "scripts");
|
||||
|
||||
it("sign-macos.sh exists and is executable", () => {
|
||||
const scriptPath = join(scriptsDir, "sign-macos.sh");
|
||||
expect(() => accessSync(scriptPath, constants.F_OK)).not.toThrow();
|
||||
expect(() => accessSync(scriptPath, constants.X_OK)).not.toThrow();
|
||||
});
|
||||
|
||||
it("sign-windows.ps1 exists", () => {
|
||||
const scriptPath = join(scriptsDir, "sign-windows.ps1");
|
||||
expect(() => accessSync(scriptPath, constants.F_OK)).not.toThrow();
|
||||
});
|
||||
|
||||
it("sign-macos.sh references codesign, notarytool, and security import", () => {
|
||||
const script = readFileSync(join(scriptsDir, "sign-macos.sh"), "utf-8");
|
||||
expect(script).toContain("codesign");
|
||||
expect(script).toContain("notarytool");
|
||||
expect(script).toContain("security import");
|
||||
});
|
||||
|
||||
it("sign-windows.ps1 references signtool", () => {
|
||||
const script = readFileSync(join(scriptsDir, "sign-windows.ps1"), "utf-8");
|
||||
expect(script).toContain("signtool");
|
||||
});
|
||||
});
|
||||
|
||||
118
scripts/sign-macos.sh
Executable file
118
scripts/sign-macos.sh
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
# sign-macos.sh — Codesign and notarize a macOS binary for Gatekeeper.
|
||||
#
|
||||
# Usage:
|
||||
# APPLE_CERTIFICATE_BASE64=... APPLE_CERTIFICATE_PASSWORD=... \
|
||||
# APPLE_IDENTITY=... APPLE_ID=... APPLE_TEAM_ID=... APPLE_APP_PASSWORD=... \
|
||||
# bash scripts/sign-macos.sh path/to/binary
|
||||
#
|
||||
# Environment variables (all required):
|
||||
# APPLE_CERTIFICATE_BASE64 — Base64-encoded .p12 Developer ID Application certificate
|
||||
# APPLE_CERTIFICATE_PASSWORD — Password for the .p12 certificate
|
||||
# APPLE_IDENTITY — Signing identity (e.g., "Developer ID Application: Your Name (TEAMID)")
|
||||
# APPLE_ID — Apple ID email for notarization
|
||||
# APPLE_TEAM_ID — Apple Developer Team ID
|
||||
# APPLE_APP_PASSWORD — App-specific password for notarization
|
||||
#
|
||||
# The script is idempotent — re-running on an already-signed binary will re-sign it.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Validate arguments ────────────────────────────────────────────────
|
||||
BINARY="${1:-}"
|
||||
if [[ -z "$BINARY" ]]; then
|
||||
echo "ERROR: No binary path provided."
|
||||
echo "Usage: $0 <path-to-binary>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$BINARY" ]]; then
|
||||
echo "ERROR: Binary not found: $BINARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Validate environment variables ────────────────────────────────────
|
||||
REQUIRED_VARS=(
|
||||
APPLE_CERTIFICATE_BASE64
|
||||
APPLE_CERTIFICATE_PASSWORD
|
||||
APPLE_IDENTITY
|
||||
APPLE_ID
|
||||
APPLE_TEAM_ID
|
||||
APPLE_APP_PASSWORD
|
||||
)
|
||||
|
||||
for var in "${REQUIRED_VARS[@]}"; do
|
||||
if [[ -z "${!var:-}" ]]; then
|
||||
echo "ERROR: Required environment variable $var is not set."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "==> Signing macOS binary: $BINARY"
|
||||
|
||||
# ── Set up temporary keychain ─────────────────────────────────────────
|
||||
KEYCHAIN_NAME="signing-$(date +%s).keychain-db"
|
||||
KEYCHAIN_PASSWORD="$(openssl rand -hex 16)"
|
||||
CERT_FILE="$(mktemp -t cert.XXXXXX).p12"
|
||||
|
||||
cleanup() {
|
||||
echo "==> Cleaning up..."
|
||||
security delete-keychain "$KEYCHAIN_NAME" 2>/dev/null || true
|
||||
rm -f "$CERT_FILE"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Decode certificate from base64
|
||||
echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > "$CERT_FILE"
|
||||
|
||||
# Create temporary keychain
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
|
||||
security set-keychain-settings -lut 900 "$KEYCHAIN_NAME"
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
|
||||
|
||||
# Import certificate into keychain
|
||||
security import "$CERT_FILE" \
|
||||
-k "$KEYCHAIN_NAME" \
|
||||
-P "$APPLE_CERTIFICATE_PASSWORD" \
|
||||
-T /usr/bin/codesign \
|
||||
-T /usr/bin/security
|
||||
|
||||
# Allow codesign to access the keychain without UI prompt
|
||||
security set-key-partition-list -S "apple-tool:,apple:,codesign:" \
|
||||
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
|
||||
|
||||
# Add temporary keychain to the search list
|
||||
security list-keychains -d user -s "$KEYCHAIN_NAME" $(security list-keychains -d user | tr -d '"')
|
||||
|
||||
echo "==> Keychain configured."
|
||||
|
||||
# ── Codesign the binary ───────────────────────────────────────────────
|
||||
echo "==> Codesigning with identity: $APPLE_IDENTITY"
|
||||
codesign --force --options runtime --sign "$APPLE_IDENTITY" \
|
||||
--keychain "$KEYCHAIN_NAME" \
|
||||
"$BINARY"
|
||||
|
||||
echo "==> Codesign complete. Verifying..."
|
||||
codesign --verify --verbose "$BINARY"
|
||||
|
||||
# ── Notarize the binary ──────────────────────────────────────────────
|
||||
echo "==> Preparing for notarization..."
|
||||
ZIP_FILE="$(mktemp -t notarize.XXXXXX).zip"
|
||||
trap 'rm -f "$ZIP_FILE"; cleanup' EXIT
|
||||
|
||||
# Create a ZIP for notarization submission
|
||||
ditto -c -k --keepParent "$BINARY" "$ZIP_FILE"
|
||||
|
||||
echo "==> Submitting to Apple notarization service..."
|
||||
xcrun notarytool submit "$ZIP_FILE" \
|
||||
--apple-id "$APPLE_ID" \
|
||||
--team-id "$APPLE_TEAM_ID" \
|
||||
--password "$APPLE_APP_PASSWORD" \
|
||||
--wait
|
||||
|
||||
echo "==> Notarization complete."
|
||||
|
||||
# Clean up ZIP
|
||||
rm -f "$ZIP_FILE"
|
||||
|
||||
echo "==> ✓ Binary signed and notarized: $BINARY"
|
||||
78
scripts/sign-windows.ps1
Normal file
78
scripts/sign-windows.ps1
Normal file
@@ -0,0 +1,78 @@
|
||||
# sign-windows.ps1 — Authenticode sign a Windows binary.
|
||||
#
|
||||
# Usage:
|
||||
# $env:WINDOWS_CERTIFICATE_BASE64 = "..."
|
||||
# $env:WINDOWS_CERTIFICATE_PASSWORD = "..."
|
||||
# pwsh scripts/sign-windows.ps1 path\to\binary.exe
|
||||
#
|
||||
# Environment variables (all required):
|
||||
# WINDOWS_CERTIFICATE_BASE64 — Base64-encoded .pfx code signing certificate
|
||||
# WINDOWS_CERTIFICATE_PASSWORD — Password for the .pfx certificate
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory = $true, Position = 0)]
|
||||
[string]$BinaryPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# ── Validate arguments ────────────────────────────────────────────────
|
||||
if (-not (Test-Path $BinaryPath)) {
|
||||
Write-Error "ERROR: Binary not found: $BinaryPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── Validate environment variables ────────────────────────────────────
|
||||
$requiredVars = @("WINDOWS_CERTIFICATE_BASE64", "WINDOWS_CERTIFICATE_PASSWORD")
|
||||
foreach ($var in $requiredVars) {
|
||||
if (-not [System.Environment]::GetEnvironmentVariable($var)) {
|
||||
Write-Error "ERROR: Required environment variable $var is not set."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "==> Signing Windows binary: $BinaryPath"
|
||||
|
||||
# ── Decode certificate to temporary file ──────────────────────────────
|
||||
$certFile = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "signing-cert-$(Get-Random).pfx")
|
||||
|
||||
try {
|
||||
# Decode base64 certificate
|
||||
$certBytes = [System.Convert]::FromBase64String($env:WINDOWS_CERTIFICATE_BASE64)
|
||||
[System.IO.File]::WriteAllBytes($certFile, $certBytes)
|
||||
Write-Host "==> Certificate decoded to temporary file."
|
||||
|
||||
# ── Sign the binary ───────────────────────────────────────────────
|
||||
Write-Host "==> Signing with signtool..."
|
||||
& signtool sign `
|
||||
/f $certFile `
|
||||
/p $env:WINDOWS_CERTIFICATE_PASSWORD `
|
||||
/tr http://timestamp.digicert.com `
|
||||
/td sha256 `
|
||||
/fd sha256 `
|
||||
$BinaryPath
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "ERROR: signtool sign failed with exit code $LASTEXITCODE"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "==> Sign complete. Verifying..."
|
||||
|
||||
# ── Verify the signature ──────────────────────────────────────────
|
||||
& signtool verify /pa $BinaryPath
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "ERROR: signtool verify failed with exit code $LASTEXITCODE"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "==> ✓ Binary signed and verified: $BinaryPath"
|
||||
}
|
||||
finally {
|
||||
# ── Clean up temporary certificate file ───────────────────────────
|
||||
if (Test-Path $certFile) {
|
||||
Remove-Item -Force $certFile
|
||||
Write-Host "==> Temporary certificate file removed."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user