| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Federation has never worked between two hubs, and the tests said so without
anyone reading it that way.
`federation.py` did `from meshbay_hub.auth import _hub_id, _hub_sk_pem` at
import — which is before `load_hub_keypair` runs. So it held the key as
`None` and the identity as the module default: `_issue_mhp_token` could only
raise, and `/mhp/info`, the directory export and every token announced this
instance as `meshbay.org` whatever it was configured as. Read through
accessors now, at call time.
And `_verify_mhp_token` named no audience while `_issue_mhp_token` sets one.
PyJWT refuses a token carrying `aud` when decode is given none, so every
token this hub issues was rejected by every hub running this code. Naming the
audience fixes that and makes the binding real: a token minted for one peer
is refused by another, which is what stops a captured request being replayed
at a third hub. The comment claiming audience binding was unavailable because
"the sending side is unbuilt" was describing a function four lines below it.
Both were already written down. `test_federation.py` built envelopes by hand
without an `aud`; `test_public_groups_toggle.py` signed its own token with a
comment saying `_issue_mhp_token` "binds `_hub_sk_pem` at import time, before
the lifespan loads it, so it cannot be used from a test", and another saying
PyJWT rejects a token carrying `aud` when decode is given none. Both
observations were exactly right, and both were treated as facts to route
around. When a test has to work around the code to run, the thing it worked
around is the finding. Those helpers now go through the real issuer, and two
tests pin the identity and the audience refusal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4YmK41VsEURWFdop4EEeT
|
|
|
A registered peer was trusted with more than "advertise your own
public groups":
- `receive_directory` set `source_hub` from `body.hub_id`, so a peer
could relay or spoof a third hub's groups into our directory. It is
now bound to the token's verified `iss`. The push is also capped
(500 groups/request, 2000/peer), rows are type- and length-checked,
and a federated id that collides with a local group is refused so it
cannot shadow one.
- `receive_revocation` forwarded the peer's token to local nodes,
which reject a token signed by another hub's key — a silent no-op,
and there is no local node hosting a federated group anyway. It now
verifies the inner token against the sending peer's key and, for
`target == "group"`, prunes our copy of the peer's directory entry
when `source_hub` matches. A peer cannot revoke our users or a group
it did not advertise.
- The state-changing endpoints (`POST /mhp/directory`, `/mhp/revoke`)
now reject a replayed `jti` within the token's TTL. Audience binding
is unavailable — the sending side that would set `aud` is unbuilt —
and this covers the replay concern in its place; the idempotent
`GET /mhp/directory` is not affected.
Third security review, finding M4.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pG75yGK3NthNfyjH74omG
|