Host our public key, and add proper ways to sign JSON + send requests.

This commit is contained in:
2025-09-10 20:05:38 -04:00
parent 4f17450992
commit 9fd2f84741
4 changed files with 142 additions and 47 deletions

View File

@@ -19,7 +19,7 @@ room_dir_room = {
"world_readable": False
}
],
"total_room_count_estimate": 43502
"total_room_count_estimate": 1
}
# Where users should reach out for support.
@@ -49,9 +49,7 @@ users_can_register = False
# The funny number.
the_funny_number = 1337
# Your private key for Vona.
# Your private key for Vona, in the format of:
# ed25519 <key id> <base64 encoded key>
#
# To generate a private key:
# openssl genpkey -algorithm Ed25519 -out privkey.pem
#
ed25519_key_path = "/etc/vonakey.pem"
signing_key = ""

View File

@@ -1,50 +1,107 @@
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import nacl.signing
import hashlib
import base64
import config
import json
import re
import os
vona_version = "1.2.4"
def canonical_json(value):
return json.dumps(
value,
ensure_ascii=False,
separators=(',',':'),
sort_keys=True
separators=(",", ":"),
sort_keys=True,
).encode("UTF-8")
'''
def encode_base64(data: bytes) -> str:
return base64.b64encode(data).decode('utf-8')
def sign_json(json_object, signing_key, signing_name):
signatures = json_object.pop("signatures", {})
unsigned = json_object.pop("unsigned", None)
def sign_json(data):
parts = config.signing_key.split()
base64_key = parts[2]
signed = signing_key.sign(canonical_json(json_object))
signature_base64 = encode_base64(signed)
while len(base64_key) % 4 != 0:
base64_key += "="
key_id = "ed25519:VonaA"
signatures.setdefault(signing_name, {})[key_id] = signature_base64
decoded_key = base64.b64decode(base64_key)
signing_key = nacl.signing.SigningKey(decoded_key)
json_object["signatures"] = signatures
if unsigned is not None:
json_object["unsigned"] = unsigned
signed_message = signing_key.sign(canonical_json(data))
return json_object
'''
signature = signed_message.signature
key_version = parts[1]
signature_base64 = base64.b64encode(signature).decode("utf-8").rstrip("=")
signed_json = {
**data,
"signatures": {
config.server_name: {
f"ed25519:{key_version}": signature_base64,
},
},
}
return signed_json
vona_version = '1.2.3'
def make_event_id():
return re.sub(r'[\/+=]', '_', base64.b64encode(os.urandom(32)).decode('utf-8'))[:44]
return re.sub(r"[\/+=]", "_", base64.b64encode(os.urandom(32)).decode("utf-8"))[:44]
def hash_event(input) -> str:
input.pop('signatures', None)
input.pop('unsigned', None)
input.pop("signatures", None)
input.pop("unsigned", None)
sha256_hash = hashlib.sha256(canonical_json(input)).digest()
base64_encoded = base64.b64encode(sha256_hash)
return base64_encoded.decode().rstrip('=')
return base64_encoded.decode().rstrip("=")
def pubkey() -> str:
private_key = config.signing_key.split()[2]
while len(private_key) % 4 != 0:
private_key += "="
public_key = nacl.signing.SigningKey(base64.b64decode(private_key)).verify_key
return (
public_key.encode(encoder=nacl.encoding.Base64Encoder)
.decode("utf-8")
.rstrip("=")
)
def make_auth_header(destination, method, path, content=None) -> str:
request_json = {
"method": method,
"uri": path,
"origin": config.server_name,
"destination": destination,
}
if content is not None:
request_json["content"] = content
signed_json = sign_json(request_json)
authorization_headers = []
for key, sig in signed_json["signatures"][origin_name].items():
authorization_headers.append(
bytes(
'X-Matrix origin="%s",destination="%s",key="%s",sig="%s"'
% (
config.server_name,
destination,
key,
sig,
)
)
)
return ("Authorization", authorization_headers[0])

View File

@@ -26,7 +26,7 @@ async def root():
@app.route("/_matrix/static/")
async def matrix_static():
return f'<!DOCTYPE html><html lang="en"><head><title>Vona {globals.vona_version} is running</title><style>body {{font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;max-width: 40em;margin: auto;text-align: center;}}h1,p {{margin: 1.5em;}}hr {{border: none;background-color: #ccc;color: #ccc;height: 1px;width: 7em;margin-top: 4em;}}.logo {{display: block;width: 12em;height: auto;margin: 4em auto;}}</style></head><body><img src="https://matrix.org/images/matrix-logo.svg" class="logo"><h1>It works! Vona {globals.vona_version} is running</h1><p>Your Vona server is listening on this port and is ready for messages.</p><p>To use this server you\'ll need <a href="https://matrix.org/ecosystem/clients/" target="_blank"rel="noopener noreferrer">a Matrix client</a>.</p><p>Welcome to the Matrix universe :)</p><hr><p><small><a href="https://matrix.org" target="_blank" rel="noopener noreferrer">matrix.org</a></small></p></body></html>'
return f'<!DOCTYPE html><html lang="en"><head><title>Vona {globals.vona_version} is running</title><style>body {{font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;max-width: 40em;margin: auto;text-align: center;}}h1,p {{margin: 1.5em;}}hr {{border: none;background-color: #ccc;color: #ccc;height: 1px;width: 7em;margin-top: 4em;}}.logo {{display: block;width: 12em;height: auto;margin: 4em auto;}}</style></head><body><img src="https://matrix.org/images/matrix-logo.svg" class="logo"><h1>It works! Vona {globals.vona_version} is running</h1><p>Your Vona server is listening on this port and is ready for messages.</p><p>To use this server you\'ll need <a href="https://matrix.org/ecosystem/clients/" target="_blank"rel="noopener noreferrer">a Matrix client</a>.</p><p>Welcome to the Matrix universe :)</p><hr><p><small><a href="https://natribu.org/en/" target="_blank" rel="noopener noreferrer">matrix.org</a></small></p></body></html>'
# Error handlers
@@ -44,7 +44,7 @@ async def internal_error(error):
# Well-known endpoints for federation,
# clients, and support information
# clients, and support information.
@app.route("/.well-known/matrix/server")
async def server():
return jsonify({"m.server": f"{config.server_name}:443"})

View File

@@ -1,21 +1,20 @@
from flask import Flask, jsonify, Response, request, send_file, abort, Blueprint
from globals import vona_version, make_event_id, hash_event
from config import *
import requests
import globals
import json
import time
import os
server = Blueprint('matrix_server', __name__)
def send_join(request, roomId):
# We may have to include signatures here
# as well, which will be a pain
# We may have to include signatures here.
eventIds = [
f"${make_event_id()}:{server_name}",
f"${make_event_id()}:{server_name}",
f"${make_event_id()}:{server_name}",
f"${make_event_id()}:{server_name}"
f"${globals.make_event_id()}:{server_name}",
f"${globals.make_event_id()}:{server_name}",
f"${globals.make_event_id()}:{server_name}",
f"${globals.make_event_id()}:{server_name}"
]
events = {
@@ -67,7 +66,7 @@ def send_join(request, roomId):
}
for event in events['state']:
event_hash = hash_event(event)
event_hash = globals.hash_event(event)
if event['type'] != "m.room.create":
event['prev_events'] = [eventIds[event['origin_server_ts'] - 1], {"sha256": event_hash}]
@@ -75,6 +74,7 @@ def send_join(request, roomId):
join_event['prev_events'] = [eventIds[3], {"sha256": events['state'][3]['prev_events'][1]['sha256']}]
events['event'] = join_event
# debug
print(json.dumps(events, indent='\t'))
print(json.dumps(eventIds, indent='\t'))
@@ -82,12 +82,20 @@ def send_join(request, roomId):
@server.route('/_matrix/federation/v1/version')
def version():
return jsonify({"server": {"version": vona_version,"name": "Vona"}})
return jsonify({"server": {"version": globals.vona_version,"name": "Vona"}})
@server.route('/_matrix/key/v2/server')
def keys():
# todo
return jsonify({})
return jsonify(globals.sign_json({
"old_verify_keys": {},
"server_name": server_name,
"valid_until_ts": int(time.time() * 1000 + 604800000),
"verify_keys": {
f"ed25519:{signing_key.split()[1]}": {
"key": globals.pubkey()
}
}
}))
@server.route('/_matrix/federation/v1/query/directory')
def room_query():
@@ -116,6 +124,7 @@ def download_media(media_id):
response = Response(response_body, content_type=f'multipart/mixed; boundary={boundary}')
response.status_code = 200
return response
@server.route('/_matrix/federation/v1/media/thumbnail/<media_id>')
@@ -182,6 +191,7 @@ def user_profile():
return jsonify({"displayname":"Vona"})
else:
return jsonify({"errcode": "M_NOT_FOUND","error": "The requested profile key does not exist."}), 404
return jsonify({"avatar_url": f"mxc://{server_name}/cat","displayname": "Vona"})
@server.route('/_matrix/federation/v1/user/devices/<user>')
@@ -196,9 +206,39 @@ def user_devices(user):
def user_keys():
return jsonify({"device_keys":{f"@vona:{server_name}":{}}})
# https://spec.matrix.org/v1.15/server-server-api/#inviting-to-a-room
@server.route('/_matrix/federation/v2/invite/<room>/<txnId>', methods=['PUT'])
def fed_invite_user(room, txnId):
return jsonify({"errcode":"M_INCOMPATIBLE_ROOM_VERSION","error": "Don't touch my users mofo","room_version": str(the_funny_number)}), 400
def invite_user_v2(room, txnId):
# This endpoint is not allowed for room
# versions over v2 as per spec, so any
# invites received here can be discarded
# safely.
return jsonify({
"errcode": "M_INCOMPATIBLE_ROOM_VERSION",
"error": "Vona only supports room version 2.",
"room_version": str(the_funny_number)
}), 400
@server.route('/_matrix/federation/v1/invite/<room>/<txnId>', methods=['PUT'])
def invite_user(room, txnId):
# TODO: Sign provided JSON and return it.
data = request.data.decode('utf-8')
invite_data = json.loads(data)
if "room_version" in invite_data:
if invite_data["room_version"] != "2":
return jsonify({
"errcode": "M_INCOMPATIBLE_ROOM_VERSION",
"error": "Vona only supports room version 2.",
"room_version": invite_data["room_version"]
})
# Placeholder
abort(500)
@server.route('/_matrix/federation/v1/hierarchy/<roomId>')
def space_hierachy(roomId):