Skip to content

Commit e79896d

Browse files
committed
docs: add docs/signing.md with both signing-setup paths
Stages the setup walkthrough for both code-signing paths the release workflow supports — Azure Trusted Signing and PFX-from-a-CA — so future-me (or anyone with admin on the repo) can follow it without re-deriving the steps. Includes: * Prerequisites for each path (Azure subscription / Trusted Signing account + cert profile + Entra app registration, vs. buying a code-signing cert from a CA with rough 2026 pricing). * The exact `gh secret set ... --repo simtabi/claude-code-install-manager` invocations for each secret name the workflow auto-detects. * Verification recipe using a v0.1.x-rc.N tag, with cleanup commands so the smoke-test doesn't pollute the release list. * What happens when neither set of secrets is configured (which is the current state — unsigned release, the Verify-AllowUnsigned path keeps CI green). * Rotation and removal procedures. Link the new doc from README.md's "Releases and code signing" section so the first thing a reader sees is "go look at signing.md". No behavior change to the workflow.
1 parent e4d7ac5 commit e79896d

2 files changed

Lines changed: 253 additions & 0 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,12 @@ machine right now.
513513

514514
## Releases and code signing
515515

516+
**Setting up signing for the first time?** See
517+
[**docs/signing.md**](docs/signing.md) for the two supported paths
518+
(Azure Trusted Signing and PFX-from-a-CA), prerequisites, the exact
519+
`gh secret set` commands, and how to verify a signed build before
520+
cutting a real release.
521+
516522
### Why a separate `.exe`
517523

518524
**`.cmd` and `.bat` files cannot be Authenticode-signed.** Windows'

docs/signing.md

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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

Comments
 (0)