diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c2192419..5e05c60f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,10 +26,10 @@ jobs: with: go-version: '1.23' - - name: Install cross-compilation toolchains + - name: Install cross-compilation toolchains and signing tools run: | sudo apt-get update - sudo apt-get install -y gcc-mingw-w64-x86-64 gcc-aarch64-linux-gnu + sudo apt-get install -y gcc-mingw-w64-x86-64 gcc-aarch64-linux-gnu osslsigncode - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 @@ -39,6 +39,9 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Windows code signing (optional - signing is skipped if not set) + WINDOWS_SIGNING_CERT_PFX_BASE64: ${{ secrets.WINDOWS_SIGNING_CERT_PFX_BASE64 }} + WINDOWS_SIGNING_CERT_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERT_PASSWORD }} publish-pypi: runs-on: ubuntu-latest diff --git a/.goreleaser.yml b/.goreleaser.yml index ff867336..3f97adab 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -93,6 +93,11 @@ builds: - -X main.Commit={{.Commit}} - -X main.Branch={{.Branch}} - -buildmode=exe + hooks: + post: + # Sign Windows executable with Authenticode certificate + # Requires WINDOWS_SIGNING_CERT_PFX_BASE64 and WINDOWS_SIGNING_CERT_PASSWORD secrets + - ./scripts/sign-windows.sh "{{ .Path }}" archives: - id: bd-archive diff --git a/docs/ANTIVIRUS.md b/docs/ANTIVIRUS.md index 4c9f4cb5..20af618c 100644 --- a/docs/ANTIVIRUS.md +++ b/docs/ANTIVIRUS.md @@ -101,13 +101,34 @@ ldflags: These flags are already applied in the official builds. -### Code Signing (Future) +### Code Signing -Future releases may include Windows code signing to improve trust scores with antivirus vendors. Code signing: +Windows releases are signed with an Authenticode certificate when available. Code signing: - Reduces false positive rates over time - Builds reputation with SmartScreen/antivirus vendors - Provides tamper verification +**Verify a signed binary (Windows PowerShell):** +```powershell +# Check if the binary is signed +Get-AuthenticodeSignature .\bd.exe + +# Expected output for signed binary: +# SignerCertificate: [Certificate details] +# Status: Valid +``` + +**Verify a signed binary (Linux/macOS with osslsigncode):** +```bash +# Install osslsigncode if not available +# Ubuntu/Debian: apt-get install osslsigncode +# macOS: brew install osslsigncode + +osslsigncode verify -in bd.exe +``` + +**Note:** Code signing requires an EV (Extended Validation) certificate, which involves a verification process. If a release is not signed, it means the certificate was not available at build time. Follow the checksum verification steps above to verify authenticity. + ### Alternative Build Methods Some users report success with: @@ -136,12 +157,16 @@ The issue isn't specific to beads' code - it's a characteristic of Go binaries i ### Will this be fixed in future releases? -We're working on: -- Submitting beads to antivirus vendor whitelists -- Adding code signing for Windows releases -- Continuing to use build optimizations +We've implemented: +- **Code signing infrastructure** for Windows releases (requires EV certificate) +- **Build optimizations** to reduce heuristic triggers +- **Documentation** for users to add exclusions and report false positives -However, false positives may still occur with new releases until reputation is established. +Still in progress: +- Acquiring an EV code signing certificate +- Submitting beads to antivirus vendor whitelists + +False positives may still occur with new releases until the certificate builds reputation with antivirus vendors. This typically takes several months of consistent signed releases. ### Should I disable my antivirus? diff --git a/scripts/README.md b/scripts/README.md index ad502635..38a0a2fe 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -187,6 +187,56 @@ This script fixes all those issues and is now used by `release.sh`. --- +## sign-windows.sh + +Signs Windows executables with an Authenticode certificate using osslsigncode. + +### Usage + +```bash +# Sign a Windows executable +./scripts/sign-windows.sh path/to/bd.exe + +# Environment variables required for signing: +export WINDOWS_SIGNING_CERT_PFX_BASE64="" +export WINDOWS_SIGNING_CERT_PASSWORD="" +``` + +### What It Does + +This script is called automatically by GoReleaser during the release process: + +1. **Decodes** the PFX certificate from base64 +2. **Signs** the Windows executable using osslsigncode +3. **Timestamps** the signature using DigiCert's RFC3161 server +4. **Replaces** the original binary with the signed version +5. **Verifies** the signature was applied correctly + +### Prerequisites + +- `osslsigncode` installed (`apt install osslsigncode` or `brew install osslsigncode`) +- EV code signing certificate exported as PFX file +- GitHub secrets configured: + - `WINDOWS_SIGNING_CERT_PFX_BASE64` - base64-encoded PFX file + - `WINDOWS_SIGNING_CERT_PASSWORD` - certificate password + +### Graceful Degradation + +If the signing secrets are not configured: +- The script prints a warning and exits successfully +- GoReleaser continues without signing +- The release proceeds with unsigned Windows binaries + +This allows releases to work before a certificate is acquired. + +### Why This Script Exists + +Windows code signing helps reduce antivirus false positives that affect Go binaries. +Kaspersky and other AV software commonly flag unsigned Go executables as potentially +malicious due to heuristic detection. See `docs/ANTIVIRUS.md` for details. + +--- + ## Future Scripts Additional maintenance scripts may be added here as needed. diff --git a/scripts/sign-windows.sh b/scripts/sign-windows.sh new file mode 100755 index 00000000..77fb4c04 --- /dev/null +++ b/scripts/sign-windows.sh @@ -0,0 +1,122 @@ +#!/bin/bash +# sign-windows.sh - Sign Windows executables using osslsigncode +# +# This script signs Windows binaries with an Authenticode certificate. +# It's designed to be called from GoReleaser hooks or CI/CD pipelines. +# +# Required environment variables: +# WINDOWS_SIGNING_CERT_PFX_BASE64 - Base64-encoded PFX certificate file +# WINDOWS_SIGNING_CERT_PASSWORD - Password for the PFX certificate +# +# Optional environment variables: +# TIMESTAMP_SERVER - RFC3161 timestamp server URL (default: DigiCert) +# +# Usage: +# ./sign-windows.sh +# ./sign-windows.sh dist/bd-windows-amd64_windows_amd64_v1/bd.exe +# +# For GoReleaser integration, add to .goreleaser.yml: +# builds: +# - id: bd-windows-amd64 +# hooks: +# post: +# - ./scripts/sign-windows.sh "{{ .Path }}" + +set -euo pipefail + +# Configuration +TIMESTAMP_SERVER="${TIMESTAMP_SERVER:-http://timestamp.digicert.com}" +CERT_FILE="/tmp/signing-cert.pfx" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +cleanup() { + # Remove temporary certificate file + if [[ -f "$CERT_FILE" ]]; then + rm -f "$CERT_FILE" + fi +} + +trap cleanup EXIT + +# Check for required argument +if [[ $# -lt 1 ]]; then + log_error "Usage: $0 " + exit 1 +fi + +EXE_PATH="$1" + +# Verify file exists +if [[ ! -f "$EXE_PATH" ]]; then + log_error "File not found: $EXE_PATH" + exit 1 +fi + +# Check for required environment variables +if [[ -z "${WINDOWS_SIGNING_CERT_PFX_BASE64:-}" ]]; then + log_warn "WINDOWS_SIGNING_CERT_PFX_BASE64 not set - skipping code signing" + log_info "To enable Windows code signing:" + log_info " 1. Obtain an EV code signing certificate" + log_info " 2. Export it as a PFX file" + log_info " 3. Base64 encode: base64 -i cert.pfx" + log_info " 4. Add as GitHub secret: WINDOWS_SIGNING_CERT_PFX_BASE64" + exit 0 +fi + +if [[ -z "${WINDOWS_SIGNING_CERT_PASSWORD:-}" ]]; then + log_error "WINDOWS_SIGNING_CERT_PASSWORD not set" + exit 1 +fi + +# Check for osslsigncode +if ! command -v osslsigncode &> /dev/null; then + log_error "osslsigncode not found. Install it with:" + log_error " Ubuntu/Debian: apt-get install osslsigncode" + log_error " macOS: brew install osslsigncode" + exit 1 +fi + +log_info "Signing Windows executable: $EXE_PATH" + +# Decode certificate from base64 +echo "$WINDOWS_SIGNING_CERT_PFX_BASE64" | base64 -d > "$CERT_FILE" + +# Sign the executable +osslsigncode sign \ + -pkcs12 "$CERT_FILE" \ + -pass "$WINDOWS_SIGNING_CERT_PASSWORD" \ + -n "beads - AI-supervised issue tracker" \ + -i "https://github.com/steveyegge/beads" \ + -t "$TIMESTAMP_SERVER" \ + -in "$EXE_PATH" \ + -out "${EXE_PATH}.signed" + +# Replace original with signed version +mv "${EXE_PATH}.signed" "$EXE_PATH" + +log_info "Successfully signed: $EXE_PATH" + +# Verify the signature +log_info "Verifying signature..." +osslsigncode verify -in "$EXE_PATH" || { + log_warn "Signature verification returned non-zero (may still be valid)" +} + +log_info "Windows code signing complete"