diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6654d26..f656c54 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -10,7 +10,7 @@ body: id: version attributes: label: Application version - placeholder: "0.34.0" + placeholder: "0.35.0" validations: required: true - type: dropdown diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08b1593..e053aa2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Signed Windows release +name: Windows release on: push: @@ -7,6 +7,8 @@ on: permissions: contents: write + id-token: write + attestations: write jobs: release: @@ -44,70 +46,31 @@ jobs: - name: Build application run: pyinstaller --noconfirm AntennaPatternLab.spec - - name: Import code-signing certificate - id: certificate - shell: pwsh - env: - CERTIFICATE_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }} - CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }} - run: | - if (-not $env:CERTIFICATE_BASE64 -or -not $env:CERTIFICATE_PASSWORD) { - throw "Code-signing secrets are not configured." - } - $pfx = Join-Path $env:RUNNER_TEMP "code-signing.pfx" - [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:CERTIFICATE_BASE64)) - $password = ConvertTo-SecureString $env:CERTIFICATE_PASSWORD -AsPlainText -Force - $certificate = Import-PfxCertificate -FilePath $pfx -CertStoreLocation Cert:\CurrentUser\My -Password $password | - Where-Object HasPrivateKey | - Select-Object -First 1 - if (-not $certificate.Thumbprint) { throw "Certificate import failed." } - "thumbprint=$($certificate.Thumbprint)" >> $env:GITHUB_OUTPUT - Remove-Item -LiteralPath $pfx -Force - - - name: Locate signing and installer tools + - name: Install Inno Setup id: tools shell: pwsh run: | choco install innosetup --no-progress -y - $signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" | - Sort-Object FullName -Descending | - Select-Object -First 1 $iscc = Get-Item "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" - if (-not $signtool -or -not $iscc) { throw "Required build tools were not found." } - "signtool=$($signtool.FullName)" >> $env:GITHUB_OUTPUT + if (-not $iscc) { throw "Inno Setup was not found." } "iscc=$($iscc.FullName)" >> $env:GITHUB_OUTPUT - - name: Sign application - shell: pwsh - run: | - & "${{ steps.tools.outputs.signtool }}" sign ` - /fd SHA256 ` - /sha1 "${{ steps.certificate.outputs.thumbprint }}" ` - /td SHA256 ` - /tr http://timestamp.digicert.com ` - "dist\AntennaPatternLab\AntennaPatternLab.exe" - if ($LASTEXITCODE -ne 0) { throw "Application signing failed." } - - - name: Build signed installer and update manifest + - name: Build installer and update manifest shell: pwsh run: | .\build_installer.ps1 ` -Compiler "${{ steps.tools.outputs.iscc }}" ` - -SignTool "${{ steps.tools.outputs.signtool }}" ` - -CertificateThumbprint "${{ steps.certificate.outputs.thumbprint }}" ` -ReleaseBaseUrl "https://github.com/bubakbubak500/AntennaPatternLab/releases/latest/download" - - name: Verify signatures and package application + - name: Package application and publish checksums shell: pwsh run: | $project = Get-Content pyproject.toml -Raw $version = [regex]::Match($project, '(?m)^version\s*=\s*"([^"]+)"').Groups[1].Value $installer = "release\AntennaPatternLab-$version-setup-win-x64.exe" - foreach ($file in @("dist\AntennaPatternLab\AntennaPatternLab.exe", $installer)) { - $signature = Get-AuthenticodeSignature -FilePath $file - if ($signature.Status -ne "Valid") { - throw "Invalid Authenticode signature for $file`: $($signature.Status)" - } + $signature = Get-AuthenticodeSignature -FilePath $installer + if ($signature.Status -ne "NotSigned") { + throw "Unexpected installer signing state: $($signature.Status)" } Compress-Archive -Path "dist\AntennaPatternLab\*" ` -DestinationPath "release\AntennaPatternLab-$version-win-x64.zip" @@ -116,6 +79,13 @@ jobs: ForEach-Object { "$($_.Hash.ToLowerInvariant()) $([IO.Path]::GetFileName($_.Path))" } | Set-Content release\SHA256SUMS.txt -Encoding ascii + - name: Attest release build provenance + uses: actions/attest@v4 + with: + subject-path: | + release/AntennaPatternLab-*-setup-win-x64.exe + release/AntennaPatternLab-*-win-x64.zip + - name: Publish GitHub Release shell: pwsh env: @@ -129,5 +99,6 @@ jobs: "release\release-manifest.json" ` "release\SHA256SUMS.txt" ` --title "Antenna Pattern Lab $version" ` - --generate-notes ` + --notes-file "docs\RELEASE_NOTES_0.35.0.md" ` + --latest ` --verify-tag diff --git a/README.md b/README.md index ad9ac51..fcb9274 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,20 @@ without requiring a server. ## Download -Download the current signed Windows installer from +Download the current Windows installer from [GitHub Releases](https://github.com/bubakbubak500/AntennaPatternLab/releases/latest). The application can check the same official GitHub release channel for updates. -Automatic checks are opt-in. Every downloaded installer is accepted only after its -SHA-256 digest matches the published release manifest, and it is never launched -without an additional confirmation. +It checks in the background on every startup and fails silently when offline. +Every downloaded installer is accepted only after its SHA-256 digest matches the +published release manifest, and it is never launched without an additional +confirmation. + +> [!WARNING] +> Current Windows packages are not Authenticode-signed. Windows can therefore +> display **Unknown publisher** or a Microsoft Defender SmartScreen warning. +> Download only from this repository's Releases page. Each release includes +> SHA-256 checksums and GitHub build-provenance attestations. ## Main features @@ -38,13 +45,14 @@ without an additional confirmation. - local SQLite storage and diagnostic export; - English and Czech application UI; - verified, consent-driven setup assistance for WSJT-X and Hamlib; -- signed Windows application and installer releases. +- reproducible Windows releases with checksums and GitHub build provenance. ## Quick start 1. Download the latest installer from [Releases](https://github.com/bubakbubak500/AntennaPatternLab/releases/latest). -2. Verify that Windows reports the expected publisher before installing. +2. Compare the installer's SHA-256 with `SHA256SUMS.txt`; Windows currently + reports an unknown publisher because the release is unsigned. 3. Start Antenna Pattern Lab and complete the first-run assistant. 4. Use **Help → Add demo data** to explore the application without a radio. 5. For live collection, enter your callsign and locator, select the band and mode, @@ -99,9 +107,9 @@ Build an unsigned local installer: .\build_installer.ps1 ``` -Official releases are built and Authenticode-signed by GitHub Actions. Signing -credentials are stored only as encrypted GitHub Actions secrets and are never -committed to this repository. See [Release process](docs/RELEASING.md). +Official releases are built by GitHub Actions, accompanied by SHA-256 checksums +and a GitHub artifact attestation. Authenticode signing can be added later +without changing the update channel. See [Release process](docs/RELEASING.md). ## Contributing diff --git a/SECURITY.md b/SECURITY.md index aa0105c..a6866d1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -22,6 +22,8 @@ mitigation. You should receive an initial response within seven days. ## Release integrity Official Windows releases are published only through GitHub Releases. The -application and installer are Authenticode-signed, release checksums are -published, and the in-app updater verifies the installer's SHA-256 before making -it available to launch. +current application and installer are not Authenticode-signed. Release checksums +and GitHub build-provenance attestations are published, and the in-app updater +verifies the installer's SHA-256 before making it available to launch. Windows +may show an unknown-publisher warning until a trusted signing certificate is +introduced. diff --git a/docs/RELEASE_NOTES_0.35.0.md b/docs/RELEASE_NOTES_0.35.0.md new file mode 100644 index 0000000..a3864dd --- /dev/null +++ b/docs/RELEASE_NOTES_0.35.0.md @@ -0,0 +1,23 @@ +# Antenna Pattern Lab 0.35.0 + +This release establishes the public GitHub update channel and improves Windows +shell integration. + +## What changed + +- The application now checks the official GitHub Releases channel in the + background on every startup. Offline or temporary GitHub failures are silent. +- The Updates dialog now explains the download, SHA-256 verification, and + installation flow instead of exposing an editable manifest URL. +- The application process, Start menu shortcut, and desktop shortcut now share + the stable Windows AppUserModelID `OK7PS.AntennaPatternLab`. +- The installer deploys the application icon explicitly for Windows shortcuts. +- Release assets include `SHA256SUMS.txt`, an update manifest, and a GitHub + build-provenance attestation. + +## Windows signing notice + +The application and installer in this release are **not Authenticode-signed**. +Windows can display an **Unknown publisher** or Microsoft Defender SmartScreen +warning. Download only from the official AntennaPatternLab GitHub Releases page +and verify the SHA-256 checksum supplied with the release. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 016b1cf..055c0de 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -2,30 +2,25 @@ Official releases are created by `.github/workflows/release.yml`. -## Required repository secrets - -- `WINDOWS_SIGNING_CERTIFICATE_BASE64`: a Base64-encoded, password-protected PFX - code-signing certificate; -- `WINDOWS_SIGNING_CERTIFICATE_PASSWORD`: the PFX password. - -The certificate must support Windows code signing and chain to a certificate -authority trusted by supported Windows versions. Never commit the PFX or password. +No release secrets are currently required. Packages are intentionally published +without an Authenticode signature until a trusted code-signing certificate is +available. The release page and application clearly disclose this. ## Create a release 1. Update the version in `pyproject.toml`. 2. Ensure the Inno Setup definition and application package use the same version. 3. Merge the version change through an approved pull request. -4. Create and push a signed or annotated tag named `vMAJOR.MINOR.PATCH`. +4. Create and push a tag named `vMAJOR.MINOR.PATCH`. 5. The release workflow verifies that the tag matches the project version. -6. GitHub Actions runs tests, builds the application, signs the application EXE, - builds and signs the installer and uninstaller, verifies Authenticode, creates - the update manifest and checksums, and publishes a GitHub Release. +6. GitHub Actions runs tests, builds the application and installer, confirms that + the installer is unsigned, creates the update manifest and checksums, records + GitHub build-provenance attestations, and publishes a GitHub Release. The update manifest is always available at: `https://github.com/bubakbubak500/AntennaPatternLab/releases/latest/download/release-manifest.json` -If certificate renewal is required, replace the two secrets before creating a -release. Existing releases remain timestamp-valid after the certificate expires, -provided the timestamp was created while the certificate was valid. +When Authenticode signing is added later, keep the same asset names and manifest +URL so installed applications remain on the same update channel. Never commit a +PFX file or its password. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index ddd70d7..6dfbebf 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -99,8 +99,9 @@ CSV export can be used for independent analysis or archival. ## 9. Updates -Open **Help → Updates** to check the official GitHub release channel. Automatic -checks at startup are disabled until you opt in. +Open **Settings → Updates** to check the official GitHub release channel. Automatic +checks run in the background on every startup. If the internet or GitHub is +unavailable, startup continues normally and no error is shown. The update process: @@ -110,7 +111,9 @@ The update process: 4. publishes the `.exe` only after the digest matches; 5. asks before launching the verified installer. -Always confirm that Windows displays the expected Authenticode publisher. +Current releases are not Authenticode-signed, so Windows may report an unknown +publisher or show a SmartScreen warning. Download only from the official GitHub +Releases page and compare the installer with the published SHA-256 checksum. ## 10. Diagnostics and data diff --git a/installer/AntennaPatternLab.iss b/installer/AntennaPatternLab.iss index 7a392a8..8d4e07e 100644 --- a/installer/AntennaPatternLab.iss +++ b/installer/AntennaPatternLab.iss @@ -1,6 +1,6 @@ #define MyAppName "Antenna Pattern Lab" #ifndef MyAppVersion -#define MyAppVersion "0.34.0" +#define MyAppVersion "0.35.0" #endif #define MyAppPublisher "OK7PS" #define MyAppExeName "AntennaPatternLab.exe" @@ -28,6 +28,7 @@ SolidCompression=yes WizardStyle=modern dynamic SetupIconFile=..\src\antenna_pattern_lab\assets\app-icon.ico UninstallDisplayIcon={app}\{#MyAppExeName} +ChangesAssociations=yes CloseApplications=yes RestartApplications=no VersionInfoVersion={#MyAppVersion}.0 @@ -70,11 +71,12 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ [Files] Source: "..\dist\AntennaPatternLab\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "..\src\antenna_pattern_lab\assets\app-icon.ico"; DestDir: "{app}"; DestName: "AntennaPatternLab.ico"; Flags: ignoreversion [Icons] -Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\AntennaPatternLab.ico"; AppUserModelID: "OK7PS.AntennaPatternLab" Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" -Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\AntennaPatternLab.ico"; AppUserModelID: "OK7PS.AntennaPatternLab"; Tasks: desktopicon [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent diff --git a/pyproject.toml b/pyproject.toml index 5e61444..b7b2b86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "antenna-pattern-lab" -version = "0.34.0" +version = "0.35.0" description = "Empirical antenna coverage analysis from PSK Reporter FT8 spots" readme = "README.md" requires-python = ">=3.11" diff --git a/src/antenna_pattern_lab/__init__.py b/src/antenna_pattern_lab/__init__.py index 7b448a8..531b939 100644 --- a/src/antenna_pattern_lab/__init__.py +++ b/src/antenna_pattern_lab/__init__.py @@ -1,3 +1,3 @@ """Antenna Pattern Lab.""" -__version__ = "0.34.0" +__version__ = "0.35.0" diff --git a/src/antenna_pattern_lab/app.py b/src/antenna_pattern_lab/app.py index 3ab7c32..0b5a502 100644 --- a/src/antenna_pattern_lab/app.py +++ b/src/antenna_pattern_lab/app.py @@ -21,8 +21,20 @@ def application_icon_path() -> Path: return Path(__file__).resolve().parent / "assets" / "app-icon.png" +def set_windows_app_user_model_id() -> None: + """Give Windows one stable identity for the process and its shortcuts.""" + if sys.platform != "win32": + return + import ctypes + + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + "OK7PS.AntennaPatternLab" + ) + + def main() -> int: logging.basicConfig(level=logging.INFO) + set_windows_app_user_model_id() application = QApplication(sys.argv) application.setApplicationName("Antenna Pattern Lab") application.setOrganizationName("OK7PS") @@ -43,7 +55,7 @@ def main() -> int: window = MainWindow(repository) window.show() QTimer.singleShot(0, window.show_setup_if_needed) - QTimer.singleShot(1500, window.check_updates_if_enabled) + QTimer.singleShot(1500, window.check_updates_at_startup) return application.exec() diff --git a/src/antenna_pattern_lab/ui.py b/src/antenna_pattern_lab/ui.py index a74d77e..afb2a42 100644 --- a/src/antenna_pattern_lab/ui.py +++ b/src/antenna_pattern_lab/ui.py @@ -663,6 +663,7 @@ def __init__(self, repository: SpotRepository, settings: QSettings | None = None saved_language = str(self.settings.value("language", "CZE")) self.language_code = saved_language if saved_language in TRANSLATIONS else "CZE" self.setWindowTitle("Antenna Pattern Lab · FT8 / WSPR") + self.setWindowIcon(QApplication.instance().windowIcon()) self.resize(1180, 760) self._build_ui() self._connect_signals() @@ -2403,18 +2404,10 @@ def show_setup_if_needed(self) -> None: def _open_updates(self) -> None: UpdateDialog(self.settings, self.language_code, self).exec() - def check_updates_if_enabled(self) -> None: - if not bool(int(self.settings.value("automatic_update_checks", 0))): - return - url = str( - self.settings.value("release_manifest_url", DEFAULT_RELEASE_MANIFEST_URL) - ).strip() - if not url: - return - + def check_updates_at_startup(self) -> None: def worker() -> None: try: - result = check_for_update(url, __version__) + result = check_for_update(DEFAULT_RELEASE_MANIFEST_URL, __version__) except Exception as exc: self.bridge.update_failed.emit(str(exc)) else: diff --git a/src/antenna_pattern_lab/update_dialog.py b/src/antenna_pattern_lab/update_dialog.py index defb370..9cf2dc1 100644 --- a/src/antenna_pattern_lab/update_dialog.py +++ b/src/antenna_pattern_lab/update_dialog.py @@ -3,11 +3,9 @@ from PySide6.QtCore import QSettings, QStandardPaths, QUrl from PySide6.QtGui import QDesktopServices from PySide6.QtWidgets import ( - QCheckBox, QDialog, QHBoxLayout, QLabel, - QLineEdit, QMessageBox, QPushButton, QVBoxLayout, @@ -25,35 +23,75 @@ TEXT = { "CZE": { "title": "Aktualizace aplikace", - "channel": "HTTPS adresa release manifestu", - "channel_help": "Manifest musí obsahovat version, installer_url a sha256. Bez oficiálního kanálu ponechte pole prázdné.", - "automatic": "Automaticky kontrolovat při spuštění (pouze opt-in)", - "check": "Zkontrolovat", + "heading": "Oficiální aktualizace přes GitHub Releases", + "channel_help": ( + "Antenna Pattern Lab při každém spuštění na pozadí zkontroluje " + "oficiální GitHub release kanál. Když není internet dostupný, " + "aplikace pokračuje bez upozornění." + ), + "process": ( + "Aktuální verze: {version}\n\n" + "Instalátor se stáhne do složky Stažené soubory a aplikace před " + "jeho nabídnutím ověří publikovaný kontrolní součet SHA-256. " + "Instalátor se nikdy nespustí bez vašeho potvrzení.\n\n" + "Toto vydání zatím není podepsané Authenticode certifikátem. " + "Windows proto může zobrazit varování „Neznámý vydavatel“. " + "Stahujte pouze z oficiálního GitHub repozitáře." + ), + "releases": ( + 'Otevřít GitHub Releases' + ), + "check": "Zkontrolovat nyní", "download": "Stáhnout ověřený instalátor…", "close": "Zavřít", - "missing": "Nejdříve zadejte HTTPS adresu oficiálního manifestu.", "current": "Používáte aktuální verzi {version}.", "available": "Je dostupná verze {version}.", "failed": "Kontrola aktualizace selhala: {error}", "confirm_title": "Stáhnout aktualizaci?", - "confirm": "Stáhnout instalátor verze {version} a ověřit jeho SHA-256? Nic se nespustí automaticky.", - "downloaded": "Ověřený instalátor byl uložen:\n{path}\n\nOtevřít jej nyní?", + "confirm": ( + "Stáhnout instalátor verze {version} a ověřit jeho SHA-256? " + "Nic se nespustí automaticky." + ), + "downloaded": ( + "Ověřený instalátor byl uložen:\n{path}\n\nOtevřít jej nyní?" + ), }, "ENG": { "title": "Application updates", - "channel": "HTTPS release manifest URL", - "channel_help": "Official GitHub Releases channel. The manifest must contain version, installer_url and sha256.", - "automatic": "Check automatically at startup (opt-in only)", - "check": "Check", + "heading": "Official updates through GitHub Releases", + "channel_help": ( + "Antenna Pattern Lab checks the official GitHub release channel " + "in the background on every startup. If the internet is unavailable, " + "the application continues silently." + ), + "process": ( + "Current version: {version}\n\n" + "The installer is downloaded to your Downloads folder. Before it " + "is offered, the application verifies its published SHA-256 checksum. " + "The installer is never launched without your confirmation.\n\n" + "This release is not yet signed with an Authenticode certificate, " + "so Windows may display an “Unknown publisher” warning. Download " + "only from the official GitHub repository." + ), + "releases": ( + 'Open GitHub Releases' + ), + "check": "Check now", "download": "Download verified installer…", "close": "Close", - "missing": "Enter the HTTPS URL of the official manifest first.", "current": "You are using the current version {version}.", "available": "Version {version} is available.", "failed": "Update check failed: {error}", "confirm_title": "Download update?", - "confirm": "Download installer version {version} and verify its SHA-256? Nothing will run automatically.", - "downloaded": "The verified installer was saved to:\n{path}\n\nOpen it now?", + "confirm": ( + "Download installer version {version} and verify its SHA-256? " + "Nothing will run automatically." + ), + "downloaded": ( + "The verified installer was saved to:\n{path}\n\nOpen it now?" + ), }, } @@ -65,23 +103,28 @@ def __init__(self, settings: QSettings, language: str = "CZE", parent=None): self.text = TEXT[language if language in TEXT else "CZE"] self.update_check: UpdateCheck | None = None self.setWindowTitle(self.text["title"]) - self.resize(650, 300) + self.resize(680, 390) + layout = QVBoxLayout(self) - layout.addWidget(QLabel(self.text["channel"])) - self.channel_url = QLineEdit( - str(settings.value("release_manifest_url", DEFAULT_RELEASE_MANIFEST_URL)) - ) - layout.addWidget(self.channel_url) + layout.addWidget(QLabel(f"