diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-30 21:04:39 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-30 21:04:39 +0200 |
| commit | 0378e8e0912a1a7e6cea4424e69d524e7afecbf8 (patch) | |
| tree | 4ae94e32d6638b4c2cc1ae4f74cbe5d00c940636 /packages/meshbay-node/src/meshbay_node | |
| parent | 0ed56d3a1b4f71cf622d3e27edc87a15ef33c185 (diff) | |
| download | meshbay-0378e8e0912a1a7e6cea4424e69d524e7afecbf8.tar.gz | |
fix: an identity signs a named kind, and a device approval answers a request
The desktop main process builds every transcript itself from fields
(transcripts.js) and signs no raw bytes; the page's identity has the same
contract (crypto.js transcriptFor). The keyring seals no bundle while browser
access is off. On the node, device_add must redeem a pending request filed by
the same keys, and device_revoke is signed under its own prefix
(meshbay:device_revoke:v1), so a retirement signature admits nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/roster.py | 13 | ||||
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc/admission.py | 18 |
2 files changed, 20 insertions, 11 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py index 8144373..0116aaa 100644 --- a/packages/meshbay-node/src/meshbay_node/roster.py +++ b/packages/meshbay-node/src/meshbay_node/roster.py @@ -512,19 +512,22 @@ class Roster: await self._db.commit() return expires - async def take_device_request(self, code_hash: str, - user_id: str) -> dict | None: + async def take_device_request(self, code_hash: str, user_id: str, + pk_ed25519: str, pk_x25519: str) -> dict | None: """ - Claim a pending request by its hash, for this account only. + Claim a pending request by its hash, for this account and these keys. Single use and scoped to the account: a request filed for one person cannot be redeemed by another even with the code, and a code that has - been spent is gone. + been spent is gone. Scoped to the keys too: the request is what the new + device filed and signed, so an approval redeeming it admits those keys + and no others. """ assert self._db async with self._db.execute( "SELECT * FROM device_requests WHERE code_hash = ? AND user_id = ? " - "AND expires_at > ?", (code_hash, user_id, _now()) + "AND pk_ed25519 = ? AND pk_x25519 = ? AND expires_at > ?", + (code_hash, user_id, pk_ed25519, pk_x25519, _now()) ) as cur: row = await cur.fetchone() if row is None: 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 f94cade..20ebc79 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/admission.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/admission.py @@ -20,6 +20,7 @@ from meshbay_common.device import ( device_add_transcript, device_hello_transcript, device_request_transcript, + device_revoke_transcript, ) from meshbay_common.join import JOIN_TTL, ROLE_MEMBER, ROLE_OPERATOR, join_transcript from meshbay_common.protocol import MNP @@ -516,10 +517,17 @@ class AdmissionMixin: pk_ed_b64 = str(msg.get("pk_ed25519", "")) pk_x_b64 = str(msg.get("pk_x25519", "")) + code_hash = str(msg.get("code_hash", "")) ts = int(msg.get("ts", 0)) if not (pk_ed_b64 and pk_x_b64): self._send({"type": "error", "detail": "Missing device keys"}) return + # An approval answers a request the new device filed and signed with + # these keys. Without one, a countersignature alone — obtained however + # it was — would admit any key its holder chose. + if not code_hash: + self._send({"type": "error", "detail": "No pending request named"}) + return if abs(time.time() - ts) > DEVICE_TTL: self._send({"type": "error", "detail": "Approval expired"}) return @@ -543,9 +551,8 @@ class AdmissionMixin: # Spend the request. Single use: an approval cannot be replayed, and a # code that was used is gone whatever else happens next. - code_hash = str(msg.get("code_hash", "")) - if code_hash and not await roster.take_device_request( - code_hash, self._user_id): + if not await roster.take_device_request( + code_hash, self._user_id, pk_ed_b64, pk_x_b64): self._send({"type": "error", "detail": "That request is no longer pending"}) return @@ -696,10 +703,9 @@ class AdmissionMixin: self._send({"type": "error", "detail": "No such device"}) return - transcript = device_add_transcript( + transcript = device_revoke_transcript( node_pk_b64=self._node_pk_b64(), user_id=self._user_id, - pk_ed25519_b64=target, pk_x25519_b64=victim["pk_x25519"], - nonce_node=self._nonce_node, ts=ts) + pk_ed25519_b64=target, nonce_node=self._nonce_node, ts=ts) signer = await self._verify_device_signer(roster, transcript, msg.get("sig", "")) if signer is None: |