181 lines
6.8 KiB
Python
181 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Offline policy model for a future one-shot shsrv client.
|
|
|
|
There is intentionally no transport, CLI, socket, DNS lookup, file output or
|
|
clock acquisition. Tests supply synthetic records and an explicit host time.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timedelta
|
|
import re
|
|
from typing import Any
|
|
|
|
from phase10v_shsrv_collector_model import (
|
|
CollectorError as CollectorModelError,
|
|
OfflineCollector,
|
|
)
|
|
|
|
|
|
COLLECTOR_SHA256 = \
|
|
"f8a306dafee5d135919bec5afda789dd741e57f39803b7683fb8747c186db25c"
|
|
SOURCE_BOUND_PORT = 2323
|
|
MAX_DEADLINE_SECONDS = 10
|
|
MAX_APPROVAL_LIFETIME = timedelta(minutes=15)
|
|
WINDOW_COMMANDS = {
|
|
"T2_GREETING_AND_HELP": ("help",),
|
|
"T3_ONE_EXACT_PATH": ("stat", "sum"),
|
|
}
|
|
ACTIVATION_FIELDS = {
|
|
"active", "policy_sha256", "collector_sha256", "run_id",
|
|
"target_address", "target_port", "window", "exact_literal_path",
|
|
"commands", "deadline_seconds", "expires_at",
|
|
}
|
|
REQUIRED_TRUE_FIELDS = {
|
|
"ps5_connection_authorized", "device_request_authorized",
|
|
"result_receive_authorized", "spawned_shell_effects_accepted",
|
|
"automatic_serial_query_accepted", "automatic_telemetry_query_accepted",
|
|
"sanitized_output_only_accepted",
|
|
"physical_memory_erasure_unproven_accepted",
|
|
}
|
|
REQUIRED_FALSE_FIELDS = {
|
|
"target_build_authorized", "device_transfer_authorized",
|
|
"device_execution_authorized", "installation_authorized",
|
|
"autoload_authorized", "device_write_authorized", "automatic_retry",
|
|
"reconnect_authorized", "resume_authorized", "fallback_authorized",
|
|
}
|
|
APPROVAL_FIELDS = ACTIVATION_FIELDS | REQUIRED_TRUE_FIELDS | \
|
|
REQUIRED_FALSE_FIELDS | {"attested", "listener_already_running_attested"}
|
|
|
|
|
|
class PolicyError(RuntimeError):
|
|
"""Fail-closed policy error without record values."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SessionPlan:
|
|
"""Immutable plan only; it has no method capable of network I/O."""
|
|
|
|
run_id: str
|
|
target_address: str
|
|
target_port: int
|
|
window: str
|
|
exact_literal_path: str | None
|
|
commands: tuple[str, ...]
|
|
deadline_seconds: int
|
|
expires_at: datetime
|
|
|
|
|
|
def inactive_record_is_inert(record: dict[str, Any]) -> bool:
|
|
return record == {
|
|
"active": False,
|
|
"policy_sha256": None,
|
|
"collector_sha256": None,
|
|
"run_id": None,
|
|
"target_address": None,
|
|
"target_port": None,
|
|
"window": None,
|
|
"exact_literal_path": None,
|
|
"commands": [],
|
|
"deadline_seconds": None,
|
|
"expires_at": None,
|
|
}
|
|
|
|
|
|
def _parse_expiry(value: Any, now: datetime) -> datetime:
|
|
if not isinstance(value, str):
|
|
raise PolicyError("expiry is missing")
|
|
try:
|
|
expiry = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
except ValueError as error:
|
|
raise PolicyError("expiry format is invalid") from error
|
|
if now.tzinfo is None or expiry.tzinfo is None:
|
|
raise PolicyError("expiry must be timezone aware")
|
|
if not now < expiry <= now + MAX_APPROVAL_LIFETIME:
|
|
raise PolicyError("approval is expired or too long")
|
|
return expiry
|
|
|
|
|
|
def _validate_target(value: Any) -> str:
|
|
if not isinstance(value, str) or not re.fullmatch(
|
|
r"[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?", value):
|
|
raise PolicyError("target syntax is invalid")
|
|
if ".." in value:
|
|
raise PolicyError("target syntax is invalid")
|
|
return value
|
|
|
|
|
|
def _validate_authority(record: dict[str, Any]) -> None:
|
|
if not all(record.get(field) is True for field in REQUIRED_TRUE_FIELDS):
|
|
raise PolicyError("required authority or effect acceptance is missing")
|
|
if not all(record.get(field) is False for field in REQUIRED_FALSE_FIELDS):
|
|
raise PolicyError("forbidden authority is active")
|
|
|
|
|
|
def build_session_plan(
|
|
activation: dict[str, Any], approval: dict[str, Any], now: datetime,
|
|
) -> SessionPlan:
|
|
"""Validate dual synthetic records and return an immutable offline plan."""
|
|
if set(activation) != ACTIVATION_FIELDS or set(approval) != APPROVAL_FIELDS:
|
|
raise PolicyError("record shape is not exact")
|
|
if activation.get("active") is not True or approval.get("active") is not True:
|
|
raise PolicyError("activation is inactive")
|
|
if approval.get("attested") is not True:
|
|
raise PolicyError("operator attestation is missing")
|
|
if approval.get("listener_already_running_attested") is not True:
|
|
raise PolicyError("listener attestation is missing")
|
|
_validate_authority(approval)
|
|
matching_fields = (
|
|
"policy_sha256", "collector_sha256", "run_id", "target_address", "target_port",
|
|
"window", "exact_literal_path", "commands", "deadline_seconds",
|
|
"expires_at",
|
|
)
|
|
if any(activation.get(field) != approval.get(field)
|
|
for field in matching_fields):
|
|
raise PolicyError("activation and approval do not match")
|
|
if activation.get("collector_sha256") != COLLECTOR_SHA256:
|
|
raise PolicyError("collector identity mismatch")
|
|
if not isinstance(activation.get("policy_sha256"), str) or \
|
|
not re.fullmatch(r"[0-9a-f]{64}", activation["policy_sha256"]):
|
|
raise PolicyError("policy identity syntax is invalid")
|
|
run_id = activation.get("run_id")
|
|
if not isinstance(run_id, str) or not re.fullmatch(
|
|
r"[A-Za-z0-9_-]{8,64}", run_id):
|
|
raise PolicyError("run identifier is invalid")
|
|
target = _validate_target(activation.get("target_address"))
|
|
if activation.get("target_port") != SOURCE_BOUND_PORT:
|
|
raise PolicyError("target port mismatch")
|
|
window = activation.get("window")
|
|
if window not in WINDOW_COMMANDS:
|
|
raise PolicyError("window is not allowlisted")
|
|
commands = activation.get("commands")
|
|
if commands != list(WINDOW_COMMANDS[window]):
|
|
raise PolicyError("command sequence mismatch")
|
|
path = activation.get("exact_literal_path")
|
|
if window == "T2_GREETING_AND_HELP" and path is not None:
|
|
raise PolicyError("help window cannot include a path")
|
|
if window == "T3_ONE_EXACT_PATH":
|
|
if not isinstance(path, str):
|
|
raise PolicyError("exact path is missing")
|
|
try:
|
|
OfflineCollector._validate_expected_paths({path})
|
|
except CollectorModelError as error:
|
|
raise PolicyError("exact path is invalid") from error
|
|
deadline = activation.get("deadline_seconds")
|
|
if not isinstance(deadline, int) or isinstance(deadline, bool) or \
|
|
not 1 <= deadline <= MAX_DEADLINE_SECONDS:
|
|
raise PolicyError("deadline is invalid")
|
|
expiry = _parse_expiry(activation.get("expires_at"), now)
|
|
return SessionPlan(
|
|
run_id=run_id,
|
|
target_address=target,
|
|
target_port=SOURCE_BOUND_PORT,
|
|
window=window,
|
|
exact_literal_path=path,
|
|
commands=WINDOW_COMMANDS[window],
|
|
deadline_seconds=deadline,
|
|
expires_at=expiry,
|
|
)
|