hivemind-rendezvous 0.1.1a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hivemind_rendezvous-0.1.1a1/PKG-INFO +118 -0
- hivemind_rendezvous-0.1.1a1/README.md +104 -0
- hivemind_rendezvous-0.1.1a1/hivemind_rendezvous/__init__.py +18 -0
- hivemind_rendezvous-0.1.1a1/hivemind_rendezvous/auth.py +102 -0
- hivemind_rendezvous-0.1.1a1/hivemind_rendezvous/server.py +361 -0
- hivemind_rendezvous-0.1.1a1/hivemind_rendezvous/storage.py +123 -0
- hivemind_rendezvous-0.1.1a1/hivemind_rendezvous/version.py +16 -0
- hivemind_rendezvous-0.1.1a1/hivemind_rendezvous.egg-info/PKG-INFO +118 -0
- hivemind_rendezvous-0.1.1a1/hivemind_rendezvous.egg-info/SOURCES.txt +16 -0
- hivemind_rendezvous-0.1.1a1/hivemind_rendezvous.egg-info/dependency_links.txt +1 -0
- hivemind_rendezvous-0.1.1a1/hivemind_rendezvous.egg-info/entry_points.txt +2 -0
- hivemind_rendezvous-0.1.1a1/hivemind_rendezvous.egg-info/requires.txt +7 -0
- hivemind_rendezvous-0.1.1a1/hivemind_rendezvous.egg-info/top_level.txt +1 -0
- hivemind_rendezvous-0.1.1a1/pyproject.toml +32 -0
- hivemind_rendezvous-0.1.1a1/setup.cfg +4 -0
- hivemind_rendezvous-0.1.1a1/test/test_auth.py +126 -0
- hivemind_rendezvous-0.1.1a1/test/test_server.py +346 -0
- hivemind_rendezvous-0.1.1a1/test/test_storage.py +81 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hivemind-rendezvous
|
|
3
|
+
Version: 0.1.1a1
|
|
4
|
+
Summary: Async store-and-forward dead-drop rendezvous service for HiveMind nodes
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: hivemind-bus-client
|
|
9
|
+
Requires-Dist: json-database>=0.10.2a1
|
|
10
|
+
Requires-Dist: poorman-handshake
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# hivemind-rendezvous
|
|
16
|
+
|
|
17
|
+
An async **store-and-forward dead-drop** for [HiveMind](https://github.com/JarbasHiveMind/HiveMind-core)
|
|
18
|
+
nodes that are never online at the same time. A sender deposits an encrypted
|
|
19
|
+
message addressed to a recipient's public key; the recipient later proves
|
|
20
|
+
ownership of that key and collects the message. No simultaneous connection, no
|
|
21
|
+
shared IP, no persistent HiveMind session.
|
|
22
|
+
|
|
23
|
+
## Where it sits
|
|
24
|
+
|
|
25
|
+
Normal HiveMind links are live encrypted WebSocket connections between a satellite
|
|
26
|
+
and a [hivemind-core](https://github.com/JarbasHiveMind/HiveMind-core) hub. That
|
|
27
|
+
requires both ends to be reachable at once. hivemind-rendezvous fills the gap for
|
|
28
|
+
nodes that are only intermittently online: it is a small neutral HTTP relay that
|
|
29
|
+
holds [`INTERCOM`](https://github.com/JarbasHiveMind/hivemind-websocket-client)
|
|
30
|
+
messages until the recipient comes back to fetch them.
|
|
31
|
+
|
|
32
|
+
The relay never sees plaintext — messages are end-to-end encrypted to the
|
|
33
|
+
recipient's public key before deposit. The relay only stores opaque blobs keyed by
|
|
34
|
+
recipient pubkey and enforces ownership on retrieval.
|
|
35
|
+
|
|
36
|
+
## How it works
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Node A (sender) Rendezvous node Node B (recipient)
|
|
40
|
+
│ │ │
|
|
41
|
+
│-- POST /deposit -->│ │
|
|
42
|
+
│ INTERCOM msg │ │
|
|
43
|
+
│ target=B.pubkey │ (stored, TTL ≤7d) │
|
|
44
|
+
│ │ │
|
|
45
|
+
│ (time passes) │
|
|
46
|
+
│ │<-- POST /retrieve --│
|
|
47
|
+
│ │ sign(B.privkey) │
|
|
48
|
+
│ │-- messages -------->│
|
|
49
|
+
│ │ (deleted) │
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Authentication is **proof of RSA pubkey ownership**: to retrieve, a node signs a
|
|
53
|
+
fresh timestamp with its private key. The relay verifies the signature against the
|
|
54
|
+
claimed pubkey and the timestamp freshness (replay window), then returns and
|
|
55
|
+
deletes the pending messages.
|
|
56
|
+
|
|
57
|
+
## Prerequisites
|
|
58
|
+
|
|
59
|
+
- Python 3.10+
|
|
60
|
+
- An RSA identity for each node (handled by HiveMind / `poorman-handshake`).
|
|
61
|
+
- A reachable host to run the relay (a small VPS, a Pi, or any always-on box the
|
|
62
|
+
intermittent nodes can reach over HTTP).
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install hivemind-rendezvous
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
From source:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/JarbasHiveMind/hivemind-rendezvous
|
|
74
|
+
cd hivemind-rendezvous
|
|
75
|
+
pip install -e .
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quickstart
|
|
79
|
+
|
|
80
|
+
Run the relay on an always-on host:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
hivemind-rendezvous
|
|
84
|
+
# Rendezvous server listening on 0.0.0.0:6789
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
That is the whole relay. Senders `POST /deposit` an INTERCOM message addressed to
|
|
88
|
+
a recipient pubkey; recipients `POST /retrieve` with an ownership proof to collect
|
|
89
|
+
them. See [HTTP API](docs/http-api.md) for the request bodies and
|
|
90
|
+
[examples](docs/examples.md) for deposit/retrieve snippets.
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
`run_server()` arguments (and their defaults):
|
|
95
|
+
|
|
96
|
+
| Argument | Default | Description |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `host` | `0.0.0.0` | Bind address. |
|
|
99
|
+
| `port` | `6789` | Listen port. |
|
|
100
|
+
| `node_pubkey` | `""` | This relay's own RSA pubkey (PEM), served at `/pubkey`. |
|
|
101
|
+
| `deposit_rate_limit` | `60` | Max deposits per client IP per window. |
|
|
102
|
+
| `deposit_rate_window` | `60` | Rate-limit window in seconds. |
|
|
103
|
+
| `require_depositor_proof` | `False` | Require a valid depositor ownership proof on every deposit. |
|
|
104
|
+
|
|
105
|
+
Message TTL is set per deposit (`ttl` field, default and hard cap **7 days**).
|
|
106
|
+
|
|
107
|
+
## Documentation
|
|
108
|
+
|
|
109
|
+
See [`docs/`](docs/index.md):
|
|
110
|
+
|
|
111
|
+
- [How it works](docs/how-it-works.md) — deposit/retrieve flow and authentication.
|
|
112
|
+
- [HTTP API](docs/http-api.md) — endpoints, request/response bodies, error codes.
|
|
113
|
+
- [Deploy](docs/deploy.md) — running and configuring the relay.
|
|
114
|
+
- [Examples](docs/examples.md) — deposit and retrieve from a client.
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
Apache-2.0
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# hivemind-rendezvous
|
|
2
|
+
|
|
3
|
+
An async **store-and-forward dead-drop** for [HiveMind](https://github.com/JarbasHiveMind/HiveMind-core)
|
|
4
|
+
nodes that are never online at the same time. A sender deposits an encrypted
|
|
5
|
+
message addressed to a recipient's public key; the recipient later proves
|
|
6
|
+
ownership of that key and collects the message. No simultaneous connection, no
|
|
7
|
+
shared IP, no persistent HiveMind session.
|
|
8
|
+
|
|
9
|
+
## Where it sits
|
|
10
|
+
|
|
11
|
+
Normal HiveMind links are live encrypted WebSocket connections between a satellite
|
|
12
|
+
and a [hivemind-core](https://github.com/JarbasHiveMind/HiveMind-core) hub. That
|
|
13
|
+
requires both ends to be reachable at once. hivemind-rendezvous fills the gap for
|
|
14
|
+
nodes that are only intermittently online: it is a small neutral HTTP relay that
|
|
15
|
+
holds [`INTERCOM`](https://github.com/JarbasHiveMind/hivemind-websocket-client)
|
|
16
|
+
messages until the recipient comes back to fetch them.
|
|
17
|
+
|
|
18
|
+
The relay never sees plaintext — messages are end-to-end encrypted to the
|
|
19
|
+
recipient's public key before deposit. The relay only stores opaque blobs keyed by
|
|
20
|
+
recipient pubkey and enforces ownership on retrieval.
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Node A (sender) Rendezvous node Node B (recipient)
|
|
26
|
+
│ │ │
|
|
27
|
+
│-- POST /deposit -->│ │
|
|
28
|
+
│ INTERCOM msg │ │
|
|
29
|
+
│ target=B.pubkey │ (stored, TTL ≤7d) │
|
|
30
|
+
│ │ │
|
|
31
|
+
│ (time passes) │
|
|
32
|
+
│ │<-- POST /retrieve --│
|
|
33
|
+
│ │ sign(B.privkey) │
|
|
34
|
+
│ │-- messages -------->│
|
|
35
|
+
│ │ (deleted) │
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Authentication is **proof of RSA pubkey ownership**: to retrieve, a node signs a
|
|
39
|
+
fresh timestamp with its private key. The relay verifies the signature against the
|
|
40
|
+
claimed pubkey and the timestamp freshness (replay window), then returns and
|
|
41
|
+
deletes the pending messages.
|
|
42
|
+
|
|
43
|
+
## Prerequisites
|
|
44
|
+
|
|
45
|
+
- Python 3.10+
|
|
46
|
+
- An RSA identity for each node (handled by HiveMind / `poorman-handshake`).
|
|
47
|
+
- A reachable host to run the relay (a small VPS, a Pi, or any always-on box the
|
|
48
|
+
intermittent nodes can reach over HTTP).
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install hivemind-rendezvous
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
From source:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/JarbasHiveMind/hivemind-rendezvous
|
|
60
|
+
cd hivemind-rendezvous
|
|
61
|
+
pip install -e .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Quickstart
|
|
65
|
+
|
|
66
|
+
Run the relay on an always-on host:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
hivemind-rendezvous
|
|
70
|
+
# Rendezvous server listening on 0.0.0.0:6789
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
That is the whole relay. Senders `POST /deposit` an INTERCOM message addressed to
|
|
74
|
+
a recipient pubkey; recipients `POST /retrieve` with an ownership proof to collect
|
|
75
|
+
them. See [HTTP API](docs/http-api.md) for the request bodies and
|
|
76
|
+
[examples](docs/examples.md) for deposit/retrieve snippets.
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
`run_server()` arguments (and their defaults):
|
|
81
|
+
|
|
82
|
+
| Argument | Default | Description |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `host` | `0.0.0.0` | Bind address. |
|
|
85
|
+
| `port` | `6789` | Listen port. |
|
|
86
|
+
| `node_pubkey` | `""` | This relay's own RSA pubkey (PEM), served at `/pubkey`. |
|
|
87
|
+
| `deposit_rate_limit` | `60` | Max deposits per client IP per window. |
|
|
88
|
+
| `deposit_rate_window` | `60` | Rate-limit window in seconds. |
|
|
89
|
+
| `require_depositor_proof` | `False` | Require a valid depositor ownership proof on every deposit. |
|
|
90
|
+
|
|
91
|
+
Message TTL is set per deposit (`ttl` field, default and hard cap **7 days**).
|
|
92
|
+
|
|
93
|
+
## Documentation
|
|
94
|
+
|
|
95
|
+
See [`docs/`](docs/index.md):
|
|
96
|
+
|
|
97
|
+
- [How it works](docs/how-it-works.md) — deposit/retrieve flow and authentication.
|
|
98
|
+
- [HTTP API](docs/http-api.md) — endpoints, request/response bodies, error codes.
|
|
99
|
+
- [Deploy](docs/deploy.md) — running and configuring the relay.
|
|
100
|
+
- [Examples](docs/examples.md) — deposit and retrieve from a client.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
Apache-2.0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""hivemind-rendezvous — async store-and-forward dead drop for HiveMind nodes.
|
|
2
|
+
|
|
3
|
+
Nodes from different, non-simultaneously-connected hives can exchange INTERCOM
|
|
4
|
+
messages via a shared rendezvous point. The sender deposits a message keyed by
|
|
5
|
+
the recipient's RSA public key; the recipient retrieves it later by proving
|
|
6
|
+
pubkey ownership (signed timestamp, no server-side challenge state).
|
|
7
|
+
|
|
8
|
+
Submodules:
|
|
9
|
+
auth — :func:`~hivemind_rendezvous.auth.sign_ownership` /
|
|
10
|
+
:func:`~hivemind_rendezvous.auth.verify_ownership`
|
|
11
|
+
storage — :class:`~hivemind_rendezvous.storage.RendezvousStore`
|
|
12
|
+
server — :func:`~hivemind_rendezvous.server.run_server`
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from hivemind_rendezvous.version import VERSION
|
|
16
|
+
|
|
17
|
+
__version__ = VERSION
|
|
18
|
+
__all__ = ["VERSION"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Proof-of-pubkey-ownership authentication for the rendezvous service.
|
|
2
|
+
|
|
3
|
+
Stateless, single-round-trip: the client signs a domain-separated message:
|
|
4
|
+
|
|
5
|
+
``b"hivemind-rendezvous-v1\\x00" + pubkey_bytes + b"\\x00"
|
|
6
|
+
+ server_pubkey_bytes + b"\\x00" + timestamp_bytes``
|
|
7
|
+
|
|
8
|
+
The server verifies the signature and rejects timestamps outside a ±60 second
|
|
9
|
+
window to prevent replay attacks. Binding the server's own pubkey into the
|
|
10
|
+
signed message prevents cross-server replay: a valid proof issued to one
|
|
11
|
+
rendezvous node cannot be replayed at another.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import base64
|
|
15
|
+
import time
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
from poorman_handshake.asymmetric.utils import sign_RSA, verify_RSA
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_TIMESTAMP_TOLERANCE_SECONDS: int = 60
|
|
22
|
+
_DOMAIN: bytes = b"hivemind-rendezvous-v1\x00"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _ownership_message(pubkey: str, timestamp: int, server_pubkey: str = "") -> bytes:
|
|
26
|
+
"""Return the canonical byte string that is signed/verified for ownership proof.
|
|
27
|
+
|
|
28
|
+
The message is domain-separated and binds the server's public key to prevent
|
|
29
|
+
cross-server replay attacks.
|
|
30
|
+
|
|
31
|
+
Format (bytes, null-delimited fields):
|
|
32
|
+
``DOMAIN || claimer_pubkey || 0x00 || server_pubkey || 0x00 || timestamp_decimal``
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
pubkey: PEM-encoded RSA public key of the claiming node.
|
|
36
|
+
timestamp: Unix timestamp (integer seconds).
|
|
37
|
+
server_pubkey: PEM-encoded RSA public key of the rendezvous server.
|
|
38
|
+
Empty string when server identity binding is not used (legacy / testing).
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The message bytes to sign or verify.
|
|
42
|
+
"""
|
|
43
|
+
return (
|
|
44
|
+
_DOMAIN
|
|
45
|
+
+ pubkey.encode("utf-8") + b"\x00"
|
|
46
|
+
+ server_pubkey.encode("utf-8") + b"\x00"
|
|
47
|
+
+ str(timestamp).encode("utf-8")
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def sign_ownership(private_key: Union[str, bytes], pubkey: str, timestamp: int,
|
|
52
|
+
server_pubkey: str = "") -> str:
|
|
53
|
+
"""Produce a base64-encoded ownership proof signature.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
private_key: RSA private key (PEM string, bytes, or RsaKey) of the claimer.
|
|
57
|
+
pubkey: PEM-encoded RSA public key of the claimer.
|
|
58
|
+
timestamp: Unix timestamp (integer seconds) to embed in the proof.
|
|
59
|
+
server_pubkey: PEM-encoded RSA public key of the rendezvous server.
|
|
60
|
+
Binds the proof to a specific server — must match the value used
|
|
61
|
+
by the server in :func:`verify_ownership`.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Base64-encoded signature string suitable for JSON transport.
|
|
65
|
+
"""
|
|
66
|
+
message = _ownership_message(pubkey, timestamp, server_pubkey)
|
|
67
|
+
signature_bytes = sign_RSA(private_key, message)
|
|
68
|
+
return base64.b64encode(signature_bytes).decode("utf-8")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def verify_ownership(pubkey: str, timestamp: int, signature: str,
|
|
72
|
+
server_pubkey: str = "") -> bool:
|
|
73
|
+
"""Verify a proof-of-pubkey-ownership claim.
|
|
74
|
+
|
|
75
|
+
Checks both signature validity and timestamp freshness. A valid proof
|
|
76
|
+
requires:
|
|
77
|
+
|
|
78
|
+
1. The signature over the domain-separated message verifies against
|
|
79
|
+
``pubkey``.
|
|
80
|
+
2. ``abs(now - timestamp) <= 60`` seconds (replay protection).
|
|
81
|
+
3. ``server_pubkey`` in the signed message matches the value passed here
|
|
82
|
+
(cross-server replay protection).
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
pubkey: PEM-encoded RSA public key of the claiming node.
|
|
86
|
+
timestamp: Unix timestamp (integer seconds) embedded in the proof.
|
|
87
|
+
signature: Base64-encoded signature produced by :func:`sign_ownership`.
|
|
88
|
+
server_pubkey: PEM-encoded RSA public key of the rendezvous server
|
|
89
|
+
that was used when signing. Must be supplied by the server itself.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
``True`` if the proof is valid and fresh; ``False`` otherwise.
|
|
93
|
+
"""
|
|
94
|
+
now = int(time.time())
|
|
95
|
+
if abs(now - timestamp) > _TIMESTAMP_TOLERANCE_SECONDS:
|
|
96
|
+
return False
|
|
97
|
+
try:
|
|
98
|
+
signature_bytes = base64.b64decode(signature)
|
|
99
|
+
except Exception:
|
|
100
|
+
return False
|
|
101
|
+
message = _ownership_message(pubkey, timestamp, server_pubkey)
|
|
102
|
+
return verify_RSA(pubkey, message, signature_bytes)
|