diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-14 22:20:15 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-14 22:20:15 +0200 |
| commit | 28c88908b88c58cba4de08710f6bb268ede02d57 (patch) | |
| tree | 7e55f276ee0a8a25f6175f9ef81ebb845d1c218d /packages/meshbay-node/src/meshbay_node/transport/webrtc | |
| parent | 51705056a578d2649a989bf14a056fbcfae58a6b (diff) | |
| download | meshbay-28c88908b88c58cba4de08710f6bb268ede02d57.tar.gz | |
feat(node): accept a device link request before the proof (MNP 6.3)
A new device of a known account holds no identity the node pinned, so it
cannot complete a handshake until a pinned device approves it: it now files
device_add_request in the pre-proof window, as the desktop app's case
already required. The request stays inert until countersigned; at most 10
are pending per account, the oldest dropped. A 6.2 node answers it
"Handshake required", which a client reads as "cannot link here".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/webrtc')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc/admission.py | 25 | ||||
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py | 6 |
2 files changed, 21 insertions, 10 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/admission.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/admission.py index fd9c756..baa8001 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/admission.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/admission.py @@ -404,12 +404,16 @@ class AdmissionMixin: """ A new device files itself as pending, bound to a code it displays. - Served in the pre-proof window: by construction the caller holds no key - this node knows, so there is nothing yet to prove. Filing is inert — - nothing is admitted until an existing device countersigns. + Served in the pre-proof window as well as after it: by construction the + caller holds no key this node knows, so it cannot prove the group key — + filing is what will give it one. The account is the token's, which the + node authorized before issuing the challenge. Filing is inert — nothing + is admitted until an existing device countersigns. """ roster = self._ctx.get("roster") - if roster is None or not self._user_id or not self._nonce_node: + user_id = self._user_id or getattr(self, "_pending_sub", None) + username = self._username or getattr(self, "_pending_username", "") or "" + if roster is None or not user_id or not self._nonce_node: self._send({"type": "error", "detail": "Not ready for a device request"}) return @@ -426,7 +430,7 @@ class AdmissionMixin: # The account must already be known here. Anti-spam rather than a # security boundary: the filing key is unpinned by construction, so this # bounds the table, not the trust. - existing = await roster.list_devices(self._user_id) + existing = await roster.list_devices(user_id) if not existing: self._send({"type": "error", "detail": "This account has no device on this node yet — " @@ -444,7 +448,7 @@ class AdmissionMixin: return transcript = device_request_transcript( - node_pk_b64=self._node_pk_b64(), user_id=self._user_id, + node_pk_b64=self._node_pk_b64(), user_id=user_id, pk_ed25519_b64=pk_ed_b64, pk_x25519_b64=pk_x_b64, code_hash=code_hash, nonce_node=self._nonce_node, ts=ts) try: @@ -461,11 +465,11 @@ class AdmissionMixin: ttl = int(self._ctx.get("device_request_ttl") or 3600) expires = await roster.file_device_request( - user_id=self._user_id, username=self._username or "", + user_id=user_id, username=username, pk_ed25519=pk_ed_b64, pk_x25519=pk_x_b64, code_hash=code_hash, ttl=ttl) - self._audit("device_request", f"{pk_ed_b64[:16]}") - log.info("Device request filed for %s (%s)", self._user_id[:8], + self._audit_join("device_request", f"{pk_ed_b64[:16]}") + log.info("Device request filed for %s (%s)", user_id[:8], pk_ed_b64[:16]) self._send({"type": MNP.DEVICE_REQUEST_ACK, "v": MNP_VERSION, "expires_at": expires}) @@ -761,7 +765,8 @@ class AdmissionMixin: """ self._device_attempts = getattr(self, "_device_attempts", 0) + 1 if self._device_attempts > 5: - self._audit("device_attempts_exceeded", str(self._device_attempts)) + # _audit_join: a device request may be filed before the proof. + self._audit_join("device_attempts_exceeded", str(self._device_attempts)) self._send({"type": "error", "detail": "Too many device attempts on this connection"}) return False diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py index 0ceefb5..7452f42 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py @@ -120,6 +120,12 @@ class DispatchMixin: self._spawn(self._do_gek_bundle_fetch()) else: self._spawn(self._do_keypair_bundle_fetch()) + elif mtype == MNP.DEVICE_REQUEST and self._gek_challenge is not None: + # A machine with no key on this node cannot prove the group key — + # filing is how it gets one. Inert: nothing is admitted until a + # device already pinned countersigns, the code is bound to the + # keys, and the device budget bounds it (MNP 6.3). + self._spawn(self._do_device_request(msg)) elif mtype == MNP.JOIN_REQUEST and self._nonce_node: # Valid both before the GEK proof (a new member has no GEK to prove # with) and after it (an operator pairing a browser is already |