The console served the identity nothing pins, and the tray called it dead #393
Merged
enricobuehler
merged 2 commits from 2026-08-24 20:55:11 +00:00
worktree-web-console-native-identity into main
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1e2b956de6 |
fix(web,tray,host): review follow-ups — pair the halves, and stop two comments overclaiming
ci / docs-drift (pull_request) Successful in 25s
ci / bun-nix (pull_request) Successful in 26s
ci / docs-site (pull_request) Successful in 1m11s
ci / web (pull_request) Successful in 1m14s
ci / rust-arm64 (pull_request) Successful in 1m52s
android / android (pull_request) Successful in 5m43s
ci / rust (pull_request) Successful in 5m43s
nix / flake (pull_request) Successful in 6m34s
Review of the parent commit. One real defect, the rest accuracy.
The resolver could hand back a MISMATCHED pair, which is the one invariant its
own header promised it never would: `nativeCert` came from `dirname(cert)` and
`nativeKey` from `dirname(key)`, two independent directories, so
("/a/cert.pem", "/b/key.pem") resolved to /a/native-cert.pem + /b/native-key.pem
— two unrelated files presented as a pair. No shipped launcher splits them, but
the guard is one comparison and it is the whole point of the module.
Rewritten off a suffix test instead of `node:path`, which fixes two more things
in passing. `node:path` resolves per-RUNTIME, so a POSIX CI runner reads
`C:\ProgramData\punktfunk\cert.pem` as one long filename and never swaps — and
Windows, where windows/service.rs hands us exactly that, is the platform the CI
job can never exercise. The suffix test gives the same answer everywhere and is
now covered by a win32 case. It also leaves the prefix VERBATIM, where
`join(dirname(p), …)` normalised /a/b/../cert.pem into a different directory the
moment `b` was a symlink.
Existence is no longer enough: `pf_paths::write_secret_file` is
create+truncate+write rather than temp+rename, so a console starting mid-write
could adopt a 0-byte cert and leave `Bun.serve` throwing on every restart. Not
every launcher retries forever — the Steam Deck unit is `Restart=on-failure`
under the default rate limit, i.e. permanently dead. The check is now a
non-empty stat, mirroring the host's own `!c.trim().is_empty()`. Verified: with
native-cert.pem truncated to 0 bytes the console starts and serves the legacy
pair.
Two comments of mine overclaimed and are corrected rather than left to mislead:
* serve() said "cert.pem existing implies the native pair does too". False on
an upgraded host whose native clients pinned the legacy cert — load_or_adopt
returns it and writes no native files at all. The ordering claim that IS
true is narrower: whenever that call writes a native pair, it does so before
cert.pem appears.
* the tray said the console entry "always opens the same URL regardless of the
probe". True of the menu entry, but win.rs gates the tray-icon single-click
on console_up. Also notes that the Windows probe was never pinned to begin
with (punktfunk_config_dir is None off Linux), so that half is a no-op.
Rest is doc drift the parent commit annotated in two launchers but not the other
four: web.env.example, README, web-run.cmd, and the ci.yml comment that still
said the web test step was "Scoped to server/".
18/18 web tests (was 14), biome and rustfmt clean, and the runtime check re-run
against a fresh build: both pairs -> P-256 with SANs; 0-byte native cert ->
legacy RSA, console still serving.
|
||
|
|
49b5ffa2d8 |
fix(web,tray,host): the console served the identity nothing pins, and the tray called it dead
android / android (pull_request) Successful in 4m33s
ci / bun-nix (pull_request) Successful in 24s
ci / docs-drift (pull_request) Successful in 29s
ci / web (pull_request) Successful in 1m6s
ci / docs-site (pull_request) Successful in 1m41s
ci / rust-arm64 (pull_request) Successful in 2m54s
nix / flake (pull_request) Successful in 6m58s
ci / rust (pull_request) Successful in 7m2s
A Linux operator saw "Open web console (not responding)" in the tray next to a
tooltip reading "idle", and the console would not load in a browser either.
The host has kept two identities since the identity split (crate::identity):
native-cert.pem/native-key.pem (P-256, real SANs — what the native QUIC plane,
the mgmt API and every native client pin) and the legacy cert.pem/key.pem (RSA,
CN=punktfunk, NO SAN, kept byte-stable for Moonlight). The web console never
followed the split. Every launcher — the systemd unit, the NixOS module, the
Windows service supervisor, web-run.cmd, the Steam Deck installer — still names
the LEGACY pair, and none of them CAN choose: `Environment=` has no "this file,
else that one". So the console served a certificate with no SAN at all, which
costs twice over:
* browsers reject a CN-only cert outright (ERR_CERT_COMMON_NAME_INVALID), so
the console the operator was told to open does not load;
* the tray's loopback liveness probe reused the agent PINNED to the mgmt
identity — the native cert — so rustls refused the handshake and a perfectly
healthy console was labelled "not responding". The "idle" tooltip beside it
is the proof: the same agent reached mgmt fine on the very same tick.
The entry is the one place every launcher routes through, so the choice is made
there: prefer the native sibling pair when both files exist, as a PAIR or not at
all (a native cert with the legacy key completes no handshake with anyone). A
host that never took the split has no native pair on disk and falls through
unchanged, as does an operator-supplied cert under any other name. This also
hands the bundled bun the smaller secret: on a default build key.pem is the
Moonlight pairing SIGNING key, native-key.pem is only a TLS key.
The tray's console probe loses its pin rather than gaining a second one. It is a
different server and there is no rule that it presents the mgmt certificate — an
operator fronting the console with their own LAN-CA cert would have hit this just
as squarely. The probe sends no credentials, reads no body, and decides only a
menu label, so there is nothing for a pin to protect.
`serve` now resolves the native identity BEFORE minting the legacy one. That
closes a first-run window where the console (which waits on cert.pem) could start
between the two writes and serve the SAN-less cert for the rest of the boot, and
it fixes a second latent fault: with cert.pem missing but native clients paired,
the old order let load_or_create mint a brand-new cert.pem that load_or_adopt
then adopted while logging that it was preserving their pins.
Verified against the built server: configured exactly as the shipped unit does
(PUNKTFUNK_UI_TLS_CERT=.../cert.pem), it now serves the P-256 cert with
DNS:localhost/IP:127.0.0.1; with the native pair removed it serves the RSA cert
as before. 14/14 web tests pass, biome and rustfmt clean.
|