tsudev # Signing tsudev.com
--- There are **two unrelated signatures** in this project, and confusing them wastes a lot of time. One is already in place; the other costs money. | | Update signing | Code signing (Authenticode) | |---|---|---| | What it protects | Installed copies only accept updates you published | Windows and SmartScreen recognise the publisher | | What it costs | Nothing | An OV or EV certificate from a commercial CA | | Failure symptom | Updates silently rejected on users' machines | "Windows protected your PC" on first run | | State here | **Done** | **Not done** - needs a certificate | | Key location | `~/.tsudev-cwico/`, and repository secrets | - | --- ## 1. Update signing - already configured The Tauri updater will not install a payload that is not signed by the key whose public half is baked into the application at build time (`plugins.updater.pubkey` in `app/src-tauri/tauri.conf.json`). The keypair was generated with: ```bash npx @tauri-apps/cli@2 signer generate -w ~/.tsudev-cwico/updater.key ``` * Private key → repository secret `TAURI_SIGNING_PRIVATE_KEY` * Passphrase → repository secret `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` * Public key → committed in `tauri.conf.json` * Both private halves → `~/.tsudev-cwico/`, which is **the only recoverable copy**: GitHub secrets are write-only. ### Back it up If the private key is lost, updates signed with a replacement are rejected by every copy already installed. Those users are stranded on their current version permanently; the only remedy is asking each of them to download and run a new installer by hand. `~/.tsudev-cwico/README.md` says the same thing next to the key itself. ### Verifying a release actually got signed Two layers: 1. The release workflow fails if no `.sig` files were produced at all. 2. `tools/verify_update_signature.py` checks the signature verifies against the public key **this build carries**, which the first check cannot: ```bash gh run download -n installers -D /tmp/release python3 tools/verify_update_signature.py /tmp/release ``` The second catches the failure that matters most - a key rotation applied to the repository secrets but not to `tauri.conf.json`. That produces a perfectly-signed release that every installed copy shows as a mandatory update and then refuses to install, which is worse than no release at all. --- ## 2. Code signing - what is left to do Unsigned installers work. What the user sees on first run is: > **Windows protected your PC** > Microsoft Defender SmartScreen prevented an unrecognised app from starting. with the publisher shown as *Unknown*, and a "Run anyway" hidden behind *More info*. For a tool that then asks for Administrator rights, that is a poor first impression - and it trains users to click through exactly the warning that protects them from something worse. ### Why this matters more here than for most projects SmartScreen reputation for an **unsigned** binary is tracked *per file hash*. Every release produces a new hash and starts from zero. This project uses date-based versioning and expects to ship often, so an unsigned build never accumulates enough downloads to stop warning before it is superseded - the warning is effectively permanent rather than a first-release inconvenience. Signing moves reputation to the *certificate*, so it carries across releases. That is the actual reason to sign, more than any single warning. ### Getting a certificate for free - the route for this project **[SignPath Foundation](https://signpath.org/)** issues free OV code-signing certificates to qualifying open-source projects, with the key held in an HSM and signing driven from CI. `tsudev-cwico` meets the substantive conditions - OSI-approved licence (MIT), public repository owned by the maintainers, no proprietary components - with three things to do first: 1. **Cut a release.** They require an existing released product to sign. 2. **Enable MFA** on the GitHub account, which they require for both SignPath and repository access. 3. **Publish a code-signing policy** on the project site and README: SignPath Foundation attribution, and the named people in the author / reviewer / approver roles. One condition is worth reading carefully before applying: they exclude *"hacking tools and active vulnerability scanning features"*. This is a system-maintenance utility - it removes software the machine's owner selects, on their own machine, and hard-blocks the components that would make it dangerous - but a reviewer seeing "terminates processes and deletes registry keys" may reasonably ask. The safety model in [`SAFETY.md`](SAFETY.md) is the answer to that question, and worth linking in the application. ### Paid alternatives, if that route does not work out | Option | Cost | Notes | |---|---|---| | [Azure Artifact Signing](https://azure.microsoft.com/en-us/pricing/details/trusted-signing/) (formerly Trusted Signing) | ~$9.99/month, Basic tier, 5,000 signatures | Individual developers accepted; needs 3 years of verifiable identity history | | Commercial OV certificate | ~$200-400/year | Reputation builds over time | | Commercial EV certificate | ~$400-700/year | No SmartScreen warning from the first release | Since June 2023 all publicly trusted code-signing keys must live in certified hardware, so "download a `.pfx` and put it in a repository secret" is no longer how any of this works - every option above is a cloud signing service or a hardware token. ### Wiring it into the release workflow Once you have a certificate, add the credentials as repository secrets and put a signing step after the build, before the artefacts are uploaded. With Azure Trusted Signing: ```yaml - name: Sign the installers if: startsWith(github.ref, 'refs/tags/') uses: azure/trusted-signing-action@v0 with: azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} endpoint: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }} trusted-signing-account-name: ${{ secrets.TRUSTED_SIGNING_ACCOUNT }} certificate-profile-name: ${{ secrets.TRUSTED_SIGNING_PROFILE }} files-folder: app/src-tauri/target/release/bundle files-folder-filter: msi,exe file-digest: SHA256 timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 ``` Two things to get right: * **Sign the `.exe` inside the bundle as well as the installers.** Signing only the MSI leaves the application itself unsigned, and SmartScreen evaluates what actually runs. * **Always timestamp.** Without `timestamp-rfc3161`, every signature stops validating the day the certificate expires - including on releases already in users' hands. ### Order matters Code signing must come **after** the Tauri build and **before** the update signature is used, or the `.sig` will not match the file that ships. The simplest correct order is: build → Authenticode-sign the artefacts → have `tauri-action` produce `latest.json` from the signed files. If you add code signing later, re-run the whole release rather than signing an already-published artefact in place. --- ### What is *not* affected by being unsigned The SmartScreen warning appears when a file carries the Mark of the Web - the tag a *browser* attaches to a download. The in-app updater fetches the installer over HTTP from Rust and runs it, which does not apply that tag, so an update should install without a warning even while the first manual download from a browser gets one. That bounds the problem to first install rather than every release - but it is reasoning about how Mark of the Web works, not something this project has observed. It is on the list of things to confirm during the first run on a real Windows machine. Installing through `winget` is likewise expected to avoid the prompt, which is a practical reason to prioritise the winget submission over waiting for a certificate. --- ## 3. MSIX and the Microsoft Store Store submissions are signed by Microsoft after review, so no certificate of your own is needed for the Store build specifically. Sideloaded MSIX packages do need one. See [`../packaging/msix/README.md`](../packaging/msix/README.md). --- ## Checklist for a signed release - [x] Updater keypair generated and stored - [x] `TAURI_SIGNING_PRIVATE_KEY` and `..._PASSWORD` in repository secrets - [x] Public key committed to `tauri.conf.json` - [x] Release workflow fails when no `.sig` is produced - [x] `tools/verify_update_signature.py` checks the signature against the key the build carries - [ ] Code-signing certificate obtained (try [SignPath Foundation](https://signpath.org/) first - free for OSS) - [ ] Signing step added to `release.yml` - [ ] Timestamping configured - [ ] A test release downloaded on a clean Windows install to confirm no SmartScreen warning appears