diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-04 03:03:47 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-04 03:03:47 +0200 |
| commit | dad2157ada303a45655d2919f62905369525636f (patch) | |
| tree | 083d08cd15e1258e4fbcca3c3c619debb2857022 /packages | |
| parent | 41282b997e016da0773276a935434dd03631e1f3 (diff) | |
| download | meshbay-dad2157ada303a45655d2919f62905369525636f.tar.gz | |
fix: tolerate clock skew when verifying JWTs (leeway 60s)
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>
Diffstat (limited to 'packages')
4 files changed, 17 insertions, 3 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/handshake.py b/packages/meshbay-common/src/meshbay_common/handshake.py index 2f3d641..f7d4911 100644 --- a/packages/meshbay-common/src/meshbay_common/handshake.py +++ b/packages/meshbay-common/src/meshbay_common/handshake.py @@ -77,6 +77,13 @@ ROLE_NODE = "node" NONCE_LEN = 32 +# Clock-skew tolerance for JWT `iat`/`exp`/`nbf`. The token is issued by the +# hub and verified by a node, on two machines whose clocks are only as close +# as their NTP — and a VM guest that has just resumed can be tens of seconds +# out. Without this a slightly-fast client cannot connect at all +# ("token is not yet valid (iat)"). +JWT_LEEWAY_SECONDS = 60 + class HandshakeError(Exception): """ @@ -230,7 +237,8 @@ def authorize_token( check entirely and fell back to the node's first group (M1). """ try: - decoded = jwt.decode(token, hub_pk_pem, algorithms=["EdDSA"]) + decoded = jwt.decode(token, hub_pk_pem, algorithms=["EdDSA"], + leeway=JWT_LEEWAY_SECONDS) except Exception as exc: raise HandshakeError(f"Invalid JWT: {exc}") from exc diff --git a/packages/meshbay-hub/src/meshbay_hub/auth.py b/packages/meshbay-hub/src/meshbay_hub/auth.py index 2bf59db..34baf45 100644 --- a/packages/meshbay-hub/src/meshbay_hub/auth.py +++ b/packages/meshbay-hub/src/meshbay_hub/auth.py @@ -166,7 +166,9 @@ def decode_access_token(token: str) -> dict: """Verify and decode an access token. Raises on failure.""" if _hub_pk_pem is None: raise RuntimeError("Hub keypair not loaded") - return jwt.decode(token, _hub_pk_pem, algorithms=["EdDSA"]) + # Clock-skew tolerance (meshbay_common.handshake.JWT_LEEWAY_SECONDS): a + # client whose clock is a little fast must still be able to call the API. + return jwt.decode(token, _hub_pk_pem, algorithms=["EdDSA"], leeway=60) # ── Email encryption at rest ────────────────────────────────────────────────── diff --git a/packages/meshbay-node/src/meshbay_node/hub_client.py b/packages/meshbay-node/src/meshbay_node/hub_client.py index 1e975fa..8873958 100644 --- a/packages/meshbay-node/src/meshbay_node/hub_client.py +++ b/packages/meshbay-node/src/meshbay_node/hub_client.py @@ -129,7 +129,9 @@ class HubClient: access_token = data["access_token"] - decoded = jwt.decode(access_token, hub_pk_pem, algorithms=["EdDSA"]) + from meshbay_common.handshake import JWT_LEEWAY_SECONDS + decoded = jwt.decode(access_token, hub_pk_pem, algorithms=["EdDSA"], + leeway=JWT_LEEWAY_SECONDS) # No pk_user claim to check any more: tokens carry no key. What binds this # token to this node is the Ed25519 challenge it was issued against. assert "jti" in decoded, "Hub token missing jti — hub is outdated" diff --git a/packages/meshbay-node/src/meshbay_node/revocation.py b/packages/meshbay-node/src/meshbay_node/revocation.py index abbff4d..d3ee18f 100644 --- a/packages/meshbay-node/src/meshbay_node/revocation.py +++ b/packages/meshbay-node/src/meshbay_node/revocation.py @@ -66,7 +66,9 @@ class RevocationSubscriber: def verify_and_apply(self, token: str) -> bool: """Verify a revocation token and apply it. Returns True if valid.""" try: + from meshbay_common.handshake import JWT_LEEWAY_SECONDS payload = jwt.decode(token, self._hub_pk_pem, algorithms=["EdDSA"], + leeway=JWT_LEEWAY_SECONDS, options={"verify_exp": False}) if payload.get("type") != "revocation": return False |