aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-13 14:32:47 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-13 14:32:47 +0200
commit38ad6c54f8c8a9180f0f7522dead38e57760d55e (patch)
tree9e842e7b72f34ec42263b20e84c5eb780c375a17
parent8c5227365118383540a5e77b1885aef7e62bf6ec (diff)
downloadmeshbay-38ad6c54f8c8a9180f0f7522dead38e57760d55e.tar.gz
feat(client): pin node identities on first use — closes 11.5.8
The client verified the node's Ed25519 signature but did not remember which key it had seen, so a substituted node was caught only by its lack of the GEK. Trust On First Use: the node's public key is recorded per node_id on the first successful handshake and compared on every later one. A change is refused outright — strict, per operator decision. A warning users can click through is decorative, and this is the SSH known-hosts tradeoff taken deliberately. Scope, stated honestly: with C6 closed this is defence in depth, not the primary control. A substituted node already fails the GEK proof. Pinning covers the case where an attacker HAS the group key — an ex-member, or a leaked GEK — and swaps the node underneath, which the proof alone cannot distinguish from the real one. Strict refusal needs an escape hatch or it is a dead end: a node operator who reinstalls and loses their keystore generates a new pk_node and would otherwise lock out every member. Settings gains a "Node identities" section showing the pin count and clearing them, with copy telling the user to verify out of band first. Also exposed as MeshBayTransport.clearNodePin() for the native client. Tests: hub+common green; all five static JS files syntax-checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js20
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/i18n.js4
2 files changed, 24 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index dee47ec..a006dfe 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -1978,6 +1978,15 @@ function SettingsPage({ user, theme, onThemeChange, groups }) {
const [currentNodeKey, setCurrentNodeKey] = useState(null);
const [nodeKeyStatus, setNodeKeyStatus] = useState('');
const [nodeKeyLoading, setNodeKeyLoading] = useState(false);
+ const [pinCount, setPinCount] = useState(
+ () => (window.MeshBayTransport?.pinnedNodeCount?.() ?? 0));
+
+ // 11.5.8: node identity pins are refused strictly on change, so users need a
+ // deliberate way to accept a legitimate rotation (operator reinstalled a node).
+ const clearPins = useCallback(() => {
+ window.MeshBayTransport?.clearNodePin?.();
+ setPinCount(window.MeshBayTransport?.pinnedNodeCount?.() ?? 0);
+ }, []);
useEffect(() => {
hubFetch(`/v1/users/${user.username}/pubkeys`, { token: user.token })
@@ -2069,6 +2078,17 @@ function SettingsPage({ user, theme, onThemeChange, groups }) {
</div>
<div class="settings-section">
+ <h3 class="settings-heading">${t('settings.node_pins')}</h3>
+ <p class="settings-hint">${t('settings.node_pins_hint')}</p>
+ <div class="settings-row">
+ <span class="settings-label">${t('settings.node_pins_count', { n: pinCount })}</span>
+ <button class="btn-secondary" onClick=${clearPins} disabled=${pinCount === 0}>
+ ${t('settings.node_pins_clear')}
+ </button>
+ </div>
+ </div>
+
+ <div class="settings-section">
<h3 class="settings-heading">${t('settings.appearance')}</h3>
<div class="settings-row">
<span class="settings-label">${t('settings.theme')}</span>
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js
index 3450735..b0de4b9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js
@@ -120,6 +120,10 @@ const en = {
'settings.coming_soon': 'Coming soon.',
'settings.profile': 'Profile',
'settings.username': 'Username',
+ 'settings.node_pins': 'Node identities',
+ 'settings.node_pins_hint': "Each node's identity key is remembered the first time you connect. If it changes, the connection is refused — that is expected only when an operator reinstalls a node. Verify with them before clearing.",
+ 'settings.node_pins_count': '{n} pinned',
+ 'settings.node_pins_clear': 'Clear pinned identities',
'settings.appearance': 'Appearance',
'settings.theme': 'Theme',
'settings.theme_light': 'Light',