|
| 1 | +# Code-signing setup |
| 2 | + |
| 3 | +The release workflow at `.github/workflows/release.yml` supports two |
| 4 | +paths for Authenticode-signing the launcher EXE: |
| 5 | + |
| 6 | +1. **Azure Trusted Signing** — Microsoft's HSM-backed signing service. |
| 7 | + Recommended for new projects: no PFX file to store, OIDC-friendly, |
| 8 | + and signatures issued through this service build SmartScreen |
| 9 | + reputation faster than standard OV certs. |
| 10 | +2. **PFX from a CA** (DigiCert, Sectigo, GlobalSign, SSL.com, Certum, |
| 11 | + etc.). The traditional approach. Simpler to wire up, but you are |
| 12 | + responsible for safeguarding the private key. |
| 13 | + |
| 14 | +The workflow auto-detects which set of secrets is configured and uses |
| 15 | +that path. If neither set is configured, the workflow falls through |
| 16 | +to an unsigned build and `verify-release.ps1 -AllowUnsigned` keeps |
| 17 | +CI green. Until you complete one of the setups below, every release |
| 18 | +will be unsigned — which is exactly the state of `v0.1.0`. |
| 19 | + |
| 20 | +Both paths produce a signed `release/dist/claude-code-install-manager.exe` |
| 21 | +that re-execs the unsigned `.cmd` next to it. `.cmd` files cannot be |
| 22 | +Authenticode-signed regardless of which path you pick; see the **Why a |
| 23 | +separate `.exe`** section in `README.md` for the format-level reason. |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Path 1 — Azure Trusted Signing (recommended) |
| 28 | + |
| 29 | +### Prerequisites |
| 30 | + |
| 31 | +- An Azure subscription with billing enabled. Trusted Signing is a |
| 32 | + paid service (per-signature pricing, see the |
| 33 | + [official pricing page](https://learn.microsoft.com/azure/trusted-signing/pricing) |
| 34 | + for current rates). |
| 35 | +- The Trusted Signing service available in your subscription's |
| 36 | + region. Check the |
| 37 | + [region matrix](https://learn.microsoft.com/azure/trusted-signing/concept-trusted-signing-resources-roles) |
| 38 | + before provisioning. |
| 39 | +- The Azure CLI (`az`) installed locally, OR access to the Azure |
| 40 | + portal. |
| 41 | + |
| 42 | +### Step 1 — Create the Trusted Signing resources |
| 43 | + |
| 44 | +In the Azure portal: |
| 45 | + |
| 46 | +1. Create a resource group (or reuse one), e.g. `rg-codesigning`. |
| 47 | +2. Inside the resource group, create a **Trusted Signing Account**. |
| 48 | + Pick the region closest to your CI runners (`eastus` works well |
| 49 | + for `windows-latest`). |
| 50 | +3. Inside the account, create a **Certificate Profile**. Pick: |
| 51 | + * **Profile type**: `Public Trust` for OSS releases. |
| 52 | + * **Identity validation**: complete the org or individual |
| 53 | + identity validation flow. Microsoft will email a verification |
| 54 | + request — this is the slowest step (1–7 business days). |
| 55 | +4. Once identity validation completes, note three values: |
| 56 | + * **Trusted Signing account endpoint**, e.g. |
| 57 | + `https://eus.codesigning.azure.net/`. |
| 58 | + * **Account name**, e.g. `simtabi-codesigning`. |
| 59 | + * **Certificate profile name**, e.g. `simtabi-public`. |
| 60 | + |
| 61 | +### Step 2 — Create an Entra ID app registration |
| 62 | + |
| 63 | +The workflow signs in to Trusted Signing as a service principal. |
| 64 | +Easiest path is a federated credential (no client secret to rotate). |
| 65 | + |
| 66 | +```powershell |
| 67 | +# In the same Azure tenant. Adjust display name and subscription ID. |
| 68 | +az ad sp create-for-rbac \ |
| 69 | + --name "claude-code-install-manager-signer" \ |
| 70 | + --role "Trusted Signing Certificate Profile Signer" \ |
| 71 | + --scopes "/subscriptions/<SUB_ID>/resourceGroups/rg-codesigning/providers/Microsoft.CodeSigning/codeSigningAccounts/<ACCOUNT_NAME>/certificateProfiles/<PROFILE_NAME>" |
| 72 | +``` |
| 73 | + |
| 74 | +The command prints `appId`, `password`, and `tenant`. Save them — you |
| 75 | +will need: |
| 76 | + |
| 77 | +| Azure field | GitHub secret name | |
| 78 | +|-------------|------------------------------------------| |
| 79 | +| `tenant` | `AZURE_TENANT_ID` | |
| 80 | +| `appId` | `AZURE_CLIENT_ID` | |
| 81 | +| `password` | `AZURE_CLIENT_SECRET` | |
| 82 | + |
| 83 | +Alternatively, for OIDC-based federated credentials (no rotating |
| 84 | +secret), follow Microsoft's |
| 85 | +[GitHub-Actions federation guide](https://learn.microsoft.com/azure/developer/github/connect-from-azure-openid-connect) |
| 86 | +and drop `AZURE_CLIENT_SECRET` from the secret list. |
| 87 | + |
| 88 | +### Step 3 — Wire up the six GitHub secrets |
| 89 | + |
| 90 | +Run from your local checkout. `gh` prompts for each value |
| 91 | +interactively: |
| 92 | + |
| 93 | +```bash |
| 94 | +gh secret set AZURE_TENANT_ID --repo simtabi/claude-code-install-manager |
| 95 | +gh secret set AZURE_CLIENT_ID --repo simtabi/claude-code-install-manager |
| 96 | +gh secret set AZURE_CLIENT_SECRET --repo simtabi/claude-code-install-manager |
| 97 | +gh secret set AZURE_TRUSTED_SIGNING_ENDPOINT --repo simtabi/claude-code-install-manager |
| 98 | +gh secret set AZURE_TRUSTED_SIGNING_ACCOUNT --repo simtabi/claude-code-install-manager |
| 99 | +gh secret set AZURE_TRUSTED_SIGNING_CERT_PROFILE --repo simtabi/claude-code-install-manager |
| 100 | +``` |
| 101 | + |
| 102 | +`AZURE_TRUSTED_SIGNING_ENDPOINT` is the full URL including scheme, |
| 103 | +e.g. `https://eus.codesigning.azure.net/`. |
| 104 | + |
| 105 | +### Step 4 — Verify |
| 106 | + |
| 107 | +Push a release candidate tag: |
| 108 | + |
| 109 | +```bash |
| 110 | +git tag -a v0.1.1-rc.1 -m "Trusted Signing smoke test" |
| 111 | +git push origin v0.1.1-rc.1 |
| 112 | +``` |
| 113 | + |
| 114 | +In the workflow log, look for: |
| 115 | + |
| 116 | +- `Detect signing mode` step output: `mode=azure`. |
| 117 | +- `Sign with Azure Trusted Signing` step completing without error. |
| 118 | +- `Verify signed release` step: signature `Status: Valid`, issuer is |
| 119 | + Microsoft's Trusted Signing CA. |
| 120 | + |
| 121 | +Delete the RC tag after verification: |
| 122 | + |
| 123 | +```bash |
| 124 | +git push origin :refs/tags/v0.1.1-rc.1 |
| 125 | +git tag -d v0.1.1-rc.1 |
| 126 | +gh release delete v0.1.1-rc.1 --repo simtabi/claude-code-install-manager --yes |
| 127 | +``` |
| 128 | + |
| 129 | +--- |
| 130 | + |
| 131 | +## Path 2 — Standard PFX from a CA |
| 132 | + |
| 133 | +### Prerequisites |
| 134 | + |
| 135 | +- A code-signing certificate ordered through a CA. Typical providers |
| 136 | + and approximate annual pricing as of 2026: |
| 137 | + * SSL.com — `~$179/yr` standard, `~$299/yr` EV. |
| 138 | + * Certum (OpenSource) — `~$80/yr` for the open-source code-signing |
| 139 | + cert specifically; cheapest legitimate path for a maintainer who |
| 140 | + only signs OSS releases. **Requires OSS project URL during validation.** |
| 141 | + * Sectigo — `~$229/yr` standard. |
| 142 | + * DigiCert — `~$474/yr` standard. |
| 143 | +- Identity validation completed by the CA (org docs, phone callback, |
| 144 | + etc. — takes 1–10 business days). |
| 145 | +- The cert exported as a `.pfx` (PKCS#12) bundle with the private key. |
| 146 | + |
| 147 | +### Step 1 — Encode the PFX |
| 148 | + |
| 149 | +The GitHub Actions secret store holds text only, so the binary PFX |
| 150 | +needs to be base64-encoded once and pasted in. |
| 151 | + |
| 152 | +**macOS / Linux:** |
| 153 | + |
| 154 | +```bash |
| 155 | +base64 -i path/to/codesign.pfx -o codesign.pfx.b64 |
| 156 | +``` |
| 157 | + |
| 158 | +**Windows PowerShell:** |
| 159 | + |
| 160 | +```powershell |
| 161 | +[Convert]::ToBase64String([IO.File]::ReadAllBytes('path\to\codesign.pfx')) | |
| 162 | + Set-Content -Encoding ascii codesign.pfx.b64 |
| 163 | +``` |
| 164 | + |
| 165 | +The output file contains a single long line of base64. **Do not |
| 166 | +commit it anywhere.** `.gitignore` already excludes `*.pfx` and |
| 167 | +related extensions; the `.b64` file should be deleted after upload. |
| 168 | + |
| 169 | +### Step 2 — Set the two secrets |
| 170 | + |
| 171 | +```bash |
| 172 | +gh secret set CODESIGN_PFX_BASE64 --repo simtabi/claude-code-install-manager < codesign.pfx.b64 |
| 173 | +gh secret set CODESIGN_PFX_PASSWORD --repo simtabi/claude-code-install-manager |
| 174 | +``` |
| 175 | + |
| 176 | +The second command prompts for the password. |
| 177 | + |
| 178 | +Wipe the local base64 file: |
| 179 | + |
| 180 | +```bash |
| 181 | +shred -u codesign.pfx.b64 2>/dev/null || rm -f codesign.pfx.b64 |
| 182 | +``` |
| 183 | + |
| 184 | +### Step 3 — Verify |
| 185 | + |
| 186 | +Same as for Trusted Signing, push an RC tag: |
| 187 | + |
| 188 | +```bash |
| 189 | +git tag -a v0.1.1-rc.1 -m "PFX signing smoke test" |
| 190 | +git push origin v0.1.1-rc.1 |
| 191 | +``` |
| 192 | + |
| 193 | +In the workflow log: |
| 194 | + |
| 195 | +- `Detect signing mode`: `mode=pfx`. |
| 196 | +- `Materialize PFX from secret`: drops the file at `$RUNNER_TEMP\codesign.pfx`. |
| 197 | +- `Build (PFX signing)`: `build.ps1` runs `signtool` and the |
| 198 | + `Verify signed release` step passes. |
| 199 | + |
| 200 | +If the signature is valid but SmartScreen still warns on first run, |
| 201 | +that is normal for a brand-new standard cert — SmartScreen reputation |
| 202 | +builds over the first few hundred installs. EV-signed binaries skip |
| 203 | +this warm-up. |
| 204 | + |
| 205 | +Delete the RC tag after verification, as in Path 1. |
| 206 | + |
| 207 | +--- |
| 208 | + |
| 209 | +## What happens if neither set of secrets is configured |
| 210 | + |
| 211 | +The workflow's `Detect signing mode` step prints |
| 212 | +`mode=none` and a warning, then the `Build (unsigned fallback)` |
| 213 | +step runs `build.ps1 -SkipTests -Clean`, the |
| 214 | +`Verify unsigned release (relaxed)` step runs |
| 215 | +`verify-release.ps1 -AllowUnsigned`, the release archive is created |
| 216 | +and uploaded, and the GitHub Release is published. The artifacts |
| 217 | +work exactly the same way for users — they just see "Unknown |
| 218 | +publisher" in the SmartScreen dialog instead of your org name. |
| 219 | + |
| 220 | +The `release/dist/SHA256SUMS` file pins the exact hashes regardless |
| 221 | +of signed-or-not, so users who want integrity-without-trust can |
| 222 | +still verify with `scripts/verify-release.ps1`. |
| 223 | + |
| 224 | +--- |
| 225 | + |
| 226 | +## Rotating credentials |
| 227 | + |
| 228 | +- **PFX**: re-encode the new cert and overwrite `CODESIGN_PFX_BASE64` + |
| 229 | + `CODESIGN_PFX_PASSWORD` with `gh secret set`. The next tagged |
| 230 | + release picks up the new cert automatically. |
| 231 | +- **Trusted Signing app secret**: regenerate the secret in the Azure |
| 232 | + portal under the app registration's *Certificates & secrets* blade |
| 233 | + and overwrite `AZURE_CLIENT_SECRET`. If you migrated to federated |
| 234 | + credentials, there is no secret to rotate. |
| 235 | + |
| 236 | +Both paths support overlapping cert validity, so you can roll new |
| 237 | +credentials without downtime by overwriting the secret a day or |
| 238 | +two before the old one expires. |
| 239 | + |
| 240 | +--- |
| 241 | + |
| 242 | +## Removing signing entirely |
| 243 | + |
| 244 | +`gh secret delete <NAME> --repo simtabi/claude-code-install-manager` |
| 245 | +for each of the relevant secrets, or use the GitHub web UI under |
| 246 | +*Settings → Secrets and variables → Actions*. The workflow falls |
| 247 | +back to the unsigned path on the next release. |
0 commit comments