6 Commits
Author SHA1 Message Date
ksmithandClaude Sonnet 5 4c649f727f Bump version to v2026.9.8.2
Same-day patch release: RDP TLS certificate verification fix and
Flathub submission prep.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:27:22 -06:00
ksmithandClaude Sonnet 5 18f234105d packaging: fix Flathub linter findings
Runs Flathub's own flatpak-builder-lint against both manifests and
the metainfo, and fixes what it found:

- only-arches doesn't belong in the manifest itself (linter: manifest
  unknown property); it belongs in a separate flathub.json, which is
  what Flathub's own build infrastructure actually reads it from.
- Bumps the KDE runtime from 6.10 to 6.11 per the linter's outdated-
  runtime warning; verified the app still builds and launches against
  it before committing to the bump.

The one remaining linter finding, finish-args-ssh-filesystem-access,
is a deliberate policy flag rather than a bug — Flathub requires a
written justification for any ~/.ssh access in the submission PR,
which docs/FLATHUB.md now documents with precedent from already-
approved apps in the same situation.

Also confirms (via a real interactive test with xdotool) that the
private-key Browse button correctly triggers the desktop portal
file chooser inside the sandbox, closing the last open verification
item from the previous commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:26:14 -06:00
ksmithandClaude Sonnet 5 186480dcf5 packaging: prepare Flatpak for Flathub submission
Adds a production manifest (packaging/flatpak/flathub/) using a
pinned git source instead of the local-dir source the dev manifest
uses, so Flathub's build infrastructure can reproduce the build
independently.

Narrows the dev and production manifests' filesystem permission from
--filesystem=home to --filesystem=~/.ssh (read-write, needed for SSH
known_hosts/config) after confirming in the actual sandbox that: SSH
already works there (provided by the KDE runtime base, no extra
packaging needed), RDP needs no filesystem access at all (its
certificate trust never touched disk even before today's fix), and
QFileDialog's private-key/export pickers route through the desktop
portal rather than needing static filesystem access.

Expands AppStream metainfo with bugtracker/vcs-browser URLs, a
developer block, a releases entry, an OARS content rating, and three
screenshots (profiles view, an active SSH session, an active RDP
session) using real test-system profiles.

Adds docs/FLATHUB.md tracking overall submission readiness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:19:09 -06:00
ksmithandClaude Sonnet 5 dd974c684a RDP: enforce TLS certificate verification (was fully disabled)
IgnoreCertificate was hardcoded TRUE, meaning FreeRDP's entire
certificate-verification pipeline was bypassed: every RDP server's
TLS certificate was silently accepted, including certificates that
had changed since a prior trusted connection to the same host. That
is precisely the scenario TLS verification exists to catch — an
active MITM presenting a different certificate was indistinguishable
from a legitimate server.

Switches to FreeRDP's own trust-on-first-use certificate store
(AutoAcceptCertificate) so first-time connections still connect
without a prompt, matching SSH's "accept-new" known-hosts policy.
Certificate changes now correctly refuse the connection by default,
via VerifyChangedCertificateEx, with a clear message (host, port,
old/new SHA256 fingerprints) surfaced through the existing
connection-failure event log rather than adding a new, redundant
logging path.

Also fixes CertificateCallbackPreferPEM, which handed the full PEM
certificate to the verify callbacks instead of a short fingerprint —
harmless while those callbacks were dead code, but would have
flooded the event log with multi-KB certificate dumps once actually
exercised.

Verified end-to-end against real infrastructure: first connection
trusts and stores the certificate silently, a simulated changed
certificate (server key swapped) is correctly refused with a clear
message, and restoring the original certificate reconnects normally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:18:57 -06:00
ksmithandClaude Sonnet 5 8c56d489af docs: update latest checkpoint tag to v2026.9.8
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 14:20:07 -06:00
ksmithandClaude Sonnet 5 d7910f1631 docs: link the v2026.9.8 release
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 14:17:09 -06:00
12 changed files with 203 additions and 15 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.21)
project(OrbitHub VERSION 2026.9.8 LANGUAGES CXX)
project(OrbitHub VERSION 2026.9.8.2 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
+12 -1
View File
@@ -16,18 +16,29 @@ OrbitHub is in active development.
- Milestones completed: M0-M5, and M7-M9
- Current milestone: Milestone 10 (v1.0 Stabilization)
- Deferred milestone: Milestone 6 (VNC Fully Working)
- Latest checkpoint tag: `v0-m9-done`
- Latest checkpoint tag: `v2026.9.8.2`
- VNC implementation milestone (M6) is currently deferred
Progress and milestone details:
- [docs/PROGRESS.md](docs/PROGRESS.md)
Latest release (installers for Windows, Linux, and macOS):
- [v2026.9.8.2](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.2)
## Screenshots
![Profile list with folders](docs/images/screenshot-profiles.png)
Profiles organized into folders, with protocol, host, and tags shown at a glance. (Sample data shown; not real hosts.)
![Active SSH terminal session](docs/images/screenshot-ssh-session.png)
An interactive SSH terminal session in a tab, with the event log below.
![Active RDP session](docs/images/screenshot-rdp-session.png)
An embedded RDP session in a tab.
## Implemented Features
### Profile Management
+67
View File
@@ -0,0 +1,67 @@
# Flathub Submission Readiness
Tracks OrbitHub's readiness for submission to Flathub. This is a separate
checklist from `docs/PROGRESS.md`'s development milestones, since Flathub
submission is an external process with its own requirements.
## Status
| Item | Status |
|---|---|
| Production manifest with pinned, reproducible git source | Done — `packaging/flatpak/flathub/org.darksingularity.OrbitHub.yml`; commit pin updated at each tagged release |
| `flathub.json` for build settings (`only-arches`, etc.) | Done — `packaging/flatpak/flathub/flathub.json` |
| Offline build (no network fetches during build) | Verified — no `FetchContent`/`ExternalProject`/`curl`/`wget` in CMake; all vendored deps committed in `third_party/`; confirmed with a real `flatpak-builder` build |
| SSH client available inside the sandbox | Verified — provided by the `org.kde.Platform` runtime base, no packaging needed |
| `--filesystem=home` removed | Done — narrowed to `--filesystem=~/.ssh` (read-write, needed for `known_hosts` and SSH config) |
| SSH known-hosts trust persists with narrowed permissions | Verified in sandbox against real infrastructure |
| RDP works with zero filesystem permission | Verified — FreeRDP's cert trust store lives outside the sandboxed home path concerns entirely (see below) |
| Private-key/export file pickers use the desktop portal | **Verified interactively**`QFileDialog`'s Browse button correctly opens the native GTK portal chooser ("Select Private Key"), which can browse the full filesystem via user consent regardless of the sandbox's static `~/.ssh`-only grant |
| Current, supported KDE runtime | Verified — upgraded to `6.11` (linter's recommended latest); confirmed the app still builds and launches against it |
| Desktop entry validates | Verified |
| Application icon validates | Verified — PNGs at all standard hicolor sizes, matching the real app icon |
| MetaInfo/AppStream validates | Verified via both `appstreamcli validate` and Flathub's own `flatpak-builder-lint appstream` (0 errors either way) |
| Screenshots present | Done — profiles view, active SSH session, active RDP session, all captured against real (test) infrastructure |
| Release information present | Done — `<releases>` block with `v2026.9.8` (add an entry per future tagged release) |
| Developer/project URLs present | Done — homepage, bugtracker, vcs-browser, developer block |
| Architecture support decided | `x86_64` only (no ARM hardware available to test FreeRDP/WinPR on aarch64), set via `flathub.json`'s `only-arches` |
| Flathub manifest linter passes | One expected finding remains: `finish-args-ssh-filesystem-access` (see below) — everything else passes, including `only-arches` placement and runtime-version currency |
| AppStream linter passes | Passing (both `appstreamcli validate` and `flatpak-builder-lint appstream`) |
| Clean install works without host dependencies | Verified via local `.flatpak` bundle install and launch, on both KDE 6.10 and 6.11 runtimes |
### `finish-args-ssh-filesystem-access` — expected, needs a submission-time justification
Flathub's linter flags *any* `~/.ssh` filesystem grant by policy — it's not a
bug in this manifest, it's a deliberate prompt for the submitter to justify
the access during PR review. Checked the linter's own exceptions list:
several existing SSH-client apps already have this exact permission approved
with justifications like *"Read-only access to ~/.ssh is required to load
SSH keys for connecting to devices over SSH"* and *"Needed to manage SSH keys
and configurations for connections"* — OrbitHub's case is the same pattern
(read-write, specifically for `known_hosts` persistence and default identity
file discovery). Include a similar justification in the submission PR.
## Related finding (not a packaging blocker)
During permission-narrowing research, RDP certificate verification was found
to be completely disabled (`IgnoreCertificate=TRUE`, all server certificates
silently accepted including *changed* ones). This has been fixed separately
in `src/rdp_session_backend.cpp` — FreeRDP's own trust-on-first-use
certificate store is now used, matching SSH's known-hosts model. Not a
Flathub-specific issue, but worth noting since it was found in the course of
this work.
## Explicitly out of scope for this repo
- Opening the actual submission PR against `github.com/flathub/flathub`
requires the maintainer's GitHub identity, done outside this repo.
- ARM64 build/testing — no hardware available.
- Flathub's post-acceptance developer-verification step — done via
Flathub's own website after acceptance, using DNS control of
`darksingularity.org`.
## Files
- Dev manifest (local iteration, `type: dir`): `packaging/flatpak/org.darksingularity.OrbitHub.yml`
- Flathub submission manifest (pinned `type: git`): `packaging/flatpak/flathub/org.darksingularity.OrbitHub.yml`
- AppStream metainfo: `packaging/linux/org.darksingularity.OrbitHub.metainfo.xml`
- Desktop entry: `packaging/linux/org.darksingularity.OrbitHub.desktop`
+2
View File
@@ -197,6 +197,8 @@ Validation:
Git:
- Tag: `v0-m9-done`
- Release: [v2026.9.8](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8) (`v2026.9.8` tag, installers for Windows/Linux/macOS)
- Release: [v2026.9.8.2](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.2) — same-day patch fixing RDP TLS certificate verification (was fully disabled) and preparing Flatpak packaging for Flathub submission
## Milestone 10 - v1.0 Stabilization
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

+3
View File
@@ -0,0 +1,3 @@
{
"only-arches": ["x86_64"]
}
@@ -0,0 +1,23 @@
app-id: org.darksingularity.OrbitHub
runtime: org.kde.Platform
runtime-version: "6.11"
sdk: org.kde.Sdk
command: orbithub
finish-args:
- --share=network
- --share=ipc
- --socket=fallback-x11
- --socket=wayland
- --device=dri
- --filesystem=~/.ssh
modules:
- name: orbithub
buildsystem: cmake-ninja
builddir: true
config-opts:
- -DCMAKE_BUILD_TYPE=Release
sources:
- type: git
url: https://git.darksingularity.org/DarkSingularity/orbithub.git
tag: v2026.9.8
commit: 68214db744714f19e0dcf01c1e535afed2dab1e7
@@ -1,6 +1,6 @@
app-id: org.darksingularity.OrbitHub
runtime: org.kde.Platform
runtime-version: "6.10"
runtime-version: "6.11"
sdk: org.kde.Sdk
command: orbithub
finish-args:
@@ -9,7 +9,7 @@ finish-args:
- --socket=fallback-x11
- --socket=wayland
- --device=dri
- --filesystem=home
- --filesystem=~/.ssh
modules:
- name: orbithub
buildsystem: cmake-ninja
@@ -10,7 +10,38 @@
</description>
<launchable type="desktop-id">org.darksingularity.OrbitHub.desktop</launchable>
<url type="homepage">https://git.darksingularity.org/DarkSingularity/orbithub</url>
<url type="bugtracker">https://git.darksingularity.org/DarkSingularity/orbithub/issues</url>
<url type="vcs-browser">https://git.darksingularity.org/DarkSingularity/orbithub</url>
<developer id="org.darksingularity">
<name>DarkSingularity</name>
</developer>
<provides>
<binary>orbithub</binary>
</provides>
<content_rating type="oars-1.1" />
<screenshots>
<screenshot type="default">
<caption>Profiles organized into folders</caption>
<image>https://git.darksingularity.org/DarkSingularity/orbithub/raw/branch/main/docs/images/screenshot-profiles.png</image>
</screenshot>
<screenshot>
<caption>Active SSH terminal session</caption>
<image>https://git.darksingularity.org/DarkSingularity/orbithub/raw/branch/main/docs/images/screenshot-ssh-session.png</image>
</screenshot>
<screenshot>
<caption>Active RDP session</caption>
<image>https://git.darksingularity.org/DarkSingularity/orbithub/raw/branch/main/docs/images/screenshot-rdp-session.png</image>
</screenshot>
</screenshots>
<releases>
<release version="2026.9.8.2" date="2026-09-08">
<url>https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.2</url>
<description>
<p>Fixes RDP connections silently accepting any server TLS certificate, including changed ones. Certificates are now verified with trust-on-first-use, matching SSH's known-hosts behavior.</p>
</description>
</release>
<release version="2026.9.8" date="2026-09-08">
<url>https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8</url>
</release>
</releases>
</component>
+53 -11
View File
@@ -420,9 +420,19 @@ BOOL orbitPreConnect(freerdp* instance)
}
rdpSettings* settings = instance->context->settings;
freerdp_settings_set_bool(settings, FreeRDP_CertificateCallbackPreferPEM, TRUE);
// FALSE gives the verify callbacks a short SHA256 fingerprint string
// (e.g. "ab:cd:12:...") instead of the full PEM certificate blob, which
// is what actually belongs in a user-facing changed-certificate message.
freerdp_settings_set_bool(settings, FreeRDP_CertificateCallbackPreferPEM, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_DesktopResize, TRUE);
freerdp_settings_set_bool(settings, FreeRDP_IgnoreCertificate, TRUE);
// Trust servers on first connection (matching SSH's "accept-new" known-hosts
// policy) and persist that trust via FreeRDP's own certificate store, but
// actually verify it on every subsequent connection: IgnoreCertificate
// previously bypassed verification entirely, so a changed certificate
// (e.g. an active MITM) was silently accepted, indistinguishable from a
// legitimate server. See orbitVerifyChangedCertificateEx below.
freerdp_settings_set_bool(settings, FreeRDP_IgnoreCertificate, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_AutoAcceptCertificate, TRUE);
return TRUE;
}
@@ -708,10 +718,13 @@ DWORD orbitVerifyCertificateEx(freerdp* instance,
const char*,
DWORD)
{
// With FreeRDP_AutoAcceptCertificate set, FreeRDP accepts and stores a
// first-seen certificate itself without calling this callback at all;
// it is kept as a defensive fallback for any code path that reaches it.
if (instance != nullptr && instance->context != nullptr) {
if (RdpSessionBackend* backend = backendFromContext(instance->context)) {
emit backend->eventLogged(
QStringLiteral("Accepting server certificate for %1:%2.")
QStringLiteral("Trusting new server certificate for %1:%2.")
.arg(QString::fromUtf8(host == nullptr ? "" : host))
.arg(port));
}
@@ -725,21 +738,42 @@ DWORD orbitVerifyChangedCertificateEx(freerdp* instance,
const char*,
const char*,
const char*,
const char* newFingerprint,
const char*,
const char*,
const char*,
const char*,
const char* oldFingerprint,
DWORD)
{
// The server's certificate no longer matches the one trusted on a prior
// connection. This is exactly the scenario TLS verification exists to
// catch: either the server legitimately rotated its certificate, or an
// active man-in-the-middle is presenting a different one. Refuse the
// connection by default rather than silently trusting it.
const QString hostStr = QString::fromUtf8(host == nullptr ? "" : host);
const QString message = QStringLiteral(
"Server certificate for %1:%2 has changed since it was last trusted. "
"Connection refused for safety — this could mean the server's "
"certificate was legitimately renewed, or that a different host is "
"impersonating it. Previously trusted fingerprint: %3 — now "
"presented: %4. If this change is expected, remove the stored entry "
"for this host from FreeRDP's certificate store "
"(~/.config/freerdp/server) and reconnect.")
.arg(hostStr)
.arg(port)
.arg(QString::fromUtf8(oldFingerprint == nullptr ? "unknown" : oldFingerprint))
.arg(QString::fromUtf8(newFingerprint == nullptr ? "unknown" : newFingerprint));
// Not logged directly here: recordCertificateRejection() feeds this
// message into the generic connect-failure handling in workerMain(),
// which already reports it through eventLogged/connectionError/setState
// (the same channels every other RDP connection failure uses) — an
// extra direct log call here would just duplicate that.
if (instance != nullptr && instance->context != nullptr) {
if (RdpSessionBackend* backend = backendFromContext(instance->context)) {
emit backend->eventLogged(
QStringLiteral("Accepting changed server certificate for %1:%2.")
.arg(QString::fromUtf8(host == nullptr ? "" : host))
.arg(port));
backend->recordCertificateRejection(message);
}
}
return 1;
return 0;
}
const char* authReasonName(rdp_auth_reason reason)
@@ -1451,6 +1485,11 @@ void RdpSessionBackend::setState(SessionState state, const QString& message)
emit eventLogged(message);
}
void RdpSessionBackend::recordCertificateRejection(const QString& reason)
{
m_certificateRejectionReason = reason;
}
bool RdpSessionBackend::validateProfile(QString& message) const
{
const Profile& p = profile();
@@ -1510,6 +1549,7 @@ void RdpSessionBackend::workerMain()
m_workerRunning.store(false);
return;
#else
m_certificateRejectionReason.clear();
ensureFreeRdpRuntimeInitialized();
freerdp* instance = freerdp_new();
@@ -1671,7 +1711,9 @@ void RdpSessionBackend::workerMain()
emit eventLogged(QStringLiteral("RDP connect aborted: %1").arg(raw));
emitStateAsync(SessionState::Disconnected, disconnectMessageForCode(code));
} else {
const QString mapped = mapRdpError(code);
const QString mapped = m_certificateRejectionReason.isEmpty()
? mapRdpError(code)
: m_certificateRejectionReason;
emit eventLogged(QStringLiteral("RDP connect failure detail: %1").arg(raw));
emitConnectionFailureAsync(mapped, raw);
emitStateAsync(SessionState::Failed, mapped);
+9
View File
@@ -90,6 +90,14 @@ private:
void* m_cliprdrContext;
QString m_pendingLocalClipboardText;
// Set synchronously by orbitVerifyChangedCertificateEx (called from this
// object's own worker thread during freerdp_connect) when a server's TLS
// certificate has changed since a prior trusted connection. Read back by
// workerMain() right after freerdp_connect() fails, to show the specific
// reason instead of a generic "TLS negotiation failed" message. Cleared
// at the start of every connect attempt.
QString m_certificateRejectionReason;
void setState(SessionState state, const QString& message);
bool validateProfile(QString& message) const;
void startWorker();
@@ -109,6 +117,7 @@ public:
void onCliprdrServerFormatList(bool hasUnicodeText);
void onCliprdrServerFormatDataRequest(uint32_t requestedFormatId);
void onCliprdrServerFormatDataResponse(bool success, const uint8_t* data, uint32_t size);
void recordCertificateRejection(const QString& reason);
private:
void emitStateAsync(SessionState state, const QString& message);
void emitConnectionFailureAsync(const QString& displayMessage, const QString& rawMessage);