"""Secrets and package bundling for Agent distribution. Two independent secrets guard the distribution flow, both random(256 bits), single-use and short-TTL, both stored only as SHA-256 hashes: - the *download* token: embedded in the shareable link an operator hands to whoever installs the Agent (or clicked directly, authenticated, from the panel); - the *enrollment* token: embedded inside the downloaded bundle as `enrollment.json`, consumed exactly once by `POST /v1/agents/enroll`. Neither token is ever the Agent's own permanent Bearer token (issued only after a successful enrollment) and neither is ever logged or audited in plaintext. """ from __future__ import annotations import hashlib import hmac import io import json import re import secrets import zipfile from dataclasses import dataclass class EnrollmentRejected(Exception): def __init__(self, reason: str): super().__init__(reason) self.reason = reason def new_token() -> str: """256 bits of randomness, URL-safe — usable directly in a link or a JSON field.""" return secrets.token_urlsafe(32) def token_digest(token: str) -> str: return hashlib.sha256(token.encode()).hexdigest() def token_matches(token: str | None, stored_hash: str | None) -> bool: if not token or not stored_hash: return False return hmac.compare_digest(token_digest(token), stored_hash) ENROLLMENT_UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") def valid_uuid(value: str | None) -> bool: return bool(value) and bool(ENROLLMENT_UUID_RE.match(value)) @dataclass(frozen=True) class BundlePlan: hub_url: str enrollment_uuid: str enrollment_token: str enroll_path: str = "/v1/agents/enroll" def build_bundle(release_zip_bytes: bytes, plan: BundlePlan) -> bytes: """Return a new zip = the release zip's entries plus one `/enrollment.json` holding only temporary, single-use data. Never a permanent credential — that is issued only once enrollment succeeds.""" with zipfile.ZipFile(io.BytesIO(release_zip_bytes)) as source: names = source.namelist() if not names: raise EnrollmentRejected("EMPTY_RELEASE_PACKAGE") root = names[0].split("/", 1)[0] out = io.BytesIO() with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as dest: for name in names: dest.writestr(name, source.read(name)) payload = { "installation_id": plan.enrollment_uuid, "enrollment_token": plan.enrollment_token, "hub_url": plan.hub_url, "enroll_path": plan.enroll_path, } dest.writestr(f"{root}/enrollment.json", json.dumps(payload, indent=1)) return out.getvalue()