Context
Phase 1 of GHSA-wfx7-w6p8-4jr2 shipped in PR #2947 (commit 5275f42). It silently captures the radio's TLS cert fingerprint on first SmartLink connect and logs a warning on mismatch — but the connection still proceeds.
Phase 2 turns the warn-only behavior into actual enforcement + UX.
What Phase 2 adds
1. Mismatch dialog (enforcement)
When WanConnection::onTlsConnected detects a fingerprint mismatch:
- Pause the connection (don't send
wan validate handle=... yet)
- Emit new signal
certFingerprintMismatch(host, expectedHex, presentedHex)
- UI catches signal and shows a modal:
Certificate changed for <radio nickname / IP>
Expected: aa:bb:cc:…:ff (pinned 2026-04-15)
Presented: 11:22:33:…:99
This may be normal (radio firmware update, replaced radio) or a possible man-in-the-middle attack.
[Accept new certificate] [Reject and disconnect]
- Accept → overwrite the stored pin, resume connection (call internal
sendWanValidate())
- Reject →
disconnectFromRadio(), emit errorOccurred("Certificate rejected by user")
2. Pinned Certificates settings UI
New section at Radio Setup → SmartLink → Pinned Certificates:
- List view:
<host, fingerprint, pinned-date> rows
- Per-entry "Forget" button removes that host's pin (next connect re-pins as first-use)
- Global "Forget all" button at the bottom
Use case: user deliberately updated radio firmware and expects a cert change → forget the old pin so the next connect doesn't trip the mismatch dialog.
3. Cache schema bump (with back-compat)
Phase 1 cache shape: {"<host>": "<hex>"}
Phase 2 cache shape: {"<host>": {"fp": "<hex>", "pinnedAt": "<iso8601>"}}
Cache loader must handle the Phase 1 string-only entries (treat as pinnedAt: null) to avoid breaking existing pins on upgrade.
Touch points
| File |
Change |
src/core/WanConnection.h |
Add acceptPresentedCert(), rejectPresentedCert(), signal certFingerprintMismatch(...); defer wan validate write until user decision |
src/core/WanConnection.cpp |
Mismatch path emits signal + stores presented cert pending decision; cache JSON shape bump + back-compat reader |
src/gui/SmartLinkApplet.cpp (or wherever SmartLink connect UI lives) |
Modal dialog wired to the mismatch signal |
New: src/gui/PinnedCertsApplet.cpp |
List/delete UI in Radio Setup → SmartLink |
Open questions
- Defer-vs-disconnect-and-reprompt: preferred to defer the
wan validate send until user responds, so the attacker never sees an authenticated session. Alternative is to let the session complete and then disconnect on rejection — simpler code but exposes a live session window to the MITM.
- First-use prompt? Phase 1 silently pins on first use. Adding a "Pin this cert?" prompt is UX overhead at a moment when the user has no signal to decide on. Recommend keeping silent first-use; only mismatch prompts.
Effort estimate
~half day:
- WanConnection changes: ~2 hours
- Mismatch dialog: ~1 hour
- Pinned Certs Settings applet: ~2 hours
- Test pass with manual cert rotation: ~30 min
Test plan
Requires SmartLink connection over WAN (cannot exercise from LAN-attached radio). Need either:
- Off-LAN test via phone hotspot tether, OR
- A second tester / external network
Verify:
- First-use: pin captured silently
- Match: silent debug log
- Mismatch dialog: appears on intentional cert rotation; Accept persists new pin; Reject disconnects cleanly
- Forget button: removes pin from cache; next connect re-pins
- Upgrade from Phase 1: existing string-only cache entries don't crash; treated as legitimate prior pins
References
Context
Phase 1 of GHSA-wfx7-w6p8-4jr2 shipped in PR #2947 (commit 5275f42). It silently captures the radio's TLS cert fingerprint on first SmartLink connect and logs a warning on mismatch — but the connection still proceeds.
Phase 2 turns the warn-only behavior into actual enforcement + UX.
What Phase 2 adds
1. Mismatch dialog (enforcement)
When
WanConnection::onTlsConnecteddetects a fingerprint mismatch:wan validate handle=...yet)certFingerprintMismatch(host, expectedHex, presentedHex)sendWanValidate())disconnectFromRadio(), emiterrorOccurred("Certificate rejected by user")2. Pinned Certificates settings UI
New section at Radio Setup → SmartLink → Pinned Certificates:
<host, fingerprint, pinned-date>rowsUse case: user deliberately updated radio firmware and expects a cert change → forget the old pin so the next connect doesn't trip the mismatch dialog.
3. Cache schema bump (with back-compat)
Phase 1 cache shape:
{"<host>": "<hex>"}Phase 2 cache shape:
{"<host>": {"fp": "<hex>", "pinnedAt": "<iso8601>"}}Cache loader must handle the Phase 1 string-only entries (treat as
pinnedAt: null) to avoid breaking existing pins on upgrade.Touch points
src/core/WanConnection.hacceptPresentedCert(),rejectPresentedCert(), signalcertFingerprintMismatch(...); deferwan validatewrite until user decisionsrc/core/WanConnection.cppsrc/gui/SmartLinkApplet.cpp(or wherever SmartLink connect UI lives)src/gui/PinnedCertsApplet.cppOpen questions
wan validatesend until user responds, so the attacker never sees an authenticated session. Alternative is to let the session complete and then disconnect on rejection — simpler code but exposes a live session window to the MITM.Effort estimate
~half day:
Test plan
Requires SmartLink connection over WAN (cannot exercise from LAN-attached radio). Need either:
Verify:
References