diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-12 12:14:40 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-12 16:36:54 +0200 |
| commit | 903ea022e918a05c7c8cb43d95e46d82368566f1 (patch) | |
| tree | b6cb4eb8fbac87f512948f5005e2ed48f26629c9 /packages/meshbay-hub/src/meshbay_hub/auth.py | |
| parent | 6260825bf6d8340549de53905de3bd0b84d97d0a (diff) | |
| download | meshbay-903ea022e918a05c7c8cb43d95e46d82368566f1.tar.gz | |
fix(hub): MHP binds its audience, and the hub knows its own name
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
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/auth.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/auth.py | 20 |
1 files changed, 20 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/auth.py b/packages/meshbay-hub/src/meshbay_hub/auth.py index 34baf45..c5ea34d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/auth.py +++ b/packages/meshbay-hub/src/meshbay_hub/auth.py @@ -88,6 +88,26 @@ def hub_public_key_pem() -> bytes: return _hub_pk_pem +def hub_private_key_pem() -> bytes: + if _hub_sk_pem is None: + raise RuntimeError("Hub keypair not loaded — call load_hub_keypair() first") + return _hub_sk_pem + + +def hub_id() -> str: + """This hub's configured identity. + + An accessor, not the module global, because `load_hub_keypair` runs at + startup and every one of these is set *after* import. A module that wrote + `from meshbay_hub.auth import _hub_id` captured the default and kept it: + `federation.py` did, so it signed with a `None` key and announced itself + as `meshbay.org` whatever its configuration said. Reading through a + function is what makes "call once at startup" true for readers as well as + for the writer. + """ + return _hub_id + + # ── Password ────────────────────────────────────────────────────────────────── def hash_password(password: str) -> tuple[bytes, bytes]: |