Skip to content

Update integrity rests on the GitHub release alone unless a publisher policy is set; the Windows publisher check skips non-executable files #181

Open

Description

@siebe41

What

Related: #102 (signed releases), #103 (updates under administrator control).

  • Default path. DesktopUpdater.Create uses Velopack's GithubSource (src/SemanticRouter.Desktop/DesktopUpdater.cs:74-76). Velopack checks each package against the hash in releases.<channel>.json, but that file comes from the same release. Verify returns true when the policy names no publisher (DesktopUpdater.cs:316).
    • So anyone who can change a release's assets (a stolen maintainer token, a compromised release job) can ship an update that installs silently and is applied when the app quits.
    • On Linux no publisher check is possible at all (UpdatePublisher.cs:69-72).
  • The Windows publisher check covers only executables. With a publisher policy, only zip entries under lib/app/ that start with MZ are checked (src/SemanticRouter.Desktop/UpdatePublisher.cs:103).
    • Data files such as SemanticRouter.runtimeconfig.json and SemanticRouter.deps.json are neither signed nor checked.
    • The app is published self-contained but not single-file (deploy/publish-desktop.sh:18-21), so these loose files decide what the .NET host loads and how (probing paths, runtime properties).
    • On macOS, codesign --deep --strict seals every file, so there is no such gap there.
  • Feed override in release builds. SEMANTICROUTER_UPDATE_SOURCE (DesktopUpdater.cs:107) is described as "for testing", but every build honours it.
    • It accepts a local folder or a UNC path.
    • The only thing refused is plain http to a host that is not loopback (line 108).

Evidence

The code is as cited. The UpdatePublisher and DesktopUpdater tests pass.

Impact

A compromised release or feed can deliver code. So can a process that can set the user's environment, but that crosses no privilege boundary for a process running as the same user. The main risk is the release itself.

Suggested fix

  • Once signing lands (Signed, verifiable releases: signatures, provenance, SBOM and checksums #102), verify the publisher by default: bake the project's own certificate or team ID into the app instead of relying on a machine policy. Until then, say in docs/desktop.md that updates are trusted on GitHub's word.
  • On Windows, also refuse a package whose non-executable files differ from a manifest signed with the release, or check a detached signature over the whole .nupkg.
  • Honour SEMANTICROUTER_UPDATE_SOURCE only in debug and test builds, or only when the policy allows it. Require HTTPS for feeds that are not local, instead of only refusing plain HTTP.

Done when

  • Once the certificates exist, release builds verify the publisher of every update by default.
  • The Windows check covers every file in lib/app/, tested with a package whose runtimeconfig.json was changed.
  • The environment override is gone from release builds or gated by policy, with a test of DesktopUpdater.Feed.

Activity

  1. siebe41 commented on Oct 21, 2026

    @siebe41
    OwnerAuthor

    Draft PR #319 covers the second "Done when": under Updates:Publisher on Windows, every file of an update must now match a list of SHA-256 hashes (SemanticRouter.Files.dll, written at publish and signed by vpk with the release). It's tested with a package whose runtimeconfig.json was changed after signing. The environment-override item was already on main (Updates:AllowCustomFeed). The default path (no policy) and the first "Done when" (which waits for the certificates, #102) are still open.

  2. siebe41 commented on Oct 21, 2026

    @siebe41
    OwnerAuthor

    Draft PR #323 (stacked on #319) covers the default path, without a publisher policy. An update from GitHub must now be attested by release.yml at its version's tag: the app asks GitHub's attestations API for the package's SHA-256 and verifies the Sigstore bundle itself, against the trust root it carries. That covers the certificate chain and identity, the DSSE signature, and the Rekor entry with its inclusion proof and checkpoint. A missing or failing attestation refuses the update. When the check can't run (offline, rate-limited, an unreadable log entry), the update is applied with a warning. Such updates are downloaded whole, since Velopack re-zips a package it rebuilds from deltas. The first "Done when" (verifying the publisher by default) still waits for the certificates in #102.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions