From 16f9ac2ab8c64c739e0d07f5622a51649836075c Mon Sep 17 00:00:00 2001 From: Developer Date: Fri, 10 Apr 2026 18:02:46 -0700 Subject: [PATCH] Add code signing config for Windows (Azure Artifact Signing) and macOS (Apple notarization) CI workflows now support code signing when secrets are configured: - macOS: Apple Developer certificate + App Store Connect API key for notarization - Windows: Azure Artifact Signing via signtool + dlib - Both are no-ops when secrets aren't set (backwards-compatible) - Add Entitlements.plist (mic, network) and Info.plist (NSMicrophoneUsageDescription) - Add SIGNING.md with full setup guide for both platforms Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitea/workflows/build-app-macos.yml | 23 +++++ .gitea/workflows/build-app-windows.yml | 37 +++++++ CLAUDE.md | 20 ++++ SIGNING.md | 136 +++++++++++++++++++++++++ src-tauri/Entitlements.plist | 14 +++ src-tauri/Info.plist | 8 ++ src-tauri/tauri.conf.json | 9 +- 7 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 SIGNING.md create mode 100644 src-tauri/Entitlements.plist create mode 100644 src-tauri/Info.plist diff --git a/.gitea/workflows/build-app-macos.yml b/.gitea/workflows/build-app-macos.yml index c5d2a25..240458f 100644 --- a/.gitea/workflows/build-app-macos.yml +++ b/.gitea/workflows/build-app-macos.yml @@ -39,7 +39,27 @@ jobs: - name: Install npm dependencies run: npm ci + - name: Setup code signing + env: + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }} + run: | + if [ -n "${APPLE_API_KEY_CONTENT}" ]; then + echo "Setting up notarization API key..." + mkdir -p ~/private_keys + echo "${APPLE_API_KEY_CONTENT}" > ~/private_keys/AuthKey_${APPLE_API_KEY}.p8 + else + echo "No signing secrets configured, skipping code signing setup" + fi + - name: Build Tauri app + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + APPLE_API_KEY_PATH: ~/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8 run: npm run tauri build - name: Upload to release @@ -91,3 +111,6 @@ jobs: "${REPO_API}/releases/${RELEASE_ID}/assets?name=${encoded_name}") echo "Upload response: HTTP ${HTTP_CODE}" done + + - name: Cleanup signing artifacts + run: rm -rf ~/private_keys diff --git a/.gitea/workflows/build-app-windows.yml b/.gitea/workflows/build-app-windows.yml index 18d6cca..fe761d9 100644 --- a/.gitea/workflows/build-app-windows.yml +++ b/.gitea/workflows/build-app-windows.yml @@ -46,8 +46,45 @@ jobs: shell: powershell run: npm ci + - name: Setup Azure Artifact Signing + shell: powershell + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }} + AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }} + AZURE_CERT_PROFILE: ${{ secrets.AZURE_CERT_PROFILE }} + run: | + if (-not $env:AZURE_CLIENT_ID) { + Write-Host "No Azure signing secrets configured, skipping code signing setup" + return + } + + Write-Host "Setting up Azure Artifact Signing..." + + # Install Artifact Signing client tools + nuget install Microsoft.ArtifactSigning.Client -x -OutputDirectory .\signing-tools + $dlibPath = (Resolve-Path ".\signing-tools\Microsoft.ArtifactSigning.Client*\bin\x64\Azure.CodeSigning.Dlib.dll").Path + + # Write metadata.json + @{ + Endpoint = $env:AZURE_SIGNING_ENDPOINT + CodeSigningAccountName = $env:AZURE_SIGNING_ACCOUNT + CertificateProfileName = $env:AZURE_CERT_PROFILE + } | ConvertTo-Json | Out-File -Encoding UTF8 metadata.json + $metadataPath = (Resolve-Path "metadata.json").Path + + # Inject signCommand into tauri.conf.json for this build + $conf = Get-Content src-tauri\tauri.conf.json -Raw | ConvertFrom-Json + $signCmd = "signtool.exe sign /v /fd SHA256 /tr http://timestamp.acs.microsoft.com /td SHA256 /dlib `"$dlibPath`" /dmdf `"$metadataPath`" %1" + $conf.bundle.windows | Add-Member -NotePropertyName "signCommand" -NotePropertyValue $signCmd -Force + $conf | ConvertTo-Json -Depth 10 | Set-Content src-tauri\tauri.conf.json -Encoding UTF8 + - name: Build Tauri app shell: powershell + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} run: npm run tauri build - name: Upload to release diff --git a/CLAUDE.md b/CLAUDE.md index 8c6ba6d..9686870 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -275,9 +275,29 @@ All per-OS build workflows can be re-run independently via `workflow_dispatch` w - `Info.plist` must include `NSMicrophoneUsageDescription` for mic access - No CUDA builds — CPU/MPS only +## Code Signing + +Code signing is configured for Windows and macOS to eliminate install warnings (SmartScreen / Gatekeeper). See [SIGNING.md](SIGNING.md) for full setup details. + +**Status (as of 2026-04-10):** CI workflow changes are committed. Waiting on identity verification for both platforms before secrets can be configured. + +**How it works:** +- macOS: Tauri auto-signs when `APPLE_CERTIFICATE` and related env vars are set in CI. Notarization uses App Store Connect API key. +- Windows: Azure Artifact Signing via `signtool.exe` + dlib. CI workflow injects `signCommand` into `tauri.conf.json` at build time when `AZURE_CLIENT_ID` is set. +- Both are no-ops when secrets aren't configured — unsigned builds work as before. + +**Key files:** +- `src-tauri/Entitlements.plist` — macOS hardened runtime entitlements (mic, network) +- `src-tauri/Info.plist` — macOS microphone usage description +- `.gitea/workflows/build-app-macos.yml` — Apple signing + notarization +- `.gitea/workflows/build-app-windows.yml` — Azure Artifact Signing + +**Secrets required (12 total):** See [SIGNING.md](SIGNING.md) for the full list — 6 Apple secrets, 6 Azure secrets. + ## Related Documentation - [README.md](README.md) — User-facing documentation - [BUILD.md](BUILD.md) — Detailed build instructions - [INSTALL.md](INSTALL.md) — Installation guide +- [SIGNING.md](SIGNING.md) — Code signing setup guide - [server/nodejs/README.md](server/nodejs/README.md) — Node.js server setup diff --git a/SIGNING.md b/SIGNING.md new file mode 100644 index 0000000..98fa4e6 --- /dev/null +++ b/SIGNING.md @@ -0,0 +1,136 @@ +# Code Signing Setup + +This document explains how to configure code signing for Local Transcription so that Windows and macOS installers are trusted by the operating system. + +## Overview + +Without code signing: +- **Windows**: SmartScreen shows "Windows protected your PC" warnings +- **macOS**: Gatekeeper blocks the app — "app can't be opened because it is from an unidentified developer" + +The CI/CD workflows are configured to sign automatically when the required secrets are present. Without secrets, builds still work — they just produce unsigned installers. + +--- + +## Windows — Azure Artifact Signing + +**Cost**: ~$9.99/month (up to 5,000 signatures) + +### 1. Create an Azure Account + +Sign up at https://azure.microsoft.com if you don't already have one. + +### 2. Set Up Artifact Signing + +1. In the Azure Portal, search for **Artifact Signing** +2. Create a new **Artifact Signing Account** + - Choose a region (e.g., West US 2) — note this for the endpoint URL + - The endpoint will be like `https://wus2.codesigning.azure.net/` +3. Complete **Identity Verification** (required before you can create certificate profiles) +4. Create a **Certificate Profile** with type "Public Trust" for code signing + +### 3. Create an App Registration (Service Principal) + +This allows CI to authenticate to Azure: + +1. Go to **Azure Active Directory** > **App registrations** > **New registration** +2. Name it (e.g., `local-transcription-signing`) +3. After creation, note the **Application (client) ID** and **Directory (tenant) ID** +4. Go to **Certificates & secrets** > **New client secret** — note the secret value +5. Grant the app registration the **Artifact Signing Certificate Profile Signer** role on your Artifact Signing Account + +### 4. Add Gitea Secrets + +In your Gitea repository, go to **Settings** > **Actions** > **Secrets** and add: + +| Secret Name | Value | +|-------------|-------| +| `AZURE_CLIENT_ID` | App registration Application (client) ID | +| `AZURE_CLIENT_SECRET` | App registration client secret value | +| `AZURE_TENANT_ID` | Directory (tenant) ID | +| `AZURE_SIGNING_ENDPOINT` | Artifact Signing endpoint URL (e.g., `https://wus2.codesigning.azure.net/`) | +| `AZURE_SIGNING_ACCOUNT` | Artifact Signing account name | +| `AZURE_CERT_PROFILE` | Certificate profile name | + +--- + +## macOS — Apple Developer Code Signing + Notarization + +**Cost**: $99/year (Apple Developer Program) + +### 1. Enroll in the Apple Developer Program + +Sign up at https://developer.apple.com/programs/ + +### 2. Create a Developer ID Certificate + +1. Open **Xcode** > **Settings** > **Accounts** > select your team > **Manage Certificates** +2. Click **+** > **Developer ID Application** +3. Or create via the Apple Developer portal: **Certificates, Identifiers & Profiles** > **Certificates** > **+** > **Developer ID Application** + +### 3. Export the Certificate as .p12 + +1. Open **Keychain Access** +2. Find your **Developer ID Application** certificate +3. Right-click > **Export** > save as `.p12` with a password +4. Base64-encode it: + ```bash + base64 -i certificate.p12 | tr -d '\n' + ``` + +### 4. Create an App Store Connect API Key + +This is used for notarization (submitting the app to Apple for verification): + +1. Go to https://appstoreconnect.apple.com/access/integrations/api +2. Click **Generate API Key** +3. Give it a name and **Developer** role (minimum) +4. Download the `.p8` private key file (you can only download it once) +5. Note the **Key ID** and **Issuer ID** shown on the page + +### 5. Find Your Signing Identity + +Your signing identity looks like: +``` +Developer ID Application: Your Name (TEAMID) +``` + +You can find it by running: +```bash +security find-identity -v -p codesigning +``` + +### 6. Add Gitea Secrets + +| Secret Name | Value | +|-------------|-------| +| `APPLE_CERTIFICATE` | Base64-encoded .p12 certificate (from step 3) | +| `APPLE_CERTIFICATE_PASSWORD` | Password used when exporting the .p12 | +| `APPLE_SIGNING_IDENTITY` | Full identity string (e.g., `Developer ID Application: Your Name (TEAMID)`) | +| `APPLE_API_KEY` | App Store Connect API Key ID | +| `APPLE_API_ISSUER` | API issuer UUID | +| `APPLE_API_KEY_CONTENT` | Full contents of the `.p8` private key file | + +--- + +## Verifying Signing Works + +### Trigger a Build + +Both build workflows use `workflow_dispatch`, so you can trigger them manually in Gitea: + +1. Go to **Actions** > select the workflow > **Run workflow** +2. Enter the release tag (e.g., `v2.0.15`) + +### Check macOS + +After installing the `.dmg`, the app should open without any Gatekeeper warnings. You can also verify from the command line: + +```bash +codesign -dv --verbose=4 /Applications/Local\ Transcription.app +spctl --assess --type execute /Applications/Local\ Transcription.app +``` + +### Check Windows + +After running the `.msi` or `-setup.exe`, there should be no SmartScreen warning. The installer properties should show your organization name as the publisher. diff --git a/src-tauri/Entitlements.plist b/src-tauri/Entitlements.plist new file mode 100644 index 0000000..2e17b83 --- /dev/null +++ b/src-tauri/Entitlements.plist @@ -0,0 +1,14 @@ + + + + + com.apple.security.device.audio-input + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.cs.allow-unsigned-executable-memory + + + diff --git a/src-tauri/Info.plist b/src-tauri/Info.plist new file mode 100644 index 0000000..f806533 --- /dev/null +++ b/src-tauri/Info.plist @@ -0,0 +1,8 @@ + + + + + NSMicrophoneUsageDescription + Local Transcription needs microphone access for real-time speech-to-text transcription. + + diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2a8b07d..d162a40 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -33,7 +33,14 @@ "icons/icon.icns", "icons/icon.ico", "icons/icon.png" - ] + ], + "macOS": { + "entitlements": "Entitlements.plist", + "hardenedRuntime": true + }, + "windows": { + "digestAlgorithm": "sha256" + } }, "plugins": { "shell": {