mesh-peer-registry 0.1.0__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.
- mesh_peer_registry-0.1.0/LICENSE +21 -0
- mesh_peer_registry-0.1.0/PKG-INFO +121 -0
- mesh_peer_registry-0.1.0/README.md +90 -0
- mesh_peer_registry-0.1.0/mesh_peer_registry/__init__.py +26 -0
- mesh_peer_registry-0.1.0/mesh_peer_registry/cli.py +26 -0
- mesh_peer_registry-0.1.0/mesh_peer_registry/client.py +106 -0
- mesh_peer_registry-0.1.0/mesh_peer_registry/crypto.py +87 -0
- mesh_peer_registry-0.1.0/mesh_peer_registry/models.py +15 -0
- mesh_peer_registry-0.1.0/mesh_peer_registry/server.py +97 -0
- mesh_peer_registry-0.1.0/mesh_peer_registry/store.py +56 -0
- mesh_peer_registry-0.1.0/mesh_peer_registry.egg-info/PKG-INFO +121 -0
- mesh_peer_registry-0.1.0/mesh_peer_registry.egg-info/SOURCES.txt +17 -0
- mesh_peer_registry-0.1.0/mesh_peer_registry.egg-info/dependency_links.txt +1 -0
- mesh_peer_registry-0.1.0/mesh_peer_registry.egg-info/entry_points.txt +2 -0
- mesh_peer_registry-0.1.0/mesh_peer_registry.egg-info/requires.txt +6 -0
- mesh_peer_registry-0.1.0/mesh_peer_registry.egg-info/top_level.txt +1 -0
- mesh_peer_registry-0.1.0/pyproject.toml +48 -0
- mesh_peer_registry-0.1.0/setup.cfg +4 -0
- mesh_peer_registry-0.1.0/tests/test_registry.py +159 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Emil Tsoi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mesh-peer-registry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared, language-agnostic peer registry for hermes-mesh and openclaw-mesh with Ed25519 signing.
|
|
5
|
+
Author: Emil Tsoi
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/emiltsoi/mesh-peer-registry
|
|
8
|
+
Project-URL: Repository, https://github.com/emiltsoi/mesh-peer-registry.git
|
|
9
|
+
Project-URL: Issues, https://github.com/emiltsoi/mesh-peer-registry/issues
|
|
10
|
+
Keywords: mesh,peer,registry,ed25519,aiohttp,openclaw,hermes
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Communications
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: aiohttp>=3.9
|
|
26
|
+
Requires-Dist: cryptography>=41.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# mesh-peer-registry
|
|
33
|
+
|
|
34
|
+
A small, shared, language-agnostic peer registry for the mesh network used by [`hermes-mesh`](https://github.com/emiltsoi/hermes-mesh) and [`openclaw-mesh`](https://github.com/emiltsoi/openclaw-mesh).
|
|
35
|
+
|
|
36
|
+
Peers register with an Ed25519 public key and a webhook URL, then discover each other over a simple HTTP API. The server never holds private keys.
|
|
37
|
+
|
|
38
|
+
## About
|
|
39
|
+
|
|
40
|
+
`mesh-peer-registry` provides a centralized but state-light way for mesh agents to announce themselves to one another:
|
|
41
|
+
|
|
42
|
+
- Peers register an Ed25519 **public key** and a **webhook URL** under a unique name.
|
|
43
|
+
- Registration and deregistration requests are signed by the peer's private key, so the registry can trust the public key it stores.
|
|
44
|
+
- Other peers query the registry to find a recipient's webhook URL and public key.
|
|
45
|
+
- Mesh messages are signed by the sender and verified by the receiver against the sender's public key from the registry.
|
|
46
|
+
|
|
47
|
+
The signing protocol uses compact, deterministic, sorted-key JSON, making it straightforward to implement in other runtimes (e.g. the Node.js implementation in `openclaw-mesh`).
|
|
48
|
+
|
|
49
|
+
## Features
|
|
50
|
+
|
|
51
|
+
- **HTTP API** for register, list, get, and deregister operations.
|
|
52
|
+
- **Ed25519 signatures** on all mutating requests.
|
|
53
|
+
- **In-memory** store by default, or **file-backed** persistence via `--store`.
|
|
54
|
+
- **CLI** server launcher.
|
|
55
|
+
- **Python client** (`RegistryClient`) with built-in signing.
|
|
56
|
+
- No private keys kept by the registry.
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
Install and run the server:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install mesh-peer-registry
|
|
64
|
+
mesh-peer-registry --port 8646 --store ./registry.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The server will listen on `http://127.0.0.1:8646`.
|
|
68
|
+
|
|
69
|
+
## API
|
|
70
|
+
|
|
71
|
+
- `POST /register` — register or update a peer (signed with the peer's private key).
|
|
72
|
+
- `GET /peers` — list all peers.
|
|
73
|
+
- `GET /peers/{name}` — get one peer.
|
|
74
|
+
- `DELETE /peers/{name}` — deregister a peer (signed with the peer's private key).
|
|
75
|
+
|
|
76
|
+
All registration/deregistration requests must include a valid Ed25519 signature in the `X-Mesh-Signature` header.
|
|
77
|
+
|
|
78
|
+
## Python client
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from mesh_peer_registry.crypto import generate_keypair
|
|
82
|
+
from mesh_peer_registry.client import RegistryClient
|
|
83
|
+
|
|
84
|
+
private, public = generate_keypair()
|
|
85
|
+
client = RegistryClient("http://127.0.0.1:8646", private, public)
|
|
86
|
+
|
|
87
|
+
client.register(
|
|
88
|
+
"agent0",
|
|
89
|
+
"http://127.0.0.1:8645/mesh/receive",
|
|
90
|
+
role="operator",
|
|
91
|
+
description="Hermes operator node",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
print(client.list_peers())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Verifying a message
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from mesh_peer_registry.crypto import verify_message
|
|
101
|
+
from mesh_peer_registry.client import RegistryClient
|
|
102
|
+
|
|
103
|
+
client = RegistryClient("http://127.0.0.1:8646", "", "")
|
|
104
|
+
peer = client.get_peer("agent0")
|
|
105
|
+
|
|
106
|
+
# body is the raw request body; signature is from the X-Mesh-Signature header.
|
|
107
|
+
ok = verify_message(peer.public_key, body, signature)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
python -m venv .venv
|
|
114
|
+
source .venv/bin/activate
|
|
115
|
+
pip install -e ".[dev]"
|
|
116
|
+
pytest
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# mesh-peer-registry
|
|
2
|
+
|
|
3
|
+
A small, shared, language-agnostic peer registry for the mesh network used by [`hermes-mesh`](https://github.com/emiltsoi/hermes-mesh) and [`openclaw-mesh`](https://github.com/emiltsoi/openclaw-mesh).
|
|
4
|
+
|
|
5
|
+
Peers register with an Ed25519 public key and a webhook URL, then discover each other over a simple HTTP API. The server never holds private keys.
|
|
6
|
+
|
|
7
|
+
## About
|
|
8
|
+
|
|
9
|
+
`mesh-peer-registry` provides a centralized but state-light way for mesh agents to announce themselves to one another:
|
|
10
|
+
|
|
11
|
+
- Peers register an Ed25519 **public key** and a **webhook URL** under a unique name.
|
|
12
|
+
- Registration and deregistration requests are signed by the peer's private key, so the registry can trust the public key it stores.
|
|
13
|
+
- Other peers query the registry to find a recipient's webhook URL and public key.
|
|
14
|
+
- Mesh messages are signed by the sender and verified by the receiver against the sender's public key from the registry.
|
|
15
|
+
|
|
16
|
+
The signing protocol uses compact, deterministic, sorted-key JSON, making it straightforward to implement in other runtimes (e.g. the Node.js implementation in `openclaw-mesh`).
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **HTTP API** for register, list, get, and deregister operations.
|
|
21
|
+
- **Ed25519 signatures** on all mutating requests.
|
|
22
|
+
- **In-memory** store by default, or **file-backed** persistence via `--store`.
|
|
23
|
+
- **CLI** server launcher.
|
|
24
|
+
- **Python client** (`RegistryClient`) with built-in signing.
|
|
25
|
+
- No private keys kept by the registry.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
Install and run the server:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install mesh-peer-registry
|
|
33
|
+
mesh-peer-registry --port 8646 --store ./registry.json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The server will listen on `http://127.0.0.1:8646`.
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
- `POST /register` — register or update a peer (signed with the peer's private key).
|
|
41
|
+
- `GET /peers` — list all peers.
|
|
42
|
+
- `GET /peers/{name}` — get one peer.
|
|
43
|
+
- `DELETE /peers/{name}` — deregister a peer (signed with the peer's private key).
|
|
44
|
+
|
|
45
|
+
All registration/deregistration requests must include a valid Ed25519 signature in the `X-Mesh-Signature` header.
|
|
46
|
+
|
|
47
|
+
## Python client
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from mesh_peer_registry.crypto import generate_keypair
|
|
51
|
+
from mesh_peer_registry.client import RegistryClient
|
|
52
|
+
|
|
53
|
+
private, public = generate_keypair()
|
|
54
|
+
client = RegistryClient("http://127.0.0.1:8646", private, public)
|
|
55
|
+
|
|
56
|
+
client.register(
|
|
57
|
+
"agent0",
|
|
58
|
+
"http://127.0.0.1:8645/mesh/receive",
|
|
59
|
+
role="operator",
|
|
60
|
+
description="Hermes operator node",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
print(client.list_peers())
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Verifying a message
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from mesh_peer_registry.crypto import verify_message
|
|
70
|
+
from mesh_peer_registry.client import RegistryClient
|
|
71
|
+
|
|
72
|
+
client = RegistryClient("http://127.0.0.1:8646", "", "")
|
|
73
|
+
peer = client.get_peer("agent0")
|
|
74
|
+
|
|
75
|
+
# body is the raw request body; signature is from the X-Mesh-Signature header.
|
|
76
|
+
ok = verify_message(peer.public_key, body, signature)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
python -m venv .venv
|
|
83
|
+
source .venv/bin/activate
|
|
84
|
+
pip install -e ".[dev]"
|
|
85
|
+
pytest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Shared peer registry package for hermes-mesh and openclaw-mesh."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .crypto import (
|
|
5
|
+
canonicalize_payload,
|
|
6
|
+
generate_keypair,
|
|
7
|
+
sign_json,
|
|
8
|
+
sign_message,
|
|
9
|
+
verify_json,
|
|
10
|
+
verify_message,
|
|
11
|
+
)
|
|
12
|
+
from .client import RegistryClient
|
|
13
|
+
from .models import PeerInfo
|
|
14
|
+
from .server import create_app
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"canonicalize_payload",
|
|
18
|
+
"generate_keypair",
|
|
19
|
+
"sign_json",
|
|
20
|
+
"sign_message",
|
|
21
|
+
"verify_json",
|
|
22
|
+
"verify_message",
|
|
23
|
+
"RegistryClient",
|
|
24
|
+
"PeerInfo",
|
|
25
|
+
"create_app",
|
|
26
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""CLI entry point for the mesh peer registry server."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from aiohttp import web
|
|
8
|
+
|
|
9
|
+
from .server import create_app
|
|
10
|
+
|
|
11
|
+
__all__ = ["main"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
parser = argparse.ArgumentParser(description="Run the mesh peer registry")
|
|
16
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
17
|
+
parser.add_argument("--port", type=int, default=8646)
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"--store",
|
|
20
|
+
default="~/.mesh/registry.json",
|
|
21
|
+
help="Path to the JSON store file",
|
|
22
|
+
)
|
|
23
|
+
args = parser.parse_args()
|
|
24
|
+
|
|
25
|
+
app = create_app(os.path.expanduser(args.store))
|
|
26
|
+
web.run_app(app, host=args.host, port=args.port)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Synchronous HTTP client for the mesh peer registry."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.request
|
|
7
|
+
|
|
8
|
+
from .crypto import sign_json
|
|
9
|
+
from .models import PeerInfo
|
|
10
|
+
|
|
11
|
+
__all__ = ["RegistryClient"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RegistryClient:
|
|
15
|
+
"""Synchronous client for a mesh-peer-registry server."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
registry_url: str,
|
|
20
|
+
private_key_pem: str,
|
|
21
|
+
public_key_pem: str,
|
|
22
|
+
timeout: float = 10.0,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.registry_url = registry_url.rstrip("/")
|
|
25
|
+
self.private_key_pem = private_key_pem
|
|
26
|
+
self.public_key_pem = public_key_pem
|
|
27
|
+
self.timeout = timeout
|
|
28
|
+
|
|
29
|
+
def _request(
|
|
30
|
+
self,
|
|
31
|
+
method: str,
|
|
32
|
+
path: str,
|
|
33
|
+
payload: dict | None = None,
|
|
34
|
+
signature_payload: dict | None = None,
|
|
35
|
+
) -> dict:
|
|
36
|
+
url = f"{self.registry_url}{path}"
|
|
37
|
+
headers: dict[str, str] = {}
|
|
38
|
+
data: bytes | None = None
|
|
39
|
+
|
|
40
|
+
if payload is not None:
|
|
41
|
+
data = json.dumps(payload, sort_keys=True).encode("utf-8")
|
|
42
|
+
headers["Content-Type"] = "application/json"
|
|
43
|
+
|
|
44
|
+
if signature_payload is not None:
|
|
45
|
+
sig = sign_json(self.private_key_pem, signature_payload)
|
|
46
|
+
headers["X-Mesh-Signature"] = sig
|
|
47
|
+
|
|
48
|
+
req = urllib.request.Request(
|
|
49
|
+
url, data=data, headers=headers, method=method
|
|
50
|
+
)
|
|
51
|
+
try:
|
|
52
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
53
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
54
|
+
except urllib.error.HTTPError as exc:
|
|
55
|
+
body = exc.read().decode("utf-8")
|
|
56
|
+
try:
|
|
57
|
+
detail = json.loads(body)
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
detail = body
|
|
60
|
+
raise RegistryClientError(
|
|
61
|
+
status=exc.code, message=detail
|
|
62
|
+
) from exc
|
|
63
|
+
|
|
64
|
+
def register(
|
|
65
|
+
self,
|
|
66
|
+
name: str,
|
|
67
|
+
url: str,
|
|
68
|
+
role: str = "agent",
|
|
69
|
+
description: str = "",
|
|
70
|
+
) -> dict:
|
|
71
|
+
payload = {
|
|
72
|
+
"name": name,
|
|
73
|
+
"url": url,
|
|
74
|
+
"public_key": self.public_key_pem,
|
|
75
|
+
"role": role,
|
|
76
|
+
"description": description,
|
|
77
|
+
}
|
|
78
|
+
return self._request("POST", "/register", payload, payload)
|
|
79
|
+
|
|
80
|
+
def list_peers(self) -> list[PeerInfo]:
|
|
81
|
+
data = self._request("GET", "/peers")
|
|
82
|
+
return [PeerInfo(**p) for p in data.get("peers", [])]
|
|
83
|
+
|
|
84
|
+
def get_peer(self, name: str) -> PeerInfo | None:
|
|
85
|
+
try:
|
|
86
|
+
data = self._request("GET", f"/peers/{name}")
|
|
87
|
+
return PeerInfo(**data)
|
|
88
|
+
except RegistryClientError as exc:
|
|
89
|
+
if exc.status == 404:
|
|
90
|
+
return None
|
|
91
|
+
raise
|
|
92
|
+
|
|
93
|
+
def deregister(self, name: str) -> dict:
|
|
94
|
+
sig_payload = {"name": name, "action": "deregister"}
|
|
95
|
+
return self._request(
|
|
96
|
+
"DELETE", f"/peers/{name}", signature_payload=sig_payload
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class RegistryClientError(Exception):
|
|
101
|
+
"""Registry client error with HTTP status and server message."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, status: int, message: object):
|
|
104
|
+
self.status = status
|
|
105
|
+
self.message = message
|
|
106
|
+
super().__init__(f"Registry HTTP {status}: {message}")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Ed25519 signature primitives for the mesh peer registry."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
from cryptography.exceptions import InvalidSignature
|
|
9
|
+
from cryptography.hazmat.primitives import serialization
|
|
10
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
11
|
+
Ed25519PrivateKey,
|
|
12
|
+
Ed25519PublicKey,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"generate_keypair",
|
|
17
|
+
"sign_message",
|
|
18
|
+
"verify_message",
|
|
19
|
+
"canonicalize_payload",
|
|
20
|
+
"sign_json",
|
|
21
|
+
"verify_json",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def generate_keypair() -> tuple[str, str]:
|
|
26
|
+
"""Generate a new Ed25519 keypair and return (private_key_pem, public_key_pem)."""
|
|
27
|
+
private_key = Ed25519PrivateKey.generate()
|
|
28
|
+
public_key = private_key.public_key()
|
|
29
|
+
private_pem = private_key.private_bytes(
|
|
30
|
+
encoding=serialization.Encoding.PEM,
|
|
31
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
32
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
33
|
+
).decode("utf-8")
|
|
34
|
+
public_pem = public_key.public_bytes(
|
|
35
|
+
encoding=serialization.Encoding.PEM,
|
|
36
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
37
|
+
).decode("utf-8")
|
|
38
|
+
return private_pem, public_pem
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _load_private_key(private_key_pem: str) -> Ed25519PrivateKey:
|
|
42
|
+
return serialization.load_pem_private_key(
|
|
43
|
+
private_key_pem.encode("utf-8"), password=None
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _load_public_key(public_key_pem: str) -> Ed25519PublicKey:
|
|
48
|
+
return serialization.load_pem_public_key(public_key_pem.encode("utf-8"))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def sign_message(private_key_pem: str, message: Union[str, bytes]) -> str:
|
|
52
|
+
"""Sign a message and return a base64-encoded signature."""
|
|
53
|
+
if isinstance(message, str):
|
|
54
|
+
message = message.encode("utf-8")
|
|
55
|
+
private_key = _load_private_key(private_key_pem)
|
|
56
|
+
signature = private_key.sign(message)
|
|
57
|
+
return base64.b64encode(signature).decode("utf-8")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def verify_message(
|
|
61
|
+
public_key_pem: str, message: Union[str, bytes], signature_b64: str
|
|
62
|
+
) -> bool:
|
|
63
|
+
"""Verify a base64-encoded signature against a message and public key."""
|
|
64
|
+
if isinstance(message, str):
|
|
65
|
+
message = message.encode("utf-8")
|
|
66
|
+
public_key = _load_public_key(public_key_pem)
|
|
67
|
+
signature = base64.b64decode(signature_b64.encode("utf-8"))
|
|
68
|
+
try:
|
|
69
|
+
public_key.verify(signature, message)
|
|
70
|
+
return True
|
|
71
|
+
except InvalidSignature:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def canonicalize_payload(payload: dict) -> bytes:
|
|
76
|
+
"""Return a deterministic JSON encoding of a payload for signing."""
|
|
77
|
+
return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def sign_json(private_key_pem: str, payload: dict) -> str:
|
|
81
|
+
"""Sign a JSON-serializable payload and return a base64-encoded signature."""
|
|
82
|
+
return sign_message(private_key_pem, canonicalize_payload(payload))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def verify_json(public_key_pem: str, payload: dict, signature_b64: str) -> bool:
|
|
86
|
+
"""Verify a signature over a JSON-serializable payload."""
|
|
87
|
+
return verify_message(public_key_pem, canonicalize_payload(payload), signature_b64)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Data models for the mesh peer registry."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class PeerInfo:
|
|
9
|
+
"""Public peer record held by the registry."""
|
|
10
|
+
|
|
11
|
+
name: str
|
|
12
|
+
url: str
|
|
13
|
+
public_key: str
|
|
14
|
+
role: str = "agent"
|
|
15
|
+
description: str = ""
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""HTTP registry server for mesh peers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from aiohttp import web
|
|
9
|
+
|
|
10
|
+
from .crypto import verify_json
|
|
11
|
+
from .models import PeerInfo
|
|
12
|
+
from .store import FileStore
|
|
13
|
+
|
|
14
|
+
__all__ = ["create_app"]
|
|
15
|
+
|
|
16
|
+
AppKeyStore = web.AppKey("store", FileStore)
|
|
17
|
+
|
|
18
|
+
REGISTRY_FIELDS = {"name", "url", "public_key", "role", "description"}
|
|
19
|
+
REQUIRED_FIELDS = {"name", "url", "public_key"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def _json_body(request: web.Request) -> dict:
|
|
23
|
+
try:
|
|
24
|
+
return await request.json()
|
|
25
|
+
except json.JSONDecodeError:
|
|
26
|
+
raise web.HTTPBadRequest(text='{"error": "invalid json"}')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def register(request: web.Request) -> web.Response:
|
|
30
|
+
body = await _json_body(request)
|
|
31
|
+
missing = REQUIRED_FIELDS - set(body)
|
|
32
|
+
if missing:
|
|
33
|
+
return web.json_response(
|
|
34
|
+
{"error": f"missing fields: {sorted(missing)}"}, status=400
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
sig = request.headers.get("X-Mesh-Signature", "")
|
|
38
|
+
if not sig:
|
|
39
|
+
return web.json_response(
|
|
40
|
+
{"error": "missing X-Mesh-Signature header"}, status=400
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
payload = {k: body[k] for k in REGISTRY_FIELDS if k in body}
|
|
44
|
+
public_key = body["public_key"]
|
|
45
|
+
if not verify_json(public_key, payload, sig):
|
|
46
|
+
return web.json_response({"error": "invalid signature"}, status=401)
|
|
47
|
+
|
|
48
|
+
store: FileStore = request.app[AppKeyStore]
|
|
49
|
+
store.put(PeerInfo(**payload))
|
|
50
|
+
return web.json_response({"ok": True, "peer": payload})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def list_peers(request: web.Request) -> web.Response:
|
|
54
|
+
store: FileStore = request.app[AppKeyStore]
|
|
55
|
+
peers = store.list()
|
|
56
|
+
return web.json_response({"peers": [p.__dict__ for p in peers]})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def get_peer(request: web.Request) -> web.Response:
|
|
60
|
+
name = request.match_info["name"]
|
|
61
|
+
store: FileStore = request.app[AppKeyStore]
|
|
62
|
+
peer = store.get(name)
|
|
63
|
+
if not peer:
|
|
64
|
+
return web.json_response({"error": "not found"}, status=404)
|
|
65
|
+
return web.json_response(peer.__dict__)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def delete_peer(request: web.Request) -> web.Response:
|
|
69
|
+
name = request.match_info["name"]
|
|
70
|
+
sig = request.headers.get("X-Mesh-Signature", "")
|
|
71
|
+
if not sig:
|
|
72
|
+
return web.json_response(
|
|
73
|
+
{"error": "missing X-Mesh-Signature header"}, status=400
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
store: FileStore = request.app[AppKeyStore]
|
|
77
|
+
peer = store.get(name)
|
|
78
|
+
if not peer:
|
|
79
|
+
return web.json_response({"error": "not found"}, status=404)
|
|
80
|
+
|
|
81
|
+
sig_payload = {"name": name, "action": "deregister"}
|
|
82
|
+
if not verify_json(peer.public_key, sig_payload, sig):
|
|
83
|
+
return web.json_response({"error": "invalid signature"}, status=401)
|
|
84
|
+
|
|
85
|
+
store.delete(name)
|
|
86
|
+
return web.json_response({"ok": True})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def create_app(store_path: str | None = None) -> web.Application:
|
|
90
|
+
store_path = store_path or os.path.expanduser("~/.mesh/registry.json")
|
|
91
|
+
app = web.Application()
|
|
92
|
+
app[AppKeyStore] = FileStore(Path(store_path))
|
|
93
|
+
app.router.add_post("/register", register)
|
|
94
|
+
app.router.add_get("/peers", list_peers)
|
|
95
|
+
app.router.add_get("/peers/{name}", get_peer)
|
|
96
|
+
app.router.add_delete("/peers/{name}", delete_peer)
|
|
97
|
+
return app
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Persistence layer for the mesh peer registry."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .models import PeerInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FileStore:
|
|
13
|
+
"""Atomic JSON file storage for peer records."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, path: str | Path) -> None:
|
|
16
|
+
self.path = Path(path).expanduser()
|
|
17
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
|
|
19
|
+
def _load(self) -> dict[str, Any]:
|
|
20
|
+
if not self.path.exists():
|
|
21
|
+
return {}
|
|
22
|
+
try:
|
|
23
|
+
return json.loads(self.path.read_text(encoding="utf-8"))
|
|
24
|
+
except (json.JSONDecodeError, OSError):
|
|
25
|
+
return {}
|
|
26
|
+
|
|
27
|
+
def _save(self, data: dict[str, Any]) -> None:
|
|
28
|
+
tmp = self.path.with_suffix(".tmp")
|
|
29
|
+
tmp.write_text(
|
|
30
|
+
json.dumps(data, sort_keys=True, indent=2),
|
|
31
|
+
encoding="utf-8",
|
|
32
|
+
)
|
|
33
|
+
os.replace(tmp, self.path)
|
|
34
|
+
|
|
35
|
+
def get(self, name: str) -> PeerInfo | None:
|
|
36
|
+
data = self._load()
|
|
37
|
+
if name not in data:
|
|
38
|
+
return None
|
|
39
|
+
return PeerInfo(**data[name])
|
|
40
|
+
|
|
41
|
+
def list(self) -> list[PeerInfo]:
|
|
42
|
+
data = self._load()
|
|
43
|
+
return [PeerInfo(**v) for _, v in sorted(data.items())]
|
|
44
|
+
|
|
45
|
+
def put(self, peer: PeerInfo) -> None:
|
|
46
|
+
data = self._load()
|
|
47
|
+
data[peer.name] = peer.__dict__
|
|
48
|
+
self._save(data)
|
|
49
|
+
|
|
50
|
+
def delete(self, name: str) -> bool:
|
|
51
|
+
data = self._load()
|
|
52
|
+
if name not in data:
|
|
53
|
+
return False
|
|
54
|
+
del data[name]
|
|
55
|
+
self._save(data)
|
|
56
|
+
return True
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mesh-peer-registry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared, language-agnostic peer registry for hermes-mesh and openclaw-mesh with Ed25519 signing.
|
|
5
|
+
Author: Emil Tsoi
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/emiltsoi/mesh-peer-registry
|
|
8
|
+
Project-URL: Repository, https://github.com/emiltsoi/mesh-peer-registry.git
|
|
9
|
+
Project-URL: Issues, https://github.com/emiltsoi/mesh-peer-registry/issues
|
|
10
|
+
Keywords: mesh,peer,registry,ed25519,aiohttp,openclaw,hermes
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Communications
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: aiohttp>=3.9
|
|
26
|
+
Requires-Dist: cryptography>=41.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# mesh-peer-registry
|
|
33
|
+
|
|
34
|
+
A small, shared, language-agnostic peer registry for the mesh network used by [`hermes-mesh`](https://github.com/emiltsoi/hermes-mesh) and [`openclaw-mesh`](https://github.com/emiltsoi/openclaw-mesh).
|
|
35
|
+
|
|
36
|
+
Peers register with an Ed25519 public key and a webhook URL, then discover each other over a simple HTTP API. The server never holds private keys.
|
|
37
|
+
|
|
38
|
+
## About
|
|
39
|
+
|
|
40
|
+
`mesh-peer-registry` provides a centralized but state-light way for mesh agents to announce themselves to one another:
|
|
41
|
+
|
|
42
|
+
- Peers register an Ed25519 **public key** and a **webhook URL** under a unique name.
|
|
43
|
+
- Registration and deregistration requests are signed by the peer's private key, so the registry can trust the public key it stores.
|
|
44
|
+
- Other peers query the registry to find a recipient's webhook URL and public key.
|
|
45
|
+
- Mesh messages are signed by the sender and verified by the receiver against the sender's public key from the registry.
|
|
46
|
+
|
|
47
|
+
The signing protocol uses compact, deterministic, sorted-key JSON, making it straightforward to implement in other runtimes (e.g. the Node.js implementation in `openclaw-mesh`).
|
|
48
|
+
|
|
49
|
+
## Features
|
|
50
|
+
|
|
51
|
+
- **HTTP API** for register, list, get, and deregister operations.
|
|
52
|
+
- **Ed25519 signatures** on all mutating requests.
|
|
53
|
+
- **In-memory** store by default, or **file-backed** persistence via `--store`.
|
|
54
|
+
- **CLI** server launcher.
|
|
55
|
+
- **Python client** (`RegistryClient`) with built-in signing.
|
|
56
|
+
- No private keys kept by the registry.
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
Install and run the server:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install mesh-peer-registry
|
|
64
|
+
mesh-peer-registry --port 8646 --store ./registry.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The server will listen on `http://127.0.0.1:8646`.
|
|
68
|
+
|
|
69
|
+
## API
|
|
70
|
+
|
|
71
|
+
- `POST /register` — register or update a peer (signed with the peer's private key).
|
|
72
|
+
- `GET /peers` — list all peers.
|
|
73
|
+
- `GET /peers/{name}` — get one peer.
|
|
74
|
+
- `DELETE /peers/{name}` — deregister a peer (signed with the peer's private key).
|
|
75
|
+
|
|
76
|
+
All registration/deregistration requests must include a valid Ed25519 signature in the `X-Mesh-Signature` header.
|
|
77
|
+
|
|
78
|
+
## Python client
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from mesh_peer_registry.crypto import generate_keypair
|
|
82
|
+
from mesh_peer_registry.client import RegistryClient
|
|
83
|
+
|
|
84
|
+
private, public = generate_keypair()
|
|
85
|
+
client = RegistryClient("http://127.0.0.1:8646", private, public)
|
|
86
|
+
|
|
87
|
+
client.register(
|
|
88
|
+
"agent0",
|
|
89
|
+
"http://127.0.0.1:8645/mesh/receive",
|
|
90
|
+
role="operator",
|
|
91
|
+
description="Hermes operator node",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
print(client.list_peers())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Verifying a message
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from mesh_peer_registry.crypto import verify_message
|
|
101
|
+
from mesh_peer_registry.client import RegistryClient
|
|
102
|
+
|
|
103
|
+
client = RegistryClient("http://127.0.0.1:8646", "", "")
|
|
104
|
+
peer = client.get_peer("agent0")
|
|
105
|
+
|
|
106
|
+
# body is the raw request body; signature is from the X-Mesh-Signature header.
|
|
107
|
+
ok = verify_message(peer.public_key, body, signature)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
python -m venv .venv
|
|
114
|
+
source .venv/bin/activate
|
|
115
|
+
pip install -e ".[dev]"
|
|
116
|
+
pytest
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
mesh_peer_registry/__init__.py
|
|
5
|
+
mesh_peer_registry/cli.py
|
|
6
|
+
mesh_peer_registry/client.py
|
|
7
|
+
mesh_peer_registry/crypto.py
|
|
8
|
+
mesh_peer_registry/models.py
|
|
9
|
+
mesh_peer_registry/server.py
|
|
10
|
+
mesh_peer_registry/store.py
|
|
11
|
+
mesh_peer_registry.egg-info/PKG-INFO
|
|
12
|
+
mesh_peer_registry.egg-info/SOURCES.txt
|
|
13
|
+
mesh_peer_registry.egg-info/dependency_links.txt
|
|
14
|
+
mesh_peer_registry.egg-info/entry_points.txt
|
|
15
|
+
mesh_peer_registry.egg-info/requires.txt
|
|
16
|
+
mesh_peer_registry.egg-info/top_level.txt
|
|
17
|
+
tests/test_registry.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mesh_peer_registry
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mesh-peer-registry"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Shared, language-agnostic peer registry for hermes-mesh and openclaw-mesh with Ed25519 signing."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {text = "MIT"}
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [{name = "Emil Tsoi"}]
|
|
9
|
+
keywords = ["mesh", "peer", "registry", "ed25519", "aiohttp", "openclaw", "hermes"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Topic :: Communications",
|
|
19
|
+
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"aiohttp>=3.9",
|
|
25
|
+
"cryptography>=41.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=8.0",
|
|
31
|
+
"pytest-asyncio>=0.23",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/emiltsoi/mesh-peer-registry"
|
|
36
|
+
Repository = "https://github.com/emiltsoi/mesh-peer-registry.git"
|
|
37
|
+
Issues = "https://github.com/emiltsoi/mesh-peer-registry/issues"
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
mesh-peer-registry = "mesh_peer_registry.cli:main"
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["setuptools>=68"]
|
|
44
|
+
build-backend = "setuptools.build_meta"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools]
|
|
47
|
+
packages = ["mesh_peer_registry"]
|
|
48
|
+
license-files = ["LICENSE"]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Tests for mesh-peer-registry crypto, store, and server."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from aiohttp.test_utils import AioHTTPTestCase
|
|
11
|
+
|
|
12
|
+
from mesh_peer_registry.client import RegistryClient, RegistryClientError
|
|
13
|
+
from mesh_peer_registry.crypto import (
|
|
14
|
+
canonicalize_payload,
|
|
15
|
+
generate_keypair,
|
|
16
|
+
sign_json,
|
|
17
|
+
sign_message,
|
|
18
|
+
verify_json,
|
|
19
|
+
verify_message,
|
|
20
|
+
)
|
|
21
|
+
from mesh_peer_registry.models import PeerInfo
|
|
22
|
+
from mesh_peer_registry.server import create_app
|
|
23
|
+
from mesh_peer_registry.store import FileStore
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestCrypto(unittest.TestCase):
|
|
27
|
+
def test_generate_keypair(self):
|
|
28
|
+
private, public = generate_keypair()
|
|
29
|
+
assert "BEGIN PRIVATE KEY" in private
|
|
30
|
+
assert "BEGIN PUBLIC KEY" in public
|
|
31
|
+
|
|
32
|
+
def test_sign_and_verify(self):
|
|
33
|
+
private, public = generate_keypair()
|
|
34
|
+
sig = sign_message(private, "hello mesh")
|
|
35
|
+
assert verify_message(public, "hello mesh", sig)
|
|
36
|
+
|
|
37
|
+
def test_verify_fails_with_wrong_key(self):
|
|
38
|
+
private, public = generate_keypair()
|
|
39
|
+
_, wrong_public = generate_keypair()
|
|
40
|
+
sig = sign_message(private, "hello")
|
|
41
|
+
assert not verify_message(wrong_public, "hello", sig)
|
|
42
|
+
|
|
43
|
+
def test_verify_fails_with_tampered_message(self):
|
|
44
|
+
private, public = generate_keypair()
|
|
45
|
+
sig = sign_message(private, "hello")
|
|
46
|
+
assert not verify_message(public, "tampered", sig)
|
|
47
|
+
|
|
48
|
+
def test_sign_and_verify_json(self):
|
|
49
|
+
private, public = generate_keypair()
|
|
50
|
+
payload = {"from": "linda", "to": "britney", "id": "msg-123"}
|
|
51
|
+
sig = sign_json(private, payload)
|
|
52
|
+
assert verify_json(public, payload, sig)
|
|
53
|
+
assert not verify_json(public, {"from": "linda"}, sig)
|
|
54
|
+
|
|
55
|
+
def test_canonicalize_is_deterministic(self):
|
|
56
|
+
a = canonicalize_payload({"b": 2, "a": 1})
|
|
57
|
+
b = canonicalize_payload({"a": 1, "b": 2})
|
|
58
|
+
assert a == b
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestStore(unittest.TestCase):
|
|
62
|
+
def test_put_get_list_delete(self):
|
|
63
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
64
|
+
store = FileStore(Path(tmpdir) / "registry.json")
|
|
65
|
+
peer = PeerInfo(
|
|
66
|
+
name="britney",
|
|
67
|
+
url="http://127.0.0.1:8645",
|
|
68
|
+
public_key="pk",
|
|
69
|
+
role="swe",
|
|
70
|
+
)
|
|
71
|
+
store.put(peer)
|
|
72
|
+
fetched = store.get("britney")
|
|
73
|
+
assert fetched is not None
|
|
74
|
+
assert fetched.name == "britney"
|
|
75
|
+
assert store.list()[0].name == "britney"
|
|
76
|
+
assert store.delete("britney")
|
|
77
|
+
assert store.get("britney") is None
|
|
78
|
+
|
|
79
|
+
def test_delete_missing_returns_false(self):
|
|
80
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
81
|
+
store = FileStore(Path(tmpdir) / "registry.json")
|
|
82
|
+
assert not store.delete("nobody")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestRegistryServer(AioHTTPTestCase):
|
|
86
|
+
async def get_application(self):
|
|
87
|
+
self.tmpdir = tempfile.TemporaryDirectory()
|
|
88
|
+
return create_app(str(Path(self.tmpdir.name) / "registry.json"))
|
|
89
|
+
|
|
90
|
+
def tearDown(self):
|
|
91
|
+
super().tearDown()
|
|
92
|
+
if hasattr(self, "tmpdir"):
|
|
93
|
+
self.tmpdir.cleanup()
|
|
94
|
+
|
|
95
|
+
async def test_list_empty(self):
|
|
96
|
+
resp = await self.client.request("GET", "/peers")
|
|
97
|
+
assert resp.status == 200
|
|
98
|
+
body = await resp.json()
|
|
99
|
+
assert body["peers"] == []
|
|
100
|
+
|
|
101
|
+
async def test_register_and_get(self):
|
|
102
|
+
private, public = generate_keypair()
|
|
103
|
+
client = RegistryClient(
|
|
104
|
+
f"http://127.0.0.1:{self.server.port}",
|
|
105
|
+
private,
|
|
106
|
+
public,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
result = await asyncio.to_thread(
|
|
110
|
+
client.register,
|
|
111
|
+
name="britney",
|
|
112
|
+
url="http://127.0.0.1:8645",
|
|
113
|
+
role="swe",
|
|
114
|
+
description="Principal",
|
|
115
|
+
)
|
|
116
|
+
assert result["ok"]
|
|
117
|
+
|
|
118
|
+
peer = await asyncio.to_thread(client.get_peer, "britney")
|
|
119
|
+
assert peer is not None
|
|
120
|
+
assert peer.name == "britney"
|
|
121
|
+
assert peer.role == "swe"
|
|
122
|
+
|
|
123
|
+
async def test_register_without_signature_rejected(self):
|
|
124
|
+
resp = await self.client.request(
|
|
125
|
+
"POST",
|
|
126
|
+
"/register",
|
|
127
|
+
data=json.dumps(
|
|
128
|
+
{
|
|
129
|
+
"name": "britney",
|
|
130
|
+
"url": "http://127.0.0.1:8645",
|
|
131
|
+
"public_key": "pk",
|
|
132
|
+
}
|
|
133
|
+
),
|
|
134
|
+
headers={"Content-Type": "application/json"},
|
|
135
|
+
)
|
|
136
|
+
assert resp.status == 400
|
|
137
|
+
|
|
138
|
+
async def test_deregister(self):
|
|
139
|
+
private, public = generate_keypair()
|
|
140
|
+
client = RegistryClient(
|
|
141
|
+
f"http://127.0.0.1:{self.server.port}",
|
|
142
|
+
private,
|
|
143
|
+
public,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
await asyncio.to_thread(
|
|
147
|
+
client.register,
|
|
148
|
+
name="daji",
|
|
149
|
+
url="http://127.0.0.1:8646",
|
|
150
|
+
role="agent",
|
|
151
|
+
)
|
|
152
|
+
assert (await asyncio.to_thread(client.get_peer, "daji")) is not None
|
|
153
|
+
|
|
154
|
+
await asyncio.to_thread(client.deregister, "daji")
|
|
155
|
+
assert (await asyncio.to_thread(client.get_peer, "daji")) is None
|
|
156
|
+
|
|
157
|
+
async def test_get_missing_returns_404(self):
|
|
158
|
+
resp = await self.client.request("GET", "/peers/nobody")
|
|
159
|
+
assert resp.status == 404
|