diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 66f07b8..04cf1bd 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,6 +7,18 @@
#
# The result is a DRAFT release: nothing becomes public until it is
# reviewed and published by hand on GitHub.
+#
+# Dry run: tick "dry run" when starting it by hand. Everything is built and
+# checked, the files stay on the run page as a workflow artifact, nothing is
+# signed or published. Pull requests that change this workflow, the
+# installer or .signpath/ get a dry run by themselves.
+#
+# Code signing (SignPath Foundation) is prepared but off until the
+# repository has the variable SIGNPATH_ORGANIZATION_ID and the secret
+# SIGNPATH_API_TOKEN; see .signpath/README.md. Without them the release is
+# built unsigned, exactly as before. With them the programs are signed
+# first, the installers are built from the signed programs and are then
+# signed themselves: two signing requests to approve per release.
name: Release
on:
@@ -17,13 +29,28 @@ on:
version:
description: 'Version without "v", e.g. 2.0.0'
required: true
+ dry_run:
+ description: 'Dry run: build and check only, sign and publish nothing'
+ type: boolean
+ default: false
+ pull_request:
+ paths:
+ - '.github/workflows/release.yml'
+ - '.signpath/**'
+ - 'packaging/inno/**'
permissions:
contents: write
+ actions: read
+
+env:
+ DRY_RUN: ${{ github.event_name == 'pull_request' || inputs.dry_run == true }}
jobs:
release:
runs-on: windows-latest
+ # Up to one hour of waiting for each of the two signing approvals.
+ timeout-minutes: 180
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
@@ -41,12 +68,23 @@ jobs:
INPUT_VERSION: ${{ inputs.version }}
run: |
v="$INPUT_VERSION"
+ # A pull request has no version of its own: it builds the one in
+ # wails.json.
+ if [ -z "$v" ] && [ "$GITHUB_EVENT_NAME" = pull_request ]; then
+ v=$(sed -nE 's/.*"productVersion": "([^"]*)".*/\1/p' gui/wails.json)
+ fi
[ -n "$v" ] || v="${GITHUB_REF_NAME#v}"
if ! [[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then
echo "::error::not a version: $v"; exit 1
fi
+ # numeric: what the file properties of every exe and installer
+ # carry, and what SignPath checks them against.
+ numeric=$(echo "$v" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
echo "version=$v" >> "$GITHUB_OUTPUT"
- echo "winver=$(echo "$v" | sed -E 's/^([0-9]+)\.([0-9]+)\.([0-9]+).*/\1.\2.\3.0/')" >> "$GITHUB_OUTPUT"
+ echo "winver=$numeric.0" >> "$GITHUB_OUTPUT"
+ echo "numeric=$numeric" >> "$GITHUB_OUTPUT"
+ echo "VERSION=$v" >> "$GITHUB_ENV"
+ echo "NUMERIC_VERSION=$numeric" >> "$GITHUB_ENV"
- name: tests
shell: bash
@@ -64,11 +102,13 @@ jobs:
run: |
set -euo pipefail
V="${{ steps.v.outputs.version }}"
- W="${{ steps.v.outputs.winver }}"
+ N="${{ steps.v.outputs.numeric }}"
LDFLAGS="-s -w -X github.com/burnersen/TimeSync/internal/version.Version=$V"
+ # Both exes carry the same product version text (1.2.3): SignPath
+ # compares it, and go-winres writes the text exactly as given.
(cd cmd/ntp-sync && go run github.com/tc-hib/go-winres@v0.3.3 make --in winres/winres.json --out rsrc \
- --arch amd64,arm64 --product-version "$W" --file-version "$W")
- sed -i -E "s/\"productVersion\": \"[^\"]*\"/\"productVersion\": \"${W%.0}\"/" gui/wails.json
+ --arch amd64,arm64 --product-version "$N" --file-version "$N")
+ sed -i -E "s/\"productVersion\": \"[^\"]*\"/\"productVersion\": \"$N\"/" gui/wails.json
mkdir -p dist
for arch in amd64 arm64; do
label=x64; [ "$arch" = arm64 ] && label=arm64
@@ -80,34 +120,186 @@ jobs:
cp LICENSE README.md THIRD_PARTY_NOTICES.md "$out/"
done
- # Code signing goes here once a certificate is available (for example
- # through the SignPath Foundation for open source projects). Until
- # then the files are unsigned and Windows SmartScreen will warn.
+ - name: check the file properties of the programs
+ run: |
+ $files = foreach ($label in 'x64', 'arm64') {
+ "dist/TimeSync-$env:VERSION-windows-$label/TimeSync.exe"
+ "dist/TimeSync-$env:VERSION-windows-$label/ntp-sync.exe"
+ }
+ ./.signpath/check-files.ps1 -ProductName TimeSync -ProductVersion $env:NUMERIC_VERSION -Path $files
+
+ # --- code signing: on or off? ----------------------------------------
+ - name: 'code signing: on or off?'
+ id: signing
+ env:
+ ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
+ HAS_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN != '' }}
+ run: |
+ $hasOrganization = -not [string]::IsNullOrWhiteSpace($env:ORGANIZATION_ID)
+ $hasToken = $env:HAS_TOKEN -eq 'true'
+ if ($env:DRY_RUN -eq 'true') {
+ $on = $false
+ Write-Host 'Dry run: nothing is signed.'
+ } elseif ($hasOrganization -ne $hasToken) {
+ Write-Host '::error::Code signing is half set up: SIGNPATH_ORGANIZATION_ID and SIGNPATH_API_TOKEN must both be set, or neither (see .signpath/README.md).'
+ exit 1
+ } else {
+ $on = $hasOrganization -and $hasToken
+ Write-Host ($(if ($on) { 'Code signing is on.' } else { 'Code signing is off: this release is unsigned.' }))
+ }
+ "enabled=$($on.ToString().ToLower())" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8
+
+ # --- code signing, round 1: the programs -----------------------------
+ - name: 'code signing: collect the programs'
+ if: steps.signing.outputs.enabled == 'true'
+ run: |
+ foreach ($label in 'x64', 'arm64') {
+ New-Item -ItemType Directory "sign/programs/$label" -Force | Out-Null
+ Copy-Item "dist/TimeSync-$env:VERSION-windows-$label/*.exe" "sign/programs/$label/"
+ }
+
+ - name: 'code signing: upload the programs'
+ id: unsigned-programs
+ if: steps.signing.outputs.enabled == 'true'
+ uses: actions/upload-artifact@v7
+ with:
+ name: unsigned-programs
+ path: sign/programs
+ if-no-files-found: error
+
+ - name: 'code signing: sign the programs (approve the request in SignPath)'
+ if: steps.signing.outputs.enabled == 'true'
+ uses: signpath/github-action-submit-signing-request@v3
+ with:
+ api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
+ organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
+ project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG || 'TimeSync' }}
+ signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG || 'release-signing' }}
+ artifact-configuration-slug: ${{ vars.SIGNPATH_PROGRAMS_CONFIGURATION_SLUG || 'programs' }}
+ github-artifact-id: ${{ steps.unsigned-programs.outputs.artifact-id }}
+ wait-for-completion: true
+ wait-for-completion-timeout-in-seconds: 3600
+ output-artifact-directory: sign/programs-signed
+ parameters: |
+ version: "${{ steps.v.outputs.numeric }}"
- - name: installers and zips
+ - name: 'code signing: take over the signed programs'
+ if: steps.signing.outputs.enabled == 'true'
+ run: |
+ $files = foreach ($label in 'x64', 'arm64') {
+ foreach ($exe in 'TimeSync.exe', 'ntp-sync.exe') {
+ $target = "dist/TimeSync-$env:VERSION-windows-$label/$exe"
+ Copy-Item "sign/programs-signed/$label/$exe" $target -Force
+ $target
+ }
+ }
+ ./.signpath/check-files.ps1 -ProductName TimeSync -ProductVersion $env:NUMERIC_VERSION -Path $files -Signed
+
+ - name: installers
shell: pwsh
run: |
$V = "${{ steps.v.outputs.version }}"
+ $N = "${{ steps.v.outputs.numeric }}"
$iscc = (Get-Command iscc.exe -ErrorAction SilentlyContinue).Source
if (-not $iscc) { $iscc = Get-ChildItem "C:\Program Files*\Inno Setup*\ISCC.exe" | Select-Object -First 1 -ExpandProperty FullName }
if (-not $iscc) { Write-Host "::error::Inno Setup compiler not found"; exit 1 }
foreach ($label in @("x64", "arm64")) {
$src = "dist/TimeSync-$V-windows-$label"
- & $iscc /Q "/DAppVersion=$V" "/DArch=$label" "/DSourceDir=$((Resolve-Path $src).Path)" "/O$((Resolve-Path dist).Path)" packaging/inno/TimeSync.iss
+ & $iscc /Q "/DAppVersion=$V" "/DNumericVersion=$N" "/DArch=$label" "/DSourceDir=$((Resolve-Path $src).Path)" "/O$((Resolve-Path dist).Path)" packaging/inno/TimeSync.iss
if ($LASTEXITCODE -ne 0) { exit 1 }
- Compress-Archive -Path $src -DestinationPath "dist/TimeSync-$V-windows-$label.zip"
+ }
+ $installers = foreach ($label in 'x64', 'arm64') { "dist/TimeSync-$V-setup-$label.exe" }
+ ./.signpath/check-files.ps1 -ProductName TimeSync -ProductVersion $N -Path $installers
+
+ # --- code signing, round 2: the installers ---------------------------
+ - name: 'code signing: collect the installers'
+ if: steps.signing.outputs.enabled == 'true'
+ run: |
+ New-Item -ItemType Directory sign/installers -Force | Out-Null
+ Copy-Item dist/*-setup-*.exe sign/installers/
+
+ - name: 'code signing: upload the installers'
+ id: unsigned-installers
+ if: steps.signing.outputs.enabled == 'true'
+ uses: actions/upload-artifact@v7
+ with:
+ name: unsigned-installers
+ path: sign/installers
+ if-no-files-found: error
+
+ - name: 'code signing: sign the installers (approve the request in SignPath)'
+ if: steps.signing.outputs.enabled == 'true'
+ uses: signpath/github-action-submit-signing-request@v3
+ with:
+ api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
+ organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
+ project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG || 'TimeSync' }}
+ signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG || 'release-signing' }}
+ artifact-configuration-slug: ${{ vars.SIGNPATH_INSTALLERS_CONFIGURATION_SLUG || 'installers' }}
+ github-artifact-id: ${{ steps.unsigned-installers.outputs.artifact-id }}
+ wait-for-completion: true
+ wait-for-completion-timeout-in-seconds: 3600
+ output-artifact-directory: sign/installers-signed
+ parameters: |
+ version: "${{ steps.v.outputs.version }}"
+ numeric: "${{ steps.v.outputs.numeric }}"
+
+ - name: 'code signing: take over the signed installers'
+ if: steps.signing.outputs.enabled == 'true'
+ run: |
+ $files = foreach ($label in 'x64', 'arm64') {
+ $name = "TimeSync-$env:VERSION-setup-$label.exe"
+ Copy-Item "sign/installers-signed/$name" "dist/$name" -Force
+ "dist/$name"
+ }
+ ./.signpath/check-files.ps1 -ProductName TimeSync -ProductVersion $env:NUMERIC_VERSION -Path $files -Signed
+
+ - name: zips and checksums
+ shell: pwsh
+ run: |
+ $V = "${{ steps.v.outputs.version }}"
+ foreach ($label in @("x64", "arm64")) {
+ Compress-Archive -Path "dist/TimeSync-$V-windows-$label" -DestinationPath "dist/TimeSync-$V-windows-$label.zip"
}
Get-ChildItem dist -File | ForEach-Object {
"{0} {1}" -f (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLower(), $_.Name
} | Set-Content dist/SHA256SUMS.txt -Encoding ascii
+ Get-Content dist/SHA256SUMS.txt
+
+ # The release page is the download page, so it links the code signing
+ # policy and says whether the files of this release are signed.
+ - name: release text
+ env:
+ SIGNED: ${{ steps.signing.outputs.enabled }}
+ run: |
+ $policy = "https://github.com/$env:GITHUB_REPOSITORY/blob/main/CODE_SIGNING.md"
+ if ($env:SIGNED -eq 'true') {
+ $footer = "The Windows files of this release are signed. Free code signing provided by [SignPath.io](https://about.signpath.io), certificate by [SignPath Foundation](https://signpath.org). [Code signing policy]($policy)"
+ } else {
+ $footer = "The Windows files of this release are not code signed, so Windows SmartScreen may warn. [Code signing policy]($policy)"
+ }
+ $text = [IO.File]::ReadAllText("CHANGELOG.md") + "`n---`n`n$footer`n"
+ [IO.File]::WriteAllText("$env:RUNNER_TEMP/release-notes.md", $text, [Text.UTF8Encoding]::new($false))
+
+ - name: 'dry run: keep the files on the run page'
+ if: env.DRY_RUN == 'true'
+ uses: actions/upload-artifact@v7
+ with:
+ name: release-dry-run
+ path: |
+ dist/*.zip
+ dist/*.exe
+ dist/SHA256SUMS.txt
+ retention-days: 7
- uses: softprops/action-gh-release@v2
+ if: env.DRY_RUN != 'true'
with:
draft: true
tag_name: v${{ steps.v.outputs.version }}
target_commitish: ${{ github.sha }}
name: TimeSync ${{ steps.v.outputs.version }}
- body_path: CHANGELOG.md
+ body_path: ${{ runner.temp }}/release-notes.md
files: |
dist/*.zip
dist/*.exe
diff --git a/.signpath/README.md b/.signpath/README.md
new file mode 100644
index 0000000..d8c83b7
--- /dev/null
+++ b/.signpath/README.md
@@ -0,0 +1,68 @@
+# Code signing with SignPath: prepared, off until switched on
+
+TimeSync has applied to the [SignPath Foundation](https://signpath.org) for free
+code signing of its Windows files. Everything for it is prepared here and in
+[`release.yml`](../.github/workflows/release.yml), but **it stays off** until it is
+switched on below. Until then every release is built and published exactly as before,
+unsigned, and its release text says so.
+
+| File | What it is |
+| ---- | ---------- |
+| [`../CODE_SIGNING.md`](../CODE_SIGNING.md) | The public code signing policy (required by SignPath Foundation): roles, what is signed, privacy |
+| [`artifact-configuration-programs.xml`](artifact-configuration-programs.xml) | SignPath artifact configuration `programs`: the four programs (x64 and ARM64), first request of a release |
+| [`artifact-configuration-installers.xml`](artifact-configuration-installers.xml) | SignPath artifact configuration `installers`: the two installers, built from the signed programs, second request |
+| [`check-files.ps1`](check-files.ps1) | Run by every release build: checks product name and version of every file to be signed (SignPath enforces both), and with `-Signed` that the signatures are valid |
+
+## How the release workflow decides
+
+| Repository settings | What a release does |
+| ------------------- | ------------------- |
+| Neither `SIGNPATH_ORGANIZATION_ID` nor `SIGNPATH_API_TOKEN` set | Builds and publishes unsigned, as before |
+| Both set | Uploads the files, sends a signing request to SignPath, **waits up to one hour for the approval**, downloads the signed files, checks their signatures and publishes them |
+| Only one of them set | Stops with an error, so a half finished setup never ships unsigned files by surprise |
+
+**Dry run:** *Actions > Release > Run workflow* with "dry run" ticked builds and checks
+everything, keeps the files on the run page as a workflow artifact, never signs and
+publishes nothing. It also runs by itself for every pull request that changes the
+release workflow or this folder.
+
+## Switching it on (after SignPath Foundation said yes)
+
+1. **Two-factor authentication** on GitHub and on SignPath for everybody in the team
+ (a condition of SignPath Foundation).
+2. **In SignPath** (the Foundation sets up most of it with you):
+ - the project, slug `TimeSync` (another slug: set the variable `SIGNPATH_PROJECT_SLUG`),
+ - the trusted build system *GitHub.com* linked to the project, and the SignPath
+ GitHub App installed on this repository (SignPath shows the link),
+ - the artifact configuration from [`artifact-configuration-programs.xml`](artifact-configuration-programs.xml), slug `programs` (another slug: set `SIGNPATH_PROGRAMS_CONFIGURATION_SLUG`),
+ - the artifact configuration from [`artifact-configuration-installers.xml`](artifact-configuration-installers.xml), slug `installers` (another slug: set `SIGNPATH_INSTALLERS_CONFIGURATION_SLUG`),
+ - the signing policy `release-signing` with manual approval by the approver
+ (another slug: set `SIGNPATH_SIGNING_POLICY_SLUG`),
+ - a CI user with the *Submitter* role on that policy, and an API token for it.
+3. **On GitHub:** *Settings > Secrets and variables > Actions*
+ - secret `SIGNPATH_API_TOKEN`: the API token of the CI user,
+ - variable `SIGNPATH_ORGANIZATION_ID`: the organization ID from SignPath.
+4. **Recommended:** a ruleset that protects `main` and the `v*` tags from force pushes
+ and deletion (*Settings > Rules > Rulesets*). SignPath can require this in its
+ policies.
+5. **Update the texts:** the status line at the top of
+ [`CODE_SIGNING.md`](../CODE_SIGNING.md) and the SmartScreen note in the README.
+6. **Release as usual.** Approve the signing request in SignPath within one hour (the
+ link is in the run log and in the e-mail from SignPath), then the release is published
+ with signed files.
+
+| Variable / secret | Required | Default |
+| ----------------- | -------- | ------- |
+| `SIGNPATH_API_TOKEN` (secret) | yes | |
+| `SIGNPATH_ORGANIZATION_ID` | yes | |
+| `SIGNPATH_PROJECT_SLUG` | no | `TimeSync` |
+| `SIGNPATH_SIGNING_POLICY_SLUG` | no | `release-signing` |
+| `SIGNPATH_PROGRAMS_CONFIGURATION_SLUG` | no | `programs` |
+| `SIGNPATH_INSTALLERS_CONFIGURATION_SLUG` | no | `installers` |
+
+## Taking it out again
+
+Never setting the secret and the variable is enough: nothing is signed then. To remove
+it completely: delete this folder and `CODE_SIGNING.md`, remove the steps marked
+`code signing` in `release.yml` (the file check step uses `check-files.ps1` from this
+folder) and the links to `CODE_SIGNING.md` in the README.
diff --git a/.signpath/artifact-configuration-installers.xml b/.signpath/artifact-configuration-installers.xml
new file mode 100644
index 0000000..a193891
--- /dev/null
+++ b/.signpath/artifact-configuration-installers.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.signpath/artifact-configuration-programs.xml b/.signpath/artifact-configuration-programs.xml
new file mode 100644
index 0000000..c72d97f
--- /dev/null
+++ b/.signpath/artifact-configuration-programs.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.signpath/check-files.ps1 b/.signpath/check-files.ps1
new file mode 100644
index 0000000..02d81b1
--- /dev/null
+++ b/.signpath/check-files.ps1
@@ -0,0 +1,61 @@
+# TimeSync (https://github.com/burnersen/TimeSync)
+# Copyright (C) 2026 burnersen
+# SPDX-License-Identifier: MIT
+#
+# Checks the Windows files a release is about to sign or publish:
+#
+# - every file exists,
+# - its file properties carry exactly the product name and product version
+# that the SignPath artifact configuration enforces (product-name,
+# product-version). SignPath refuses a signing request whose files do not
+# match, so this check runs on every release build, signed or not: a
+# mismatch shows up long before the first signed release.
+# - with -Signed: every file carries a valid Authenticode signature.
+#
+# Usage (from the release workflow):
+# ./.signpath/check-files.ps1 -ProductName X -ProductVersion 1.2.3 -Path a.exe, b.exe [-Signed]
+
+param(
+ [Parameter(Mandatory)] [string[]] $Path,
+ [Parameter(Mandatory)] [string] $ProductName,
+ [Parameter(Mandatory)] [string] $ProductVersion,
+ [switch] $Signed
+)
+
+$ErrorActionPreference = 'Stop'
+$failed = 0
+
+foreach ($file in $Path) {
+ if (-not (Test-Path -LiteralPath $file -PathType Leaf)) {
+ Write-Host "::error::$file is missing"
+ $failed++
+ continue
+ }
+ $info = (Get-Item -LiteralPath $file).VersionInfo
+ $name = "$($info.ProductName)".Trim()
+ $version = "$($info.ProductVersion)".Trim()
+ if ($name -ne $ProductName) {
+ Write-Host "::error::$file has product name '$name', expected '$ProductName'"
+ $failed++
+ }
+ if ($version -ne $ProductVersion) {
+ Write-Host "::error::$file has product version '$version', expected '$ProductVersion'"
+ $failed++
+ }
+ $line = "{0}: {1} {2}" -f $file, $name, $version
+ if ($Signed) {
+ $signature = Get-AuthenticodeSignature -LiteralPath $file
+ if ($signature.Status -ne 'Valid') {
+ Write-Host "::error::$file is not validly signed ($($signature.Status): $($signature.StatusMessage))"
+ $failed++
+ } else {
+ $line += ", signed by $($signature.SignerCertificate.Subject)"
+ }
+ }
+ Write-Host $line
+}
+
+if ($failed -gt 0) {
+ Write-Host "::error::$failed problem(s) with the release files"
+ exit 1
+}
diff --git a/CODE_SIGNING.md b/CODE_SIGNING.md
new file mode 100644
index 0000000..91c0d81
--- /dev/null
+++ b/CODE_SIGNING.md
@@ -0,0 +1,72 @@
+# Code signing policy
+
+Free code signing provided by [SignPath.io](https://about.signpath.io),
+certificate by [SignPath Foundation](https://signpath.org).
+
+> **Status:** TimeSync has applied to the SignPath Foundation. Until the
+> signing is active, the Windows files of a release are not signed. Every
+> release says in its text whether its files are signed.
+
+## What is signed
+
+Only files built from the source code in this repository, by the
+[release workflow](.github/workflows/release.yml) on GitHub-hosted runners:
+
+| File | What it is |
+| ---- | ---------- |
+| `TimeSync.exe` (x64, ARM64) | The window |
+| `ntp-sync.exe` (x64, ARM64) | Command line tool and Windows service |
+| `TimeSync--setup-x64.exe`, `-setup-arm64.exe` | The installers, built from the signed programs above |
+
+The `.zip` files contain the same signed programs. Nothing that was built
+elsewhere, and nothing from other projects, is signed with this certificate.
+Every signing request is approved by hand, release by release.
+
+The file properties of every signed file name the product `TimeSync` and the
+version of the release; SignPath refuses to sign files that do not match.
+
+## Team roles
+
+| Role | Who |
+| ---- | --- |
+| Committers and reviewers | [burnersen](https://github.com/burnersen) |
+| Approvers | [burnersen](https://github.com/burnersen) |
+
+Changes from anybody else (pull requests) are reviewed by a committer before
+they are merged. All team members use multi-factor authentication for GitHub
+and for SignPath.
+
+## Privacy policy
+
+This program will not transfer any information to other networked systems
+unless specifically requested by the user or the person installing or
+operating it.
+
+What TimeSync does on the network, and only for that:
+
+- It asks the **time servers in your settings** for the time (NTP or NTS).
+ The default servers are `time.cloudflare.com`, `ptbtime1.ptb.de`,
+ `ptbtime2.ptb.de` and `time.windows.com`; you can change them.
+- Only if NTP is blocked, it reads the time from the `Date` header of the
+ **web servers in `httpsFallbackUrls`** (default: ptb.de, cloudflare.com,
+ google.com, microsoft.com). This is a `HEAD` request: nothing is sent but
+ the request itself.
+
+Like any network request, these reveal your IP address to the server asked.
+There is no telemetry, no update check, no account and no advertising. The
+diagnostics file stays on your PC until you pass it on yourself, and masks
+your user name.
+
+## Verifying a download
+
+- **Signature:** right-click the file > *Properties* > *Digital Signatures*.
+ A signed release shows *SignPath Foundation*.
+- **Checksums:** every release has a `SHA256SUMS.txt`
+ (`Get-FileHash ` in PowerShell).
+
+## Reporting a problem
+
+If you think a signed file of this project does something it should not,
+please open an [issue](https://github.com/burnersen/TimeSync/issues). Signed
+files that break these rules can also be reported to the
+[SignPath Foundation](https://signpath.org).
diff --git a/README.md b/README.md
index 6c4e5fb..5d0b7ec 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,8 @@ Edge WebView2 runtime, which is part of current Windows.
> **Windows SmartScreen** may warn because the files are not yet digitally
> signed. Choose "More info" → "Run anyway". The source code is all here,
> and every release is built by GitHub Actions from this repository.
+> Free code signing through the SignPath Foundation is applied for; see the
+> [code signing policy](CODE_SIGNING.md).
## How it decides
@@ -186,7 +188,8 @@ the program file is replaced.
TimeSync contacts only the time servers in your settings and, if NTP is
blocked, the web servers in `httpsFallbackUrls` (a `HEAD` request, nothing
is sent but the request itself). There is no telemetry, no update check and
-no account. The diagnostics file masks your user name.
+no account. The diagnostics file masks your user name. The full privacy
+policy is part of the [code signing policy](CODE_SIGNING.md#privacy-policy).
## Uninstall
@@ -212,7 +215,10 @@ Releases are built by [`.github/workflows/release.yml`](.github/workflows/releas
either when a `v*` tag is pushed or by hand (Actions → Release → "Run
workflow", enter the version). The result is a draft release with
installers, zip files and `SHA256SUMS.txt`; it becomes public only when it
-is published on GitHub. Every push also leaves an unsigned test build of
+is published on GitHub. Code signing is prepared in the same workflow and
+switched on once SignPath Foundation approves it, see
+[`.signpath/README.md`](.signpath/README.md). A dry run (tick "dry run")
+builds and checks everything without publishing. Every push also leaves an unsigned test build of
the x64 programs on its CI run page.
Layout: `cmd/ntp-sync` (command line and service), `gui` (the window),
@@ -223,3 +229,4 @@ Layout: `cmd/ntp-sync` (command line and service), `gui` (the window),
## License
[MIT](LICENSE). Third-party components: [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
+Code signing policy: [CODE_SIGNING.md](CODE_SIGNING.md).
diff --git a/packaging/inno/TimeSync.iss b/packaging/inno/TimeSync.iss
index 94d0ade..4291296 100644
--- a/packaging/inno/TimeSync.iss
+++ b/packaging/inno/TimeSync.iss
@@ -1,7 +1,7 @@
; Installer for TimeSync (Inno Setup 6).
;
; Built by .github/workflows/release.yml:
-; ISCC /DAppVersion=2.0.0 /DArch=x64 /DSourceDir= TimeSync.iss
+; ISCC /DAppVersion=2.0.0 /DNumericVersion=2.0.0 /DArch=x64 /DSourceDir= TimeSync.iss
;
; It installs TimeSync.exe and ntp-sync.exe to C:\Program Files\TimeSync
; and, if ticked, sets up the background service. Uninstalling removes the
@@ -10,6 +10,11 @@
#ifndef AppVersion
#define AppVersion "0.0.0"
#endif
+; The file properties of the installer: product name and version must match
+; the programs inside, because code signing (SignPath) checks both.
+#ifndef NumericVersion
+ #define NumericVersion AppVersion
+#endif
#ifndef Arch
#define Arch "x64"
#endif
@@ -27,7 +32,12 @@ AppPublisher=burnersen
AppPublisherURL=https://github.com/burnersen/TimeSync
AppSupportURL=https://github.com/burnersen/TimeSync/issues
AppUpdatesURL=https://github.com/burnersen/TimeSync/releases
-VersionInfoVersion={#AppVersion}
+VersionInfoVersion={#NumericVersion}
+VersionInfoProductName=TimeSync
+VersionInfoProductVersion={#NumericVersion}
+VersionInfoProductTextVersion={#NumericVersion}
+VersionInfoCompany=burnersen
+VersionInfoDescription=TimeSync Setup
; The service binary must live where only administrators can change it.
DefaultDirName={autopf}\TimeSync
DisableDirPage=yes
diff --git a/scripts/build.sh b/scripts/build.sh
index 029d87d..fcd7396 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -20,7 +20,7 @@ mkdir -p "$DIST"
# Icon, version and manifest for ntp-sync.exe.
(cd "$ROOT/cmd/ntp-sync" && go run github.com/tc-hib/go-winres@v0.3.3 make \
--in winres/winres.json --out rsrc --arch amd64,arm64 \
- --product-version "$WINVER" --file-version "$WINVER")
+ --product-version "${WINVER%.0}" --file-version "${WINVER%.0}")
# The window reads its version for the file properties from wails.json.
python3 - "$ROOT/gui/wails.json" "$WINVER" <<'PY' 2>/dev/null || sed -i.bak -E "s/\"productVersion\": \"[^\"]*\"/\"productVersion\": \"${WINVER%.0}\"/" "$ROOT/gui/wails.json"