Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a09cbb-87c6-7900-a145-4ce53ba9f1a6
130 lines
6.2 KiB
Python
130 lines
6.2 KiB
Python
"""Unprivileged authority; health export is supplied by the host owner."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import stat
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
from .core import ClockError, bounded
|
|
from .protocol import strict_json, validate, sign
|
|
|
|
class Authority:
|
|
def __init__(self, config, *, wall=time.time_ns, monotonic=time.monotonic_ns):
|
|
self.config = config
|
|
self.epoch = str(uuid.uuid4())
|
|
self.wall, self.monotonic = wall, monotonic
|
|
fd = os.open(config['private_key_file'], os.O_RDONLY | os.O_NOFOLLOW)
|
|
with os.fdopen(fd, 'rb') as stream:
|
|
info = os.fstat(stream.fileno())
|
|
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o600:
|
|
raise ClockError('private authority key ownership or mode refused')
|
|
self.key = serialization.load_pem_private_key(stream.read(8193), password=None)
|
|
if not isinstance(self.key, ec.EllipticCurvePrivateKey) or not isinstance(self.key.curve, ec.SECP256R1):
|
|
raise ClockError('P-256 authority key required')
|
|
self.boot_id = Path('/proc/sys/kernel/random/boot_id').read_text().strip()
|
|
self.start_wall, self.start_mono = wall(), monotonic()
|
|
self.failed = False
|
|
self.lock = threading.Lock()
|
|
|
|
def health(self):
|
|
path = Path(self.config['health_file'])
|
|
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
|
|
with os.fdopen(fd, 'rb') as stream:
|
|
info = os.fstat(stream.fileno())
|
|
if not stat.S_ISREG(info.st_mode) or info.st_uid != self.config.get('health_owner_uid', 0) or info.st_mode & 0o022:
|
|
raise ClockError('untrusted host health file')
|
|
health = strict_json(stream.read(8193))
|
|
if health.get('quality') != 'bounded' or health.get('boot_id') != self.boot_id or health.get('policy_id') != self.config['policy_id']:
|
|
raise ClockError('host quality unusable')
|
|
age = self.monotonic() - health['observed_monotonic_ns']
|
|
if not 0 <= age < self.config['max_health_age_ns']:
|
|
raise ClockError('host health stale')
|
|
error = bounded(health['error_ns'] + (age * self.config['host_drift_ppm'] + 999999) // 1000000)
|
|
if error > self.config['max_error_ns']:
|
|
raise ClockError('host error bound exceeded')
|
|
if abs((self.wall() - self.start_wall) - (self.monotonic() - self.start_mono)) > self.config['max_continuity_error_ns']:
|
|
self.failed = True
|
|
if self.failed:
|
|
raise ClockError('authority continuity lost; new epoch admission required')
|
|
return error
|
|
|
|
def sample(self, raw):
|
|
request = validate('time-request', strict_json(raw, 1024))
|
|
for key in ('authority_id','environment'):
|
|
if request[key] != self.config[key]:
|
|
raise ClockError('wrong requested authority')
|
|
with self.lock:
|
|
received = self.wall()
|
|
error = self.health()
|
|
sent = self.wall()
|
|
if sent < received:
|
|
self.failed = True
|
|
raise ClockError('authority clock moved backwards')
|
|
payload = dict(request, epoch=self.epoch, policy_id=self.config['policy_id'],
|
|
receive_unix_ns=str(received), send_unix_ns=str(sent), error_ns=str(error),
|
|
max_age_ns=str(self.config['max_sample_age_ns']), quality='bounded')
|
|
return sign(payload, self.key, self.config['kid'])
|
|
|
|
class Server(ThreadingHTTPServer):
|
|
daemon_threads = True
|
|
request_queue_size = 8
|
|
def __init__(self, address, authority):
|
|
self.authority = authority
|
|
self.slots = threading.BoundedSemaphore(8)
|
|
super().__init__(address, Handler)
|
|
def process_request(self, request, client_address):
|
|
if not self.slots.acquire(blocking=False):
|
|
self.shutdown_request(request)
|
|
return
|
|
try:
|
|
super().process_request(request, client_address)
|
|
except Exception:
|
|
self.slots.release()
|
|
raise
|
|
def process_request_thread(self, request, client_address):
|
|
try: super().process_request_thread(request, client_address)
|
|
finally: self.slots.release()
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def setup(self):
|
|
self.request.settimeout(3)
|
|
super().setup()
|
|
def log_message(self, *_): pass
|
|
def reply(self, status, body, content_type='application/json'):
|
|
self.send_response(status)
|
|
self.send_header('Content-Type', content_type)
|
|
self.send_header('Cache-Control', 'no-store')
|
|
self.send_header('Content-Length', str(len(body)))
|
|
self.send_header('Connection', 'close')
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
def do_GET(self):
|
|
if self.path == '/healthz':
|
|
self.reply(200, json.dumps({'status':'alive','epoch':self.server.authority.epoch}).encode())
|
|
elif self.path == '/readyz':
|
|
try:
|
|
self.server.authority.health()
|
|
self.reply(200, b'{"status":"usable"}')
|
|
except (ClockError, OSError, ValueError, KeyError):
|
|
self.reply(503, b'{"status":"unusable"}')
|
|
else: self.reply(404, b'{}')
|
|
def do_POST(self):
|
|
if self.path != '/v1/time-samples':
|
|
self.reply(404, b'{}'); return
|
|
try:
|
|
lengths = self.headers.get_all('Content-Length', [])
|
|
if len(lengths) != 1 or self.headers.get('Transfer-Encoding') or self.headers.get('Content-Encoding'):
|
|
raise ClockError('ambiguous body framing')
|
|
size = int(lengths[0])
|
|
if not 0 < size <= 1024 or self.headers.get_content_type() != 'application/json':
|
|
raise ClockError('invalid request body')
|
|
raw = self.rfile.read(size)
|
|
if len(raw) != size: raise ClockError('truncated request')
|
|
body = self.server.authority.sample(raw)
|
|
self.reply(200, body, 'application/jose')
|
|
except (ClockError, OSError, ValueError, KeyError):
|
|
self.reply(503, b'{"status":"unusable"}')
|