179 lines
6.5 KiB
Python
179 lines
6.5 KiB
Python
|
|
#!/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()
|