Files
box/bin/crypt-server.py
T

179 lines
6.5 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
crypt-server.py — Public cryptographic attestation and key directory for NetVM.
Serves https://crypt.muse-dev.online/ (via cloudflared / reverse proxy).
Endpoints:
GET / -> Service directory / health JSON
GET /health -> Health check
GET /keys/allowed_signers -> OpenSSH allowed_signers formatted file
GET /keys/{identity}.pub -> Individual public key
GET /proofs -> List known proof hashes / work order attestations
GET /proofs/{id} -> Retrieve proof envelope and SSH signature
POST /proofs -> Submit / register a signed proof record
"""
import argparse
import glob
import json
import os
import re
import ssl
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler
REPO_DIR = "/home/super/Projects/NetVM"
SIGNERS_DIR = os.path.join(REPO_DIR, "dm-signers")
PROOFS_DIR = os.path.join(REPO_DIR, "var", "proofs")
os.makedirs(PROOFS_DIR, exist_ok=True)
class CryptHandler(BaseHTTPRequestHandler):
server_version = "crypt-attestation/1.0"
def log_message(self, format, *args):
sys.stderr.write(f"crypt-server: {self.client_address[0]} - {format % args}\n")
def _send(self, code, content, content_type="application/json"):
if isinstance(content, (dict, list)):
body = json.dumps(content, indent=2).encode("utf-8")
elif isinstance(content, str):
body = content.encode("utf-8")
else:
body = bytes(content)
self.send_response(code)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body)))
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(body)
def do_GET(self):
path = self.path.split("?")[0].rstrip("/")
if not path:
path = "/"
if path in ("/", "/health"):
self._send(200, {
"service": "crypt.muse-dev.online",
"status": "active",
"mode": "public_attestation",
"endpoints": [
"/keys/allowed_signers",
"/keys/<identity>.pub",
"/proofs",
"/proofs/<id>"
]
})
return
if path == "/keys/allowed_signers":
allowed_path = os.path.join(SIGNERS_DIR, "allowed_signers")
if os.path.exists(allowed_path):
with open(allowed_path, "r") as f:
data = f.read()
self._send(200, data, content_type="text/plain; charset=utf-8")
else:
self._send(404, {"error": "allowed_signers not found"})
return
m_key = re.match(r"^/keys/([a-zA-Z0-9_\-\.]+)\.pub$", path)
if m_key:
ident = m_key.group(1)
pub_path = os.path.join(SIGNERS_DIR, f"{ident}.pub")
if os.path.exists(pub_path):
with open(pub_path, "r") as f:
data = f.read()
self._send(200, data, content_type="text/plain; charset=utf-8")
else:
self._send(404, {"error": f"Public key for {ident} not found"})
return
if path == "/proofs":
proof_files = glob.glob(os.path.join(PROOFS_DIR, "*.json"))
ids = [os.path.basename(p)[:-5] for p in proof_files]
self._send(200, {"proofs": sorted(ids)})
return
m_proof = re.match(r"^/proofs/([a-zA-Z0-9_\-]+)$", path)
if m_proof:
pid = m_proof.group(1)
pf = os.path.join(PROOFS_DIR, f"{pid}.json")
if os.path.exists(pf):
with open(pf, "r") as f:
data = json.load(f)
self._send(200, data)
else:
self._send(404, {"error": f"Proof {pid} not found"})
return
self._send(404, {"error": "not found"})
def do_POST(self):
path = self.path.split("?")[0].rstrip("/")
if path == "/proofs":
try:
length = int(self.headers.get("Content-Length", 0))
except ValueError:
length = 0
if length <= 0 or length > 65536:
self._send(400, {"error": "invalid content length"})
return
try:
data = json.loads(self.rfile.read(length))
except Exception:
self._send(400, {"error": "malformed JSON"})
return
pid = data.get("id")
if not pid or not re.match(r"^[a-zA-Z0-9_\-]+$", pid):
self._send(400, {"error": "missing or invalid proof id"})
return
pf = os.path.join(PROOFS_DIR, f"{pid}.json")
with open(pf, "w") as f:
json.dump(data, f, indent=2)
self._send(201, {"status": "stored", "id": pid, "url": f"https://crypt.muse-dev.online/proofs/{pid}"})
return
self._send(404, {"error": "not found"})
def main():
parser = argparse.ArgumentParser(description="Crypt attestation and key server")
parser.add_argument("--port", type=int, default=8446)
parser.add_argument("--host", default="100.123.153.75")
parser.add_argument("--ssl", action="store_true", help="Enable self-signed HTTPS")
args = parser.parse_args()
server = HTTPServer((args.host, args.port), CryptHandler)
if args.ssl:
cert_file = os.path.join(REPO_DIR, "ssl", "crypt-selfsigned.crt")
key_file = os.path.join(REPO_DIR, "ssl", "crypt-selfsigned.key")
os.makedirs(os.path.dirname(cert_file), exist_ok=True)
if not os.path.exists(cert_file):
import subprocess
subprocess.run([
"openssl", "req", "-x509", "-newkey", "rsa:2048",
"-keyout", key_file, "-out", cert_file,
"-days", "3650", "-nodes",
"-subj", "/CN=crypt.muse-dev.online"
], check=True, capture_output=True)
os.chmod(key_file, 0o600)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(cert_file, key_file)
server.socket = ctx.wrap_socket(server.socket, server_side=True)
print(f"Crypt server running on {'https' if args.ssl else 'http'}://{args.host}:{args.port}", file=sys.stderr)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nShutting down crypt server", file=sys.stderr)
if __name__ == "__main__":
main()