Skip to content

Installer: WiX MSI (blocked on code signing) #77

Open

Description

@zcsizmadia

Skrog installs by unzip-and-run, or by the one-line bootstrap (#359, done). What is missing is a real Windows installation surface: an MSI an admin can deploy, and package-manager entries people expect to find.

Blocked on #358 — the code signing decision — and not on effort. An unsigned MSI that asks for elevation is a worse first impression than a zip: it triggers SmartScreen at the moment the user is being asked to trust it most. Shipping one before a certificate exists would make the problem it is meant to solve worse.

WiX MSI

  • installs skrog.exe / skrogw.exe / skrogtray.exe, adds to PATH, optional Start Menu entries
  • silent/unattended flags that work Intune-style (msiexec /qn)
  • uninstall runs skrog uninstall semantics — or clearly documents that engine data removal is a separate, explicit step. An MSI uninstall silently deleting a user's images and volumes would be wrong
  • upgrade in place preserves the state dir and the autostart registration

Package managers

Exit criteria

winget install skrog on a clean machine with no SmartScreen wall, and an MSI that deploys silently via Intune-style flags.

Note that the first of those is not achievable by packaging alone: SmartScreen reputation only begins accruing after signing, so even a signed MSI warns for a while. That is the circularity described in #358.

Split out of this issue

Related

skrog upgrade --apply (self-update) shipped separately in #309/#310.

Activity

  1. added this to the v0.4 — Ship it milestone on Sep 2, 2026
  2. zcsizmadia commented on Sep 11, 2026

    @zcsizmadia
    CollaboratorAuthor

    Decision: apply to the SignPath Foundation OSS programme

    Free certificates for open-source projects, issued to the project rather than to a person. Chosen over Azure Trusted Signing (whose identity validation has historically wanted an organisation with years of verifiable history — hawserhq is weeks old) and over a commercial OV certificate (real money, annual renewal, and an OV cert still has to earn SmartScreen reputation the slow way).

    Why this is the critical path, not a polish item

    It gates more than SmartScreen. From #1: winget and Chocolatey package IDs cannot be reserved — the ID is taken when a manifest is merged or a package is pushed. Publishing well needs signing. So the signing application is on the critical path for name protection as well as for distribution.

    Next steps

    • Confirm current SignPath Foundation eligibility criteria and the application form (their requirements have moved; do not assume)
    • Expect the certificate to be usable only from CI — SignPath signs from a hardened pipeline, not a developer laptop. release.yml will need a signing step, and the release artifacts stop being reproducible from a local build alone
    • Budget for a review queue measured in weeks; nothing downstream can be scheduled tightly against it
    • Once issued: sign pre-release builds immediately, so reputation starts accruing before v0.4

    What stays blocked until it lands

    The MSI, the winget manifest, the Chocolatey package, and the hawser upgrade app-apply half of #191. The one-line bootstrap and hawser upgrade --check do not depend on it and could land earlier.

  3. zcsizmadia commented on Sep 11, 2026

    @zcsizmadia
    CollaboratorAuthor

    The one-line bootstrap is live

    #223, merged.

    irm https://hawserhq.github.io/hawser/install.ps1 | iex

    Verified by running that exact published command end to end on a real machine: resolved 0.3.0, downloaded, verified sha256 dec31978… against the release's SHA256SUMS, unpacked all three binaries, and the installed hawser.exe runs.

    Checklist

    • One-line bootstrap — downloads the pinned release, verifies against SHA256SUMS, unpacks, adds to PATH; checksum-verified end to end, and no "latest" semantics beyond resolving the newest published app release explicitly
    • The README's first command

    Decisions embedded in it

    It does not provision. hawser install creates a WSL2 distro and downloads a rootfs — not something a command someone pipes into their shell should do unasked. The script names it as the next step instead.

    Hosted on the docs site, not a domain (there is no domain, by decision), and mounted from scripts/install.ps1 rather than copied, so the file people pipe into their shell is the same one reviewed in the repository. Confirmed byte-identical after deploy. The README links to it for anyone who would rather read before running.

    Release resolution filters to v<semver> non-draft tags. App and rootfs releases share one tag namespace and /releases/latest can return either — the same trap hawser upgrade and setup-hawser both had to avoid.

    PATH is read and written raw, never expanded — writing back an expanded value is how an installer turns a %USERPROFILE% in someone's PATH into a literal path that breaks when the profile moves.

    Tamper detection was tested, not assumed: a matching hash is accepted in both SHA256SUMS formats, and a tampered hash, a missing entry, and an entry for another architecture are each rejected. Pointed at the live install while the engine was running, it refuses with an actionable message rather than failing on a file lock.

    Still blocked on code signing

    Unchanged, and this does not move them:

    • WiX MSI — an unsigned MSI still hits SmartScreen, which defeats the point
    • winget manifest
    • Chocolatey package
    • Scoop manifest — the bucket is claimed and deliberately empty until there is a signed build to point at

    The code signing policy SignPath requires is published; the application itself needs your two decisions (the roles table, and accepting that signing moves into CI).

  4. zcsizmadia commented on Sep 11, 2026

    @zcsizmadia
    CollaboratorAuthor

    Both decisions confirmed

    Roles table: accepted. All three roles are the one maintainer, stated plainly in the policy rather than implying a separation that does not exist. What it still guarantees with one person: no signature without a deliberate act — signing is never automatic on a tag.

    CI-only signing: accepted — and I overstated this when I raised it.

    I described "reproducible only through the pipeline, not from a local go build" as a cost of adopting SignPath. It is not a change at all. release.yml already signs SHA256SUMS with cosign keyless:

    --certificate-oidc-issuer https://token.actions.githubusercontent.com
    

    That identity exists nowhere but inside a GitHub Actions run, so CI has always been the only thing able to produce an official release. SignPath adds an Authenticode signature to a pipeline that was already the sole source of signed artifacts. Corrected in #224.

    One condition worth knowing before applying

    The certificate is issued by the Foundation to the project, and eligibility requires an OSI licence with no commercial dual-licensing for any component. A future paid tier or dual-licence would end eligibility and mean buying a commercial certificate instead. Not a blocker — the org has funding enabled, which is fine; a paid product would not be.

    The pipeline already fits their requirements

    Checked against SignPath's GitHub Action, which requires the artifact to be a workflow artifact before submission:

    requirement status
    OSI licence, no dual-licensing ✅ Apache-2.0
    No proprietary components ✅
    Actively maintained ✅
    Already released in the form to be signed ✅ v0.3.0
    Automated build from repository source ✅ release.yml, -trimpath, GitHub-hosted
    actions/upload-artifact before signing ✅ already there (hawser-binaries)
    Published code-signing policy ✅ docs/code-signing.md
    MFA on repo and SignPath accounts your check

    Why I have not written the signing step

    Three of the action's required inputs — organization-id, project-slug, signing-policy-slug — are assigned by SignPath at onboarding. They do not exist yet, so any step I wrote now would be a guess that cannot be tested. It goes in when the project is approved; the surrounding pipeline needs no rearranging to accept it.

    What is left for you

    1. Apply at https://signpath.org/apply, pointing at the published policy
    2. Add SIGNPATH_API_TOKEN as a repo secret once approved
    3. MFA on the GitHub and SignPath accounts

    Then the MSI, winget and Chocolatey unblock together.

  5. zcsizmadia commented on Sep 11, 2026

    @zcsizmadia
    CollaboratorAuthor

    Prepared answers for the SignPath application

    Every value checked against the repository. Updated for v0.3.1, which is the release to cite — a reviewer can verify its provenance and cosign signature and both succeed.

    Identity

    Project name Hawser
    Repository https://github.com/hawserhq/hawser
    Homepage / project URL https://hawserhq.github.io/hawser/
    Licence Apache-2.0 — OSI-approved, no dual-licensing
    Your role Maintainer and owner (sole maintainer)
    Contact use the address on your GitHub account, so the identity matches the repository

    Description

    Short, for their project listing:

    Run the upstream Docker Engine on Windows via WSL2 — the real Docker API, so compose, Testcontainers and your IDE just work. No licence fees, no Electron.

    Longer, if there is a free-text box. Their terms require functionality to be described:

    Hawser runs the upstream open source Docker Engine — dockerd, containerd, runc and buildkit, built from source at pinned upstream tags — inside a dedicated WSL2 distro, and bridges it to docker.exe over a Windows named pipe. It installs once and starts at logon; it idle-stops a quiet engine to reclaim RAM and wakes it on the next docker command. It is a command-line tool plus an optional status-light tray icon; there is no GUI application and nothing is collected or transmitted.

    What needs signing

    Windows executables, amd64 and arm64, shipped inside the release zips:

    file what it is
    hawser.exe the CLI, provisioner, named-pipe bridge and supervisor
    hawserw.exe the logon launcher and supervisor watchdog
    hawsertray.exe the optional status-light tray icon

    Artifact shape, if asked: two zips per release (hawser_<version>_windows_amd64.zip, ..._arm64.zip), each containing the three executables plus LICENSE and README.md.

    The engine rootfs is out of scope — a Linux tarball, pinned by SHA-256 and separately signed with cosign.

    Why signing is needed

    Hawser is a Windows developer tool distributed as a downloadable executable. Users run hawser install, which provisions a WSL2 distro and installs a container engine — exactly the kind of action where an unsigned binary and a SmartScreen warning are reasonable causes for suspicion. Authenticode signing is what lets a user distinguish an authentic release from something that merely claims to be one.

    Build and release

    Build system GitHub Actions
    Workflow .github/workflows/release.yml, triggered by publishing a v* release
    Build go build -trimpath, GitHub-hosted windows-latest runner, from the tag's source
    Artifact staging actions/upload-artifact (hawser-binaries) — SignPath's structural prerequisite, already in place
    Existing attestation SLSA build provenance + cosign keyless signature on SHA256SUMS

    Code signing policy

    https://hawserhq.github.io/hawser/code-signing/

    Names the three roles, the manual per-release approval, the CI-only signing model, and the data-handling statement their terms require.

    Evidence of a real release

    v0.3.1. A reviewer can check it:

    gh attestation verify hawser_0.3.1_windows_amd64.zip --owner hawserhq   # exit 0
    cosign verify-blob --bundle SHA256SUMS.cosign.bundle \
      --certificate-identity-regexp '^https://github.com/hawserhq/hawser/' \
      --certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS
    sha256sum -c SHA256SUMS --ignore-missing
    

    Before submitting

    The policy page asserts that role holders use MFA on GitHub and SignPath. Confirm that is true — hawserhq does not currently enforce 2FA org-wide, and a claim on the page a reviewer is reading should not be the thing that is wrong.

  6. zcsizmadia commented on Sep 12, 2026

    @zcsizmadia
    CollaboratorAuthor

    Refreshed application sheet — every value re-checked against the repo today

    The earlier sheet on this ticket predates the rename and is now wrong in every identity field: it names Hawser, hawserhq/hawser, hawserhq.github.io/hawser, and cites v0.3.1. Submitting it would have a reviewer verifying artifacts under a project and org that no longer exist.

    This replaces it. Each row was checked against the repository or fetched live, not recalled.

    Identity

    field value
    Project name Skrog
    Repository https://github.com/wslkit/skrog
    Homepage https://wslkit.github.io/skrog/
    Licence Apache-2.0 — OSI-approved, no commercial dual-licensing, no component otherwise licensed
    Role Maintainer and owner (sole maintainer)
    Contact the address on the GitHub account that owns the org, so the identity matches the repository

    All four URLs verified to return 200 today.

    Description

    Short (this is the repo's own description, so the two cannot drift):

    Run the upstream Docker Engine on Windows via WSL2 — the real Docker API, so compose, Testcontainers and your IDE just work. No licence fees, no Electron.

    Longer, if there is a free-text box:

    Skrog runs the upstream, unmodified Docker Engine inside WSL2 and bridges it to the Windows named pipe that docker.exe already speaks, so existing tooling — compose, Testcontainers, Dev Containers, IDE integrations — works without change. The engine is built from source at pinned upstream commits, published as a rootfs whose SHA-256 is compiled into the binary, and verified before import. It is not a Docker Desktop fork and ships no Electron: the Windows-side footprint is three small Go executables.

    What gets signed

    skrog.exe, skrogw.exe, skrogtray.exe — the Windows executables in a release.

    Deliberately not the engine rootfs: it is a Linux tarball, pinned by SHA-256 in the compiled-in manifest and separately signed with Sigstore/cosign. Worth stating in the application, because it shows the certificate is scoped to what Authenticode is actually for.

    Release to cite

    v0.4.0 — published today, and the better exhibit than v0.3.1 in three specific ways a reviewer can check themselves:

    gh attestation verify skrog_0.4.0_windows_amd64.zip --owner wslkit
    
    cosign verify-blob --bundle SHA256SUMS.cosign.bundle `
      --certificate-identity-regexp '^https://github.com/wslkit/skrog/' `
      --certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS
    • SLSA provenance verifies under --owner wslkit. I confirmed the exit code discriminates: the correct owner exits 0, a wrong one exits 1 with a 404, so a pass is a real pass rather than a silent no-op. (gh prints its summary only to a TTY, so redirected output is empty either way — do not read that as failure.)
    • SHA256SUMS is cosign-signed under the current repository identity.
    • The checksum command in the release notes works. It did not before v0.4.0: SHA256SUMS shipped with CRLF, so sha256sum -c reported "no file was verified" on every release (SHA256SUMS ships with CRLF, so the documented sha256sum -c verification fails on every release #248). Citing an older release would hand a reviewer a verification step that fails.

    Programme requirements already satisfied

    • A published code-signing policy, which the Foundation requires: https://wslkit.github.io/skrog/code-signing/ — verified live and free of stale naming.
    • Roles declared (Authors / Reviewers / Approvers), stated plainly as all being the one maintainer rather than implying a separation that does not exist. What it still guarantees with one person: no signature is produced without a deliberate act — signing is never automatic on a tag.
    • CI-only signing. Not a change the programme introduces: the release already signs SHA256SUMS with cosign keyless, whose identity is a GitHub Actions OIDC token that exists nowhere but inside a workflow run. CI has always been the only thing able to produce an official release.
    • Data handling: nothing collected. No telemetry, analytics, crash reporting or phone-home. The single outbound request the product makes is skrog upgrade's query to the GitHub releases API, only when a user runs that command; --offline skips it.

    Constraints to accept before submitting

    Both are in the policy already, restated so the decision is deliberate:

    • The certificate is issued by the Foundation to the project, not owned by us.
    • The programme requires an OSI-approved licence with no commercial dual-licensing, for any component. A future paid tier or dual-licence would end eligibility. Not a one-way door, but a door.
    • The Foundation can pause or revoke over a Code of Conduct violation, including retroactively.

    Why this is still the critical path

    Unchanged from the earlier comment and worth repeating: signing gates more than SmartScreen. winget and Chocolatey package IDs cannot be reserved — an ID is taken when a manifest merges or a package is pushed. Publishing badly to claim a name is worse than waiting. So this application blocks the MSI, all three package managers, and name protection at once, while the one-line bootstrap (already shipped) carries distribution in the meantime.

    Nothing else in this ticket is blocked on engineering.

  7. 2 remaining items

  8. zcsizmadia commented on Sep 15, 2026

    @zcsizmadia
    CollaboratorAuthor

    Docs caught up with the current reality in 0.5.0, which changes what this issue has to carry.

    docs/install.md is now a full step-by-step guide for the unzip-and-run path: which zip for which architecture, verifying it against SHA256SUMS, where to unpack, what each file in the zip is for, then skrog install → skrog start → verify. It also states plainly that the three stages are separate, and that Skrog does not install a docker CLI by default — which was the single most confusing thing about a first install and is now answered in the docs and by skrog install itself, which prints the hint when it finds no docker on PATH.

    So the one-line bootstrap row of this issue is not just done, it is documented. What remains is the MSI and the package managers, both still gated on the Authenticode certificate: shipping an unsigned MSI would make the SmartScreen warning worse rather than better, because an installer that asks for elevation and is unsigned is a harder thing to talk someone through than a zip. That reasoning is now written into the README and the install guide rather than living only here.

    Suggest this issue narrows to "MSI + winget/scoop/choco, once signed", with the bootstrap row checked off.

  9. zcsizmadia commented on Sep 16, 2026

    @zcsizmadia
    CollaboratorAuthor

    Signing route changed, and this issue is now the decision rather than a wait.

    The SignPath Foundation application — the free OSS programme — was declined for now. It is for projects with an established user base, and Skrog does not have one yet. They invited a reapplication as visibility grows.

    So this is no longer "waiting for a certificate". It is a choice between two open routes:

    cost timing catch
    Reapply to the Foundation free when visibility grows gated on the very thing signing would help with
    Pay for a certificate money any time none, beyond the money

    The circularity, stated plainly

    The free route needs adoption. The fastest driver of adoption is winget, which is in this issue. An unsigned installer asking for elevation is a worse first impression than a zip, so winget effectively wants a signed binary. And SmartScreen reputation only begins accruing after signing.

    Free → needs adoption → needs winget → wants signing → needs a certificate. Paying is the only thing that breaks that loop on our own initiative. That does not make it the right answer; it makes it the answer that does not depend on anyone else.

    The non-obvious part of the trade

    The Foundation issues the certificate to the project and requires an OSI-approved licence with no commercial dual-licensing for any component — a future paid tier would end eligibility — and can pause or revoke, including retroactively.

    A purchased certificate costs money, but nobody can withdraw it over a licence change or a disagreement.

    So the free route keeps a door open that the paid route closes, and vice versa. Worth deciding on that basis rather than on price alone. Written up in docs/code-signing.md.

    What this issue should now be

    Suggest splitting:

    • The bootstrap (irm | iex) — done and documented in docs/install.md; close that part out.
    • MSI + winget/scoop/choco — blocked on the decision above, not on effort.
    • The decision itself — arguably its own issue, since it is a policy and money question rather than an engineering one.

    Docs across the repo were corrected in #357; they had all been claiming signing was applied for and imminent.

  10. changed the title [-]Installer & distribution: WiX MSI, winget/scoop/choco, one-line bootstrap[/-] [+]Installer & distribution: WiX MSI, winget/scoop/choco[/+] on Sep 16, 2026
  11. zcsizmadia commented on Sep 16, 2026

    @zcsizmadia
    CollaboratorAuthor

    Split done, per the suggestion above:

    This issue is now MSI + package managers only, retitled to match, and explicitly blocked on #358 rather than on anyone finding time.

    Worth restating the reason it is blocked, because it is easy to read as over-caution: an unsigned MSI is not a neutral step toward a signed one. It triggers SmartScreen at exactly the moment the user is being asked to grant elevation, which is worse than the zip it would replace. container-desktop's Defender-blocked installer is the cautionary tale already cited in PLAN.

  12. zcsizmadia commented on Sep 16, 2026

    @zcsizmadia
    CollaboratorAuthor

    Correction: winget is not blocked on signing. I asserted that above and it is wrong.

    winget requires a signed installer only for installers that elevate — an MSI or a setup EXE. It also supports a portable type, which needs no signature, no elevation and no MSI. Verified on this machine:

    > winget show BurntSushi.ripgrep.MSVC
      Installer Type: portable (zip)
      Installer Url:  https://github.com/.../ripgrep-15.2.0-x86_64-pc-windows-msvc.zip
    
    > winget show junegunn.fzf
      Installer Type: portable (zip)
    

    Both are unsigned GitHub-release zips, shipping on winget today. Skrog already publishes exactly that artifact shape, checksum and all.

    So this issue splits again, by blocker

    blocked on
    winget (portable zip) nothing — except #1 (package ID) and #360 doable now
    scoop / choco same — both handle portable archives doable now
    WiX MSI #358, genuinely an unsigned MSI that elevates really is worse than a zip

    The manifest needs no new build output — the release zip is the package:

    PackageIdentifier: wslkit.skrog
    Installers:
      - Architecture: x64
        InstallerType: zip
        NestedInstallerType: portable
        NestedInstallerFiles:
          - RelativeFilePath: skrog.exe
            PortableCommandAlias: skrog
        InstallerUrl: .../skrog_0.5.0_windows_amd64.zip
        InstallerSha256: 70FA1889…        # already in SHA256SUMS

    winget extracts to `...\WinGet\Packages<id>.

    Two real prerequisites, neither of them signing

    1. Name housekeeping: trademark search and package IDs for SKROG (was Hawser) #1 — the package ID is effectively permanent once published, and depends on the naming decision.
    2. os.Executable() is used to find sibling files, which breaks behind a symlink shim (winget portable) #360 — that alias is a symlink, and os.Executable() returns the shim rather than the target, so sibling lookups land in the Links directory. Measured, not assumed. Autostart is the visible casualty: it would fail to find skrogw.exe. Fixing that before submitting matters, because "installed from winget, autostart does not work" would be a bad first impression from the exact channel meant to win users.

    Also worth deciding: skrog upgrade --apply overwrites the binary in place, which is wrong for a winget-managed install. It should detect that and defer to winget upgrade.

    Why this matters beyond packaging

    It removes the circularity I described in #358 — free signing needs visibility, visibility needs winget, winget was thought to need signing. That loop was never closed. winget is reachable now, which makes it the cheapest path to the visibility the SignPath reapplication wants.

  13. zcsizmadia commented on Sep 18, 2026

    @zcsizmadia
    CollaboratorAuthor

    Review: the blocker banner is too broad — winget is not blocked, by this project'"'"'s own docs

    The issue says "Blocked on #358 — the code signing decision" for the whole ticket. That is right for the MSI and wrong for winget, and the repo says so in two places:

    README.md: "a winget package does not [wait on signing], and one is on the way."

    docs/install.md: "winget package does not — winget'"'"'s portable type unpacks per-user…"

    A portable-type winget manifest points at the existing release zip, installs per-user, needs no elevation and therefore no Authenticode signature to avoid the elevation-prompt problem this issue correctly identifies for the MSI.

    So the ticket is currently telling a reader that the one distribution channel available today is unavailable.

    Suggestion: split it.

    Worth noting the ordering argument in #358 too: winget submission takes queue time, and SmartScreen reputation only begins accruing once something ships. Starting the winget half now costs nothing and shortens the path either way the signing decision goes.

  14. changed the title [-]Installer & distribution: WiX MSI, winget/scoop/choco[/-] [+]Installer: WiX MSI (blocked on code signing)[/+] on Sep 18, 2026
  15. zcsizmadia commented on Sep 18, 2026

    @zcsizmadia
    CollaboratorAuthor

    Split: the package-manager half is now #412 and is not blocked on #358, per this repo's own docs (winget's \portable\ type unpacks per-user, so there is no elevation prompt for an unsigned binary to make frightening). scoop went with it; chocolatey stayed here, since its install model wants an installer and puts it back behind the same blocker as the MSI.

    Retitled so the blocker in the banner matches what is actually blocked. Everything left here — the WiX MSI, silent/unattended flags, uninstall semantics, in-place upgrade — is correctly gated on #358.

  16. added a commit that references this issue on Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions