Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/electron.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,50 @@ jobs:
CSC_NAME: "Distheirs LLC (788KRST4S8)"
run: yarn ${{ matrix.dist_script }}

# Signed through Azure Artifact Signing (build.win.azureSignOptions). The
# certificate lives in Azure, never on disk; these three variables are what
# electron-builder's EnvironmentCredential authenticates with.
- name: Build (Windows)
if: matrix.platform == 'win'
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
run: yarn ${{ matrix.dist_script }}

# The Store package. Locally this is `yarn dist:win-msix-<arch>`, mirroring
# dist:mac-mas; here electron-builder is called directly so it reuses what the
# step above produced instead of wiping dist/ and rebuilding Rust for the
# third time.
#
# The Store package must ship unsigned: Partner Center re-signs it, and our
# certificate subject (CN=Juan Carlos Carmona Calvo) does not match the
# package publisher (CN=96EC5B6E-…) anyway, which would make it invalid.
#
# Three separate switches are needed for that, none of them obvious:
# - "!.appx" in build.win.signExts stops the package itself being signed.
# signAndEditExecutable does NOT cover it — AppxTarget calls signIf on
# the artifact directly, and signIf never consults that flag.
# - signAndEditExecutable=false skips the binaries inside the package.
# Microsoft's signature covers them, so signing here would only burn
# signing calls — and would make this step need the Azure credentials.
# - azureSignOptions is cleared. Left in place, electron-builder builds the
# Azure signing manager just to ask it for a publisher name, which both
# demands the credentials this step does not carry and writes the
# certificate's subject into Identity/@Publisher, ignoring
# build.appx.publisher (windowsSignAzureManager.computePublisherName
# discards its argument) — a bare personal name, not a valid DN, which
# makeappx rejects. Cleared, the publisher comes from build.appx.publisher.
- name: Build MSIX (Microsoft Store)
if: matrix.platform == 'win'
shell: bash
run: >
npx electron-builder -w appx --${{ matrix.arch }}
"-c.extraMetadata.main=build/electron.js"
"-c.win.signAndEditExecutable=false"
"-c.win.azureSignOptions="
--publish never

- name: Upload artifacts (Linux)
uses: actions/upload-artifact@v4
if: matrix.platform == 'linux'
Expand Down Expand Up @@ -194,6 +234,18 @@ jobs:
dist/*.msi
dist/*.zip

# Produced by dist:win-* alongside the msi/zip, but kept in its own artifact:
# this one goes to Partner Center, which signs it on upload. That signature is
# what gets it past Smart App Control — the msi/zip above stay unsigned and
# blocked. It is excluded from the public release in the release job, since
# unsigned it cannot be installed by anyone who downloads it.
- name: Upload MSIX (Microsoft Store)
uses: actions/upload-artifact@v4
if: matrix.platform == 'win'
with:
name: zingo-pc-win-msix-${{ matrix.arch }}
path: dist/*.appx

flatpak:
needs: build
runs-on: ubuntu-latest
Expand Down Expand Up @@ -246,6 +298,12 @@ jobs:
path: artifacts
merge-multiple: false

# Store-bound packages, not user downloads. The .appx goes to Partner Center
# and the .pkg to App Store Connect; both are signed by the store on upload,
# so as published on a release page they are files nobody can install.
- name: Keep the store packages out of the public release
run: rm -rf artifacts/zingo-pc-win-msix-* artifacts/zingo-pc-mas-*

- name: Generate release notes
id: notes
uses: actions/github-script@v7
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ native.node
/resources/nym-proxy
/resources/nym-proxy.exe

# staged VC++ runtime (copied from the MSVC redist by scripts/stage-vcruntime.js)
/resources/vcruntime

# testing
/coverage

Expand All @@ -35,3 +38,6 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.provisionprofile

# throwaway self-signed cert for sideloading local MSIX builds (docs/windows-msix.md)
zingo-dev.pfx
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Pre-built binaries for each release are available on the [Releases page](https:/
| Linux | `.deb`, `.AppImage` |
| Linux (Flatpak) | `.flatpak` |

> **Windows users:** if Windows blocks the app on launch, see [Windows blocks Zingo PC from opening](#troubleshooting) in Troubleshooting. Our Windows builds are code signed, but a recently issued certificate has to accumulate reputation before Windows stops flagging it.

---

## Compiling from source
Expand Down Expand Up @@ -240,3 +242,20 @@ Antivirus products that inspect HTTPS traffic cannot decrypt those connections,
The fix is to allowlist those two hosts in your antivirus. Do not turn off Mixnet Mode just to silence it — that is what hides your IP from the indexer when you send.

Unrelated to this warning: always download releases from the [official Releases page](https://github.com/zingolabs/zingo-pc/releases) and verify the checksum. That, not an antivirus popup, is how you confirm your build is genuine.

---

**Q: Windows blocks Zingo PC from opening ("Smart App Control" or "Windows protected your PC")**

A: Expected on recent releases. Windows weighs **reputation**, not just whether a file is signed, and a signing certificate starts with no history — so early releases can be flagged exactly like unsigned ones. It clears as installs accumulate. (The publisher on the signature is an individual's name rather than an organisation; that is how the certificate was issued, not a sign the build is unofficial.)

Verify the download yourself rather than trusting Windows' verdict either way. Right-click the file → **Properties** → **Digital Signatures** for a valid, timestamped signature, then check the hash against the [Releases page](https://github.com/zingolabs/zingo-pc/releases):

```powershell
Get-FileHash "Zingo PC <version>.msi" -Algorithm SHA256
```

Then:

- **SmartScreen** (*"Windows protected your PC"*): **More info** → **Run anyway**.
- **Smart App Control** (clean installs of Windows 11 22H2+): no per-app exception exists. It can only be disabled entirely, and **cannot be re-enabled without reinstalling Windows** — we do not recommend it. Use the Microsoft Store build instead once it is published; Store packages are trusted by SAC from the first install.
2 changes: 1 addition & 1 deletion bin/printversion.ps1
Original file line number Diff line number Diff line change
@@ -1 +1 @@
echo "VERSION=2.0.24-170" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "VERSION=2.0.25-177" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
2 changes: 1 addition & 1 deletion bin/printversion.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/bin/bash
VERSION="2.0.24-170"
VERSION="2.0.25-177"
echo "VERSION=$VERSION" >> $GITHUB_ENV
164 changes: 164 additions & 0 deletions docs/windows-msix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Windows MSIX / AppX packaging

Zingo PC's Windows artifacts (`zip`, `msi`) are unsigned, so Windows Smart App Control
blocks them. Publishing an **MSIX package through the Microsoft Store** sidesteps that:
the Store signs the package with Microsoft's own certificate, and Store-installed apps
are trusted by Smart App Control. No code signing certificate has to be bought or
validated.

This is a separate channel, not a replacement — users who download the `zip`/`msi` from
GitHub releases are still affected until those artifacts get signed.

## Build

```
yarn dist:win-msix-x64 # or dist:win-msix-arm64
```

Its own script, the same way `dist:mac-mas` sits beside `dist:mac-x64`: the store package is
built separately from the ones that go on the release page. `appx` is deliberately **not** in
`build.win.target`, so `dist:win-x64` keeps producing just `zip` and `msi`.

Output is `dist/Zingo PC <version>.appx` (`-arm64.appx` on the other arch), carrying the real
Partner Center identity from `build.appx` and ready to upload as-is.

Two things this separation buys locally:

- The `msi` target needs an **elevated** shell. WiX cannot run ICE validation under a
restricted system policy, and electron-builder passes `-wx`, so that warning becomes
`LGHT1105` and kills the build. The MSIX script never touches WiX.
- No Rust rebuild is wasted on targets you are not shipping to the Store.

Requires the Windows 10/11 SDK (`makeappx.exe`, `signtool.exe`); electron-builder downloads
its own copy on first run. That extraction needs symlink privileges — enable Developer Mode
(Settings → System → For developers), or the build dies unpacking `winCodeSign` with
"Cannot create symbolic link".

## Product identity

`build.appx` holds the values Partner Center assigned when the app name was reserved. They
are not secrets — every published package carries them in its manifest — but they must match
Partner Center **byte for byte** or the upload is rejected:

| Field | Partner Center → app → Product identity |
| --- | --- |
| `identityName` | *Package/Identity/Name* |
| `publisher` | *Package/Identity/Publisher* (the full `CN=...`) |
| `publisherDisplayName` | *Package/Properties/PublisherDisplayName* |

`applicationId` is ours, not Partner Center's, and is unrelated to the Store.

The package version comes from `package.json`; the Store requires the fourth component to be
`0` (electron-builder does this) and the version to increase on every submission.

## Testing locally

The `.appx` that `dist:win-*` produces **cannot be sideloaded** — it is unsigned, and its
publisher is the Store's, not a certificate you hold. The two are mutually exclusive by
design: what you can install locally is not what you can upload.

To install one on this machine, rebuild with a development identity and sign it yourself:

```
npx electron-builder -w appx --x64 -c.extraMetadata.main=build/electron.js --publish never ^
-c.appx.identityName=ZingoPC -c.appx.publisher="CN=Zingo PC Dev" ^
-c.appx.publisherDisplayName="Zingo PC Dev"
```

```powershell
# 1. Create the cert (once). Subject must equal the publisher above, exactly.
$cert = New-SelfSignedCertificate -Type Custom -Subject "CN=Zingo PC Dev" `
-KeyUsage DigitalSignature -FriendlyName "Zingo PC Dev" `
-CertStoreLocation "Cert:\CurrentUser\My" `
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}")

# 2. Export it and trust it ($pwd is a PowerShell automatic variable — do not reuse the name)
$certPwd = ConvertTo-SecureString -String "devpass" -Force -AsPlainText
Export-PfxCertificate -Cert "Cert:\CurrentUser\My\$($cert.Thumbprint)" `
-FilePath zingo-dev.pfx -Password $certPwd
Import-PfxCertificate -FilePath zingo-dev.pfx -Password $certPwd `
-CertStoreLocation "Cert:\LocalMachine\TrustedPeople"

# 3. Sign and install
$signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" |
Sort-Object FullName | Select-Object -Last 1
& $signtool.FullName sign /fd SHA256 /f zingo-dev.pfx /p devpass "dist\Zingo PC <version>.appx"
Add-AppxPackage "dist\Zingo PC <version>.appx"
```

`zingo-dev.pfx` is a throwaway credential (gitignored) — do not reuse it for anything else.

Both installs show up as "Zingo PC" in the Start menu and are indistinguishable there. Launch
the packaged one explicitly:

```powershell
Start-Process "shell:AppsFolder\$((Get-AppxPackage *ZingoPC*).PackageFamilyName)!ZingoPC"
```

## What to verify in the packaged app

MSIX runs full-trust but virtualizes filesystem and registry writes, so these are the parts
most likely to behave differently from the `msi` build:

- **`zcash:` URI handling.** `app.setAsDefaultProtocolClient` in `public/electron.js` cannot
register the scheme from inside an MSIX container — its registry writes are virtualized.
The scheme is instead declared in the package manifest, generated from `build.win.protocols`.
Test both paths: app closed (cold start) and app already running (`second-instance`).
- **`keytar`.** `getRequireAuth`/`setRequireAuth` swallow any keytar failure and fall back to
`settings.json`, so a broken keytar looks like a working app. Prove it by toggling the
device-auth setting and checking that a `Zingo PC` entry appears in Credential Manager.
- **`nym-proxy.exe`.** Spawned as a child process from `extraResources`; confirm it launches
and that its listening socket works.
- **`electron-settings` / `electron-json-storage`.** Writes to `%APPDATA%` are redirected to
the package's private store. Existing users migrating from the `msi` build will not see
their previous settings.

## Store assets

The tile assets live in `public/appx/` (`build.directories.buildResources` is `public`) and
are generated from `resources/icon.png`:

```
powershell -ExecutionPolicy Bypass -File scripts/generate-appx-assets.ps1
```

Re-run it whenever the icon changes. Without these files electron-builder silently falls
back to its own placeholder images — a local build still succeeds, but Store certification
fails.

## CI

`.github/workflows/electron.yml` builds the MSIX inside the existing Windows job rather than
calling `dist:win-msix-*`, which would `rimraf dist` and rebuild Rust from scratch a third
time. It reuses what `dist:win-<arch>` just produced, so it costs seconds.

That is the one place the MAS symmetry stops. MAS earns its own matrix entry because it is a
genuinely different build — universal lipo, different entitlements, different signing. The
Store MSIX is the same build output packed into another container, so a second
electron-builder call in the same job is enough.

It is uploaded as its own artifact (`zingo-pc-win-msix-x64` / `-arm64`) and **excluded from
the GitHub release**, along with the MAS `.pkg`. Both are store-bound: signed by Apple and
Microsoft on upload, so on a release page they would be things nobody can install.

Download both `.appx` files and upload them to the same submission — Partner Center serves
each machine the matching one. An x64-only listing would still run on Windows on ARM through
emulation, just slower and with worse battery life.

## Publishing

The first submission has to be done by hand — reserving the name, the listing, screenshots,
age rating and privacy policy have no unattended path:

1. Register at `partner.microsoft.com` (individual account, one-off fee) and pass identity
verification.
2. Reserve the app name. That is what mints the product identity values above.
3. Push a `zingo-pc-*` tag and download the two `zingo-pc-win-msix-*` artifacts from the run.
4. Upload both `.appx` files to the submission, unsigned — Partner Center re-signs them.
5. Under Packages, tick **Windows 10/11 Desktop** under device family availability, or the
product ships available to nobody.
6. Complete the listing (category: Personal finance) and submit for certification.

Later submissions can be automated with the Microsoft Store submission API, which needs an
Entra tenant with an app registration linked under Partner Center → Account settings → User
management. Worth doing only once the manual flow has gone through at least once.
Loading
Loading