| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`ruff check .` had gone unrun long enough to report 568 errors, which is the
same as having no linter: the next real finding would have been invisible in the
noise. This is the 521 it fixes by itself, in 173 files, and nothing else — the
98 it cannot fix are the next commit.
What actually changed: import sorting (225), imports nobody used (87, none of
them a re-export — no `__init__.py` is touched, which was the one way this could
have broken an import elsewhere), `datetime.timezone.utc` to `datetime.UTC` (69)
and `asyncio.TimeoutError` to `TimeoutError` (18), both plain aliases on the 3.12
this project requires, `Optional[X]` to `X | None` (24), and f-strings with
nothing to interpolate (19).
Checked rather than assumed: every module in the three packages still imports,
and the suite is 2893 passed — the same count, test for test, as the merge
before it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
names its parameters
`pw_version` 4: Argon2id 64 MiB, t=3, lanes=4 — RFC 9106's second recommended
setting. A v3 hash (256 MB) still verifies at its own parameters and is
rewritten at the new ones on the next sign-in, through the rehash path that
already existed.
Why not more. The verifier matters against an offline attacker holding the
database; online guessing is bounded by the sign-in lockout. That attacker pays
the client's 600 000 PBKDF2-SHA512 iterations and the hub's Argon2id per guess,
since `auth_key` is 256 bits and cannot be searched directly. Memory above
64 MiB multiplies that cost by a constant — at most 16 at 256 MB, less with
PBKDF2 counted — while the hub pays the same memory at every sign-in, one
derivation at a time. Measured on meshbay.org: 450 ms at 256 MB, 105 ms at
64 MiB, so a burst of sign-ins clears about four times faster.
Changing the current version exposed a latent lockout. `hash_password` always
used the current version's parameters, while the raw-password scheme recorded
`pw_version = 2` — harmless while versions 2 and 3 shared their parameters,
and with version 4 every legacy registration and v1→v2 rehash would have
stored a 64 MiB hash labelled 256 MB, which nothing could then verify. Seventeen
tests caught it. `hash_password` now takes the version it is hashing for.
The OpenSSL deadlock between two concurrent `lanes=4` derivations is the same at
64 MiB, so Argon2 stays on its single worker. The loop-stall test measures
against a v3 hash, because half of a 45 ms inline derivation is too close to
scheduling noise to be a reliable bound.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcF3QKWii7uQ2kSyXErzCt
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
One derivation is 256 MB and a quarter to half a second of CPU (240 ms here,
485 ms on meshbay.org). All eleven call sites — sign-in, registration, the two
rehashes, passphrase change, reset and account deletion — ran it inline in an
async handler, so every one stopped the whole hub for that long: no request
served, no node socket read, no offer relayed. Measured on a local hub during
eight concurrent sign-ins, the worst `/v1/health` response went from 232 ms to
10 ms; the sign-ins themselves take the same time.
It could not simply go to a thread pool. Two concurrent `lanes=4` derivations
deadlock inside OpenSSL and never return, at no CPU — reproduced on
cryptography 50.0.x / OpenSSL 4.0.x both locally and on meshbay.org, while
`lanes=1` does not. `lanes` is part of every stored hash, so it is not ours to
change, and inline on the loop two derivations could never overlap, which is
the only reason production never hung.
So `auth.hash_password_off_loop` / `verify_password_off_loop` hand the work to
a dedicated executor with exactly one worker. Not a semaphore around
`to_thread`: a cancelled request would release its permit while its thread was
still deriving, and the next derivation would start beside it. One worker also
bounds Argon2's memory to one derivation whatever the number of callers.
`test_argon2_off_loop.py` reads every module for a direct call, pins the single
worker, runs four derivations and four sign-ins concurrently to completion, and
checks the loop keeps turning during a derivation; each fails with its guard
removed. CLAUDE.md and AV9 state the rule and the trap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcF3QKWii7uQ2kSyXErzCt
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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 client whose clock is a little fast could not connect at all: the MNP
handshake verified the hub-issued token with no leeway, so a token whose
`iat` was a few seconds ahead of the node's clock failed with "the token
is not yet valid (iat)". Seen against a freshly-resumed VM guest.
`meshbay_common.handshake.JWT_LEEWAY_SECONDS = 60` is the shared value;
applied to the handshake, the node's own hub-token decode, revocation-token
verification, and the hub's access-token decode. 60s absorbs NTP-level skew
without meaningfully widening the window on a stolen token (they already
carry a jti and an exp).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Registration now creates a pending account and sends a 6-digit code via
email; the account activates only after verification. Email changes on
the profile page follow the same flow. Group invitations send a
notification email to the invitee (without revealing their address to
the inviter) containing the invite code and hub link.
Backend: blind HMAC-SHA256 email index for uniqueness without decryption,
mail.py for localhost Postfix delivery, verification endpoints, cleanup
of expired codes and stale pending accounts, startup backfill of
email_hash for existing users.
Frontend: 3-phase register page, inline email change verification on
profile, invite-notify call with status display. All 10 locales updated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
One keypair was copied to every node its owner joined, so cracking the bundle on
any single node yielded the identity used on all of them: their content on other
operators' machines, and the ability to sign as them anywhere. That lateral reach
was the part of C4 worth attacking.
Each node now gets its own keypair, generated the first time its owner joins it
and left with that node alone. An operator who cracks what sits on their own disk
holds a key that is a stranger to every other node — and on their own node, one
that unlocks nothing they did not already hold: they serve the content, the index
and every byte of it by design.
Nothing changes for the user. A first contact with a node already needed that
operator's code, and the key is created in the same step; a second browser still
recovers it from the node with the passphrase alone. Two operators can also no
longer tell they host the same person by comparing keys.
BREAKING, and deliberately without a compatibility path — the deployment is wiped
for the next demo:
- users.pk_ed25519 / pk_x25519 dropped (migration a7c31f9e40b2)
- registration no longer sends or stores a key
- PUT /v1/users/me/keys and regenerateKeys() gone; rotation is now
`member unpin` plus a fresh code, decided on the machine that pinned it
- /pubkeys returns an account id and a node's linking key. It was the directory
H3 read, and nothing wraps for it any more
- the pk_user JWT claim is gone
That last one closed a live defect the inventory turned up: the node recorded
pk_user as the uploader's identity and authorized deletion against it, so a hub
issuing a token naming its own key could delete anyone's uploads on any node.
Attribution now uses the key the node itself pinned.
A simplification falls out. Registration generates nothing, so a scripted signup
is a real account: `demo.py bootstrap` takes a wiped hub and node to a working
demo with no browser, which was impossible while keys were born in one.
Also fixes, found by running it on a wiped deployment: the key handed back on a
join now belongs to the group the connection is for, not the group named in the
invitation — an operator pairs node-wide but redeems the code while opening a
group, and expects to read it.
Tests: 343, including the two that state the property — a key pinned by one node
is refused at another, and someone else's code does not admit it. Verified end to
end against a wiped hub and node: bootstrap, pair, invite, join, download,
stream, second browser, revoke.
Design: docs/per-node-identity-v1.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Baseline commit capturing in-progress Phase 12 work that was already present
in the working tree (uncommitted) before the Phase 11.5 security remediation
begins. Committed as-is, without review or modification, so that remediation
changes arrive as a separable diff.
Contents: BundleStore (P2P GEK + keypair bundles), password split
(auth_key / bundle_key), node Ed25519 auth (POST /v1/nodes/auth, node-scoped
JWT), GEK-HMAC handshake proof with DTLS channel binding, Ed25519 admin
challenge-response, node local admin UI rewrite, browser key persistence.
Not authored in this session — captured to establish a baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
8.1 Config-based admin authz (require_admin on all admin endpoints)
8.2 Email encrypted at rest (AES-256-GCM, HKDF from hub Ed25519 key)
8.3 Refresh token rotation with family-based reuse detection
8.4 Federation persistence (HubPeer model replaces in-memory dict)
8.5 Federation token verification now async (DB-backed)
8.6 CSAM hash check wired into swarm registration flow
8.7 Rate limiting on auth endpoints (5/10/20 per minute)
8.8 Healthcheck endpoint (GET /v1/health, no auth)
8.9 IP log cleanup background task (365-day retention)
8.10 Argon2id params bumped to 256 MB (pw_version, rehash on login)
Deployed to meshbay.org — schema migrated, existing emails encrypted.
117 tests pass (29 hub, 88 common+node).
Resolves security review items S1, S2, S5.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Implements all 8 milestones (7.0-7.7):
- 7.0: JWT carries `groups` claim; node verifies group membership at
MNP handshake (QUIC + TCP+TLS). Resolves security review C2.
- 7.1: QUIC 0-RTT session resumption via stored session tickets
(17-21ms reconnect vs 47ms cold).
- 7.2: Hub→node WebSocket signaling for NAT punch coordination
(`client_incoming`/`punch_ready`) + jti denylist push. Denylist
class blocks revoked users/jtis at handshake.
- 7.3: Multi-group daemon — one QUIC port serves N groups with
per-group GEK, shared_root, and index routing.
- 7.4: HLS streaming via QUIC (STREAM_SEGMENT message type, ffmpeg
segment extraction).
- 7.5: Sender Keys protocol for group chat (Signal Groups approach).
Each member has own sending chain key, HKDF chain ratchet, AES-256-GCM
encryption, Ed25519 signing. Resolves security review C1.
- 7.6: Chat store (SQLite via aiosqlite), CHAT_MESSAGE MNP wire type
with peer broadcast, web UI with WebSocket push.
- 7.7: Argon2id calibration CLI.
First security review included (first-review.md). 109 tests, demo-v3
validated against meshbay.org production hub.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
|
config.py: TOML + env var priority. auth.py: Argon2id passwords,
JWT EdDSA with jti, refresh token hashed (blake3). Routers:
hub (info/pubkey), users (register/login/refresh/pubkeys),
nodes (announce/get), groups (create/gek-bundle/gek-retrieve).
Rate limiting via slowapi. app.py factory with lifespan.
All 40 tests pass (SQLite in-memory, no PostgreSQL required).
Fix: remove tests/__init__.py to resolve namespace conflicts.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|