feat: Add cross-signing bootstrap + canonicaljson dep
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
148
bootstrap_cross_signing.py
Normal file
148
bootstrap_cross_signing.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
"""One-time script to bootstrap cross-signing keys for the bot."""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import olm.pk
|
||||||
|
import canonicaljson
|
||||||
|
import aiohttp
|
||||||
|
from nio import AsyncClient, AsyncClientConfig
|
||||||
|
|
||||||
|
STORE = os.environ.get("CRYPTO_STORE_PATH", "/data/crypto_store")
|
||||||
|
CREDS_FILE = os.path.join(STORE, "credentials.json")
|
||||||
|
HS = os.environ["MATRIX_HOMESERVER"]
|
||||||
|
BOT_PASS = os.environ["MATRIX_BOT_PASSWORD"]
|
||||||
|
|
||||||
|
|
||||||
|
async def bootstrap():
|
||||||
|
with open(CREDS_FILE) as f:
|
||||||
|
creds = json.load(f)
|
||||||
|
|
||||||
|
config = AsyncClientConfig(encryption_enabled=True, store_sync_tokens=True)
|
||||||
|
client = AsyncClient(HS, creds["user_id"], store_path=STORE, config=config)
|
||||||
|
client.restore_login(creds["user_id"], creds["device_id"], creds["access_token"])
|
||||||
|
client.load_store()
|
||||||
|
await client.sync(timeout=10000, full_state=True)
|
||||||
|
|
||||||
|
user_id = creds["user_id"]
|
||||||
|
device_id = creds["device_id"]
|
||||||
|
token = creds["access_token"]
|
||||||
|
|
||||||
|
# Generate cross-signing key pairs (PkSigning needs a 32-byte random seed)
|
||||||
|
import base64
|
||||||
|
master_seed = os.urandom(32)
|
||||||
|
ss_seed = os.urandom(32)
|
||||||
|
us_seed = os.urandom(32)
|
||||||
|
master_key = olm.pk.PkSigning(master_seed)
|
||||||
|
self_signing_key = olm.pk.PkSigning(ss_seed)
|
||||||
|
user_signing_key = olm.pk.PkSigning(us_seed)
|
||||||
|
|
||||||
|
master_pub = master_key.public_key
|
||||||
|
ss_pub = self_signing_key.public_key
|
||||||
|
us_pub = user_signing_key.public_key
|
||||||
|
|
||||||
|
print(f"Master key: {master_pub}")
|
||||||
|
print(f"Self-signing key: {ss_pub}")
|
||||||
|
print(f"User-signing key: {us_pub}")
|
||||||
|
|
||||||
|
def make_key(usage, pubkey):
|
||||||
|
return {
|
||||||
|
"user_id": user_id,
|
||||||
|
"usage": [usage],
|
||||||
|
"keys": {"ed25519:" + pubkey: pubkey},
|
||||||
|
}
|
||||||
|
|
||||||
|
master_obj = make_key("master", master_pub)
|
||||||
|
ss_obj = make_key("self_signing", ss_pub)
|
||||||
|
us_obj = make_key("user_signing", us_pub)
|
||||||
|
|
||||||
|
# Sign sub-keys with master key
|
||||||
|
ss_canonical = canonicaljson.encode_canonical_json(ss_obj)
|
||||||
|
ss_sig = master_key.sign(ss_canonical)
|
||||||
|
ss_obj["signatures"] = {user_id: {"ed25519:" + master_pub: ss_sig}}
|
||||||
|
|
||||||
|
us_canonical = canonicaljson.encode_canonical_json(us_obj)
|
||||||
|
us_sig = master_key.sign(us_canonical)
|
||||||
|
us_obj["signatures"] = {user_id: {"ed25519:" + master_pub: us_sig}}
|
||||||
|
|
||||||
|
# Upload cross-signing keys with password auth
|
||||||
|
upload_body = {
|
||||||
|
"master_key": master_obj,
|
||||||
|
"self_signing_key": ss_obj,
|
||||||
|
"user_signing_key": us_obj,
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.password",
|
||||||
|
"identifier": {"type": "m.id.user", "user": user_id},
|
||||||
|
"password": BOT_PASS,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {"Authorization": "Bearer " + token}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
HS + "/_matrix/client/v3/keys/device_signing/upload",
|
||||||
|
json=upload_body,
|
||||||
|
headers=headers,
|
||||||
|
) as resp:
|
||||||
|
body = await resp.text()
|
||||||
|
print(f"Upload cross-signing keys: {resp.status} {body}")
|
||||||
|
|
||||||
|
if resp.status != 200:
|
||||||
|
await client.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sign our own device with self-signing key
|
||||||
|
# Fetch the device keys from the server to sign the exact object
|
||||||
|
async with session.post(
|
||||||
|
HS + "/_matrix/client/v3/keys/query",
|
||||||
|
json={"device_keys": {user_id: [device_id]}},
|
||||||
|
headers=headers,
|
||||||
|
) as qresp:
|
||||||
|
qbody = await qresp.json()
|
||||||
|
print(f"Keys query: {qresp.status}")
|
||||||
|
|
||||||
|
device_obj = qbody["device_keys"][user_id][device_id]
|
||||||
|
# Remove existing signatures before signing
|
||||||
|
device_obj.pop("signatures", None)
|
||||||
|
device_obj.pop("unsigned", None)
|
||||||
|
dk_canonical = canonicaljson.encode_canonical_json(device_obj)
|
||||||
|
dk_sig = self_signing_key.sign(dk_canonical)
|
||||||
|
|
||||||
|
# Add the cross-signing signature to the device object
|
||||||
|
device_obj["signatures"] = {
|
||||||
|
user_id: {
|
||||||
|
"ed25519:" + ss_pub: dk_sig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sig_upload = {
|
||||||
|
user_id: {
|
||||||
|
device_id: device_obj,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async with session.post(
|
||||||
|
HS + "/_matrix/client/v3/keys/signatures/upload",
|
||||||
|
json=sig_upload,
|
||||||
|
headers=headers,
|
||||||
|
) as resp2:
|
||||||
|
body2 = await resp2.text()
|
||||||
|
print(f"Upload device signature: {resp2.status} {body2}")
|
||||||
|
|
||||||
|
# Save cross-signing seeds for persistence
|
||||||
|
xsign_file = os.path.join(STORE, "cross_signing_keys.json")
|
||||||
|
with open(xsign_file, "w") as f:
|
||||||
|
json.dump({
|
||||||
|
"master_seed": base64.b64encode(master_seed).decode(),
|
||||||
|
"self_signing_seed": base64.b64encode(ss_seed).decode(),
|
||||||
|
"user_signing_seed": base64.b64encode(us_seed).decode(),
|
||||||
|
}, f)
|
||||||
|
print(f"Cross-signing seeds saved to {xsign_file}")
|
||||||
|
|
||||||
|
await client.close()
|
||||||
|
print("Done! Bot device is now cross-signed.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(bootstrap())
|
||||||
@@ -5,3 +5,4 @@ livekit-plugins-silero>=1.4,<2.0
|
|||||||
livekit>=1.0,<2.0
|
livekit>=1.0,<2.0
|
||||||
livekit-api>=1.0,<2.0
|
livekit-api>=1.0,<2.0
|
||||||
matrix-nio[e2e]>=0.25,<1.0
|
matrix-nio[e2e]>=0.25,<1.0
|
||||||
|
canonicaljson>=2.0,<3.0
|
||||||
|
|||||||
Reference in New Issue
Block a user