708 lines
32 KiB
Python
708 lines
32 KiB
Python
"""Single-host operational delegation for AIOS migration.
|
|
|
|
This module is deliberately narrow:
|
|
* Owner signs only OperationalDelegation and ExpiredTaskRecoveryDecision.
|
|
* Machine signer signs only WorkerRuntimeBinding and WorkerRuntimeProof.
|
|
* Every object is bound to one immutable Task identity and a narrow capability set.
|
|
* There is no arbitrary sign-bytes API.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, dataclass
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import time
|
|
from typing import Any, Mapping, Sequence
|
|
|
|
from aios_single_host_authority import (
|
|
DEFAULT_MACHINE_SIGNER_ID,
|
|
DEFAULT_OWNER_SIGNER_ID,
|
|
MACHINE_NAMESPACE,
|
|
OWNER_IDENTITY,
|
|
OWNER_NAMESPACE,
|
|
LocalFileSigner,
|
|
RegistryOwnerApprovalVerifier,
|
|
SignerRegistry,
|
|
SingleHostAuthorityError,
|
|
canonical_json_bytes,
|
|
sha256_json,
|
|
verify_sshsig,
|
|
)
|
|
|
|
OPERATIONAL_POLICY_SCHEMA = "aios.single-host-operational-policy.v1"
|
|
OPERATIONAL_DELEGATION_SCHEMA = "aios.owner-operational-delegation.v1"
|
|
RECOVERY_DECISION_SCHEMA = "aios.expired-task-recovery-decision.v1"
|
|
WORKER_BINDING_SCHEMA = "aios.worker-runtime-binding.v1"
|
|
WORKER_PROOF_SCHEMA = "aios.worker-runtime-proof.v1"
|
|
REVOCATION_SCHEMA = "aios.operational-revocations.v1"
|
|
|
|
ALLOWED_OPERATIONAL_CAPABILITIES = frozenset({
|
|
"task.recovery",
|
|
"worker.runtime.bind",
|
|
"worker.runtime.proof",
|
|
"task.claim.guard",
|
|
})
|
|
ALLOWED_EXPECTED_STATE = "EXPIRED"
|
|
HEX40 = re.compile(r"^[0-9a-f]{40}$")
|
|
HEX64 = re.compile(r"^[0-9a-f]{64}$")
|
|
SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/+:-]{2,511}$")
|
|
CAP = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
|
|
|
|
|
|
class OperationalAuthorityError(RuntimeError):
|
|
def __init__(self, code: str):
|
|
super().__init__(code)
|
|
self.code = code
|
|
|
|
|
|
def fail(code: str) -> None:
|
|
raise OperationalAuthorityError(code)
|
|
|
|
|
|
def _exact_fields(value: Mapping[str, Any], fields: set[str], code: str) -> None:
|
|
if not isinstance(value, Mapping) or set(value) != fields:
|
|
fail(code)
|
|
|
|
|
|
def _safe_id(value: str, code: str) -> str:
|
|
if not isinstance(value, str) or not SAFE_ID.fullmatch(value):
|
|
fail(code)
|
|
return value
|
|
|
|
|
|
def _hex(value: str, n: int, code: str) -> str:
|
|
regex = HEX40 if n == 40 else HEX64
|
|
if not isinstance(value, str) or not regex.fullmatch(value):
|
|
fail(code)
|
|
return value
|
|
|
|
|
|
def _caps(value: Sequence[str], code: str) -> tuple[str, ...]:
|
|
if not isinstance(value, (list, tuple)) or not value:
|
|
fail(code)
|
|
out = tuple(sorted(set(value)))
|
|
if len(out) != len(value) or any(not isinstance(x, str) or not CAP.fullmatch(x) for x in out):
|
|
fail(code)
|
|
return out
|
|
|
|
|
|
def task_identity_sha256(task: Mapping[str, Any]) -> str:
|
|
"""Digest only immutable recovery/claim identity fields.
|
|
|
|
Revision, state, attempt and deadline are intentionally excluded.
|
|
"""
|
|
fields = (
|
|
"task_id", "repository", "target_branch", "base_oid", "generation",
|
|
"execution_plan_sha256", "external_job_identity",
|
|
)
|
|
try:
|
|
value = {name: task[name] for name in fields}
|
|
except KeyError as exc:
|
|
raise OperationalAuthorityError("TASK_IDENTITY_FIELD_MISSING") from exc
|
|
_safe_id(str(value["task_id"]), "TASK_ID_INVALID")
|
|
_safe_id(str(value["repository"]), "REPOSITORY_INVALID")
|
|
_safe_id(str(value["target_branch"]), "TARGET_BRANCH_INVALID")
|
|
_hex(str(value["base_oid"]), 40, "BASE_OID_INVALID")
|
|
if type(value["generation"]) is not int or value["generation"] < 1:
|
|
fail("GENERATION_INVALID")
|
|
_hex(str(value["execution_plan_sha256"]), 64, "EXECUTION_PLAN_DIGEST_INVALID")
|
|
_safe_id(str(value["external_job_identity"]), "EXTERNAL_JOB_IDENTITY_INVALID")
|
|
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class OperationalPolicy:
|
|
repository: str
|
|
task_id: str
|
|
task_identity_sha256: str
|
|
target_branch: str
|
|
base_oid: str
|
|
generation: int
|
|
execution_plan_sha256: str
|
|
external_job_identity: str
|
|
allowed_capabilities: tuple[str, ...]
|
|
allowed_worker_ids: tuple[str, ...]
|
|
maximum_delegation_ttl_seconds: int
|
|
maximum_recovery_decision_ttl_seconds: int
|
|
maximum_binding_ttl_seconds: int
|
|
maximum_proof_ttl_seconds: int
|
|
maximum_heartbeat_age_seconds: int
|
|
maximum_task_deadline_extension_seconds: int
|
|
schema: str = OPERATIONAL_POLICY_SCHEMA
|
|
|
|
def validate(self) -> None:
|
|
if self.schema != OPERATIONAL_POLICY_SCHEMA:
|
|
fail("OPERATIONAL_POLICY_SCHEMA_INVALID")
|
|
_safe_id(self.repository, "OPERATIONAL_POLICY_REPOSITORY_INVALID")
|
|
_safe_id(self.task_id, "OPERATIONAL_POLICY_TASK_ID_INVALID")
|
|
_hex(self.task_identity_sha256, 64, "OPERATIONAL_POLICY_TASK_IDENTITY_INVALID")
|
|
_safe_id(self.target_branch, "OPERATIONAL_POLICY_BRANCH_INVALID")
|
|
_hex(self.base_oid, 40, "OPERATIONAL_POLICY_BASE_OID_INVALID")
|
|
if type(self.generation) is not int or self.generation < 1:
|
|
fail("OPERATIONAL_POLICY_GENERATION_INVALID")
|
|
_hex(self.execution_plan_sha256, 64, "OPERATIONAL_POLICY_PLAN_INVALID")
|
|
_safe_id(self.external_job_identity, "OPERATIONAL_POLICY_JOB_INVALID")
|
|
caps = _caps(self.allowed_capabilities, "OPERATIONAL_POLICY_CAPABILITIES_INVALID")
|
|
if not set(caps).issubset(ALLOWED_OPERATIONAL_CAPABILITIES):
|
|
fail("OPERATIONAL_POLICY_CAPABILITY_WIDENING")
|
|
if not self.allowed_worker_ids or any(not SAFE_ID.fullmatch(x) for x in self.allowed_worker_ids):
|
|
fail("OPERATIONAL_POLICY_WORKERS_INVALID")
|
|
bounds = (
|
|
(self.maximum_delegation_ttl_seconds, 300, 7 * 24 * 3600),
|
|
(self.maximum_recovery_decision_ttl_seconds, 30, 900),
|
|
(self.maximum_binding_ttl_seconds, 60, 24 * 3600),
|
|
(self.maximum_proof_ttl_seconds, 15, 900),
|
|
(self.maximum_heartbeat_age_seconds, 5, 300),
|
|
(self.maximum_task_deadline_extension_seconds, 300, 24 * 3600),
|
|
)
|
|
if any(type(v) is not int or not lo <= v <= hi for v, lo, hi in bounds):
|
|
fail("OPERATIONAL_POLICY_TTL_INVALID")
|
|
|
|
@property
|
|
def digest(self) -> str:
|
|
self.validate()
|
|
return sha256_json(asdict(self))
|
|
|
|
@classmethod
|
|
def from_mapping(cls, value: Mapping[str, Any]) -> "OperationalPolicy":
|
|
fields = set(cls.__dataclass_fields__)
|
|
_exact_fields(value, fields, "OPERATIONAL_POLICY_FIELDS_INVALID")
|
|
try:
|
|
policy = cls(
|
|
repository=value["repository"],
|
|
task_id=value["task_id"],
|
|
task_identity_sha256=value["task_identity_sha256"],
|
|
target_branch=value["target_branch"],
|
|
base_oid=value["base_oid"],
|
|
generation=value["generation"],
|
|
execution_plan_sha256=value["execution_plan_sha256"],
|
|
external_job_identity=value["external_job_identity"],
|
|
allowed_capabilities=tuple(value["allowed_capabilities"]),
|
|
allowed_worker_ids=tuple(value["allowed_worker_ids"]),
|
|
maximum_delegation_ttl_seconds=value["maximum_delegation_ttl_seconds"],
|
|
maximum_recovery_decision_ttl_seconds=value["maximum_recovery_decision_ttl_seconds"],
|
|
maximum_binding_ttl_seconds=value["maximum_binding_ttl_seconds"],
|
|
maximum_proof_ttl_seconds=value["maximum_proof_ttl_seconds"],
|
|
maximum_heartbeat_age_seconds=value["maximum_heartbeat_age_seconds"],
|
|
maximum_task_deadline_extension_seconds=value["maximum_task_deadline_extension_seconds"],
|
|
schema=value["schema"],
|
|
)
|
|
except Exception as exc:
|
|
raise OperationalAuthorityError("OPERATIONAL_POLICY_FIELDS_INVALID") from exc
|
|
policy.validate()
|
|
return policy
|
|
|
|
|
|
def build_exact_policy(
|
|
task: Mapping[str, Any],
|
|
*,
|
|
worker_id: str,
|
|
allowed_capabilities: Sequence[str] = tuple(sorted(ALLOWED_OPERATIONAL_CAPABILITIES)),
|
|
) -> OperationalPolicy:
|
|
identity = task_identity_sha256(task)
|
|
policy = OperationalPolicy(
|
|
repository=str(task["repository"]),
|
|
task_id=str(task["task_id"]),
|
|
task_identity_sha256=identity,
|
|
target_branch=str(task["target_branch"]),
|
|
base_oid=str(task["base_oid"]),
|
|
generation=int(task["generation"]),
|
|
execution_plan_sha256=str(task["execution_plan_sha256"]),
|
|
external_job_identity=str(task["external_job_identity"]),
|
|
allowed_capabilities=tuple(sorted(allowed_capabilities)),
|
|
allowed_worker_ids=(worker_id,),
|
|
maximum_delegation_ttl_seconds=24 * 3600,
|
|
maximum_recovery_decision_ttl_seconds=300,
|
|
maximum_binding_ttl_seconds=4 * 3600,
|
|
maximum_proof_ttl_seconds=300,
|
|
maximum_heartbeat_age_seconds=30,
|
|
maximum_task_deadline_extension_seconds=4 * 3600,
|
|
)
|
|
policy.validate()
|
|
return policy
|
|
|
|
|
|
def _signed_payload_dict(payload: Mapping[str, Any], *, key_id: str, namespace: str) -> dict[str, Any]:
|
|
value = dict(payload)
|
|
value["signature_namespace"] = namespace
|
|
value["signature_key_id"] = key_id
|
|
value["canonical_payload_digest"] = sha256_json(value)
|
|
return value
|
|
|
|
|
|
def _verify_digest_field(payload: Mapping[str, Any]) -> bool:
|
|
if not isinstance(payload, Mapping) or "canonical_payload_digest" not in payload:
|
|
return False
|
|
value = dict(payload)
|
|
supplied = value.pop("canonical_payload_digest", None)
|
|
return isinstance(supplied, str) and supplied == sha256_json(value)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SignedOperationalArtifact:
|
|
payload: dict[str, Any]
|
|
signature: str
|
|
key_id: str
|
|
namespace: str
|
|
|
|
@property
|
|
def canonical_payload(self) -> bytes:
|
|
return canonical_json_bytes(self.payload)
|
|
|
|
@property
|
|
def payload_sha256(self) -> str:
|
|
return hashlib.sha256(self.canonical_payload).hexdigest()
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"payload": dict(self.payload),
|
|
"signature": self.signature,
|
|
"key_id": self.key_id,
|
|
"namespace": self.namespace,
|
|
}
|
|
|
|
|
|
class OperationalOwnerAuthority:
|
|
def __init__(self, key_path: Path, policy: OperationalPolicy, *, require_root_owner: bool = True, clock: Any = time.time):
|
|
policy.validate()
|
|
self.policy = policy
|
|
self.signer = LocalFileSigner(
|
|
Path(key_path),
|
|
signer_id=DEFAULT_OWNER_SIGNER_ID,
|
|
namespace=OWNER_NAMESPACE,
|
|
require_root_owner=require_root_owner,
|
|
)
|
|
self.clock = clock
|
|
|
|
@property
|
|
def fingerprint(self) -> str:
|
|
return self.signer.fingerprint
|
|
|
|
def _sign(self, payload: Mapping[str, Any]) -> SignedOperationalArtifact:
|
|
full = _signed_payload_dict(payload, key_id=self.fingerprint, namespace=OWNER_NAMESPACE)
|
|
art = self.signer._sign_canonical(full)
|
|
return SignedOperationalArtifact(dict(art.payload), art.signature, art.key_id, art.namespace)
|
|
|
|
def issue_operational_delegation(self, *, delegation_id: str, nonce: str, ttl_seconds: int) -> SignedOperationalArtifact:
|
|
now = int(self.clock())
|
|
p = self.policy
|
|
if not SAFE_ID.fullmatch(delegation_id) or not SAFE_ID.fullmatch(nonce):
|
|
fail("OPERATIONAL_DELEGATION_ID_INVALID")
|
|
if type(ttl_seconds) is not int or not 60 <= ttl_seconds <= p.maximum_delegation_ttl_seconds:
|
|
fail("OPERATIONAL_DELEGATION_TTL_INVALID")
|
|
payload = {
|
|
"schema": OPERATIONAL_DELEGATION_SCHEMA,
|
|
"delegation_id": delegation_id,
|
|
"nonce": nonce,
|
|
"owner_identity": OWNER_IDENTITY,
|
|
"repository": p.repository,
|
|
"task_id": p.task_id,
|
|
"task_identity_sha256": p.task_identity_sha256,
|
|
"target_branch": p.target_branch,
|
|
"base_oid": p.base_oid,
|
|
"generation": p.generation,
|
|
"execution_plan_sha256": p.execution_plan_sha256,
|
|
"external_job_identity": p.external_job_identity,
|
|
"allowed_capabilities": list(p.allowed_capabilities),
|
|
"allowed_worker_ids": list(p.allowed_worker_ids),
|
|
"policy_digest": p.digest,
|
|
"issued_at": now,
|
|
"not_before": max(0, now - 5),
|
|
"expires_at": now + ttl_seconds,
|
|
"revocation_epoch": 0,
|
|
"status": "ACTIVE",
|
|
}
|
|
return self._sign(payload)
|
|
|
|
def issue_recovery_decision(
|
|
self,
|
|
delegation: Mapping[str, Any],
|
|
*,
|
|
decision_id: str,
|
|
nonce: str,
|
|
expected_revision: int,
|
|
expected_attempt: int,
|
|
original_deadline: int,
|
|
new_deadline: int,
|
|
ttl_seconds: int,
|
|
) -> SignedOperationalArtifact:
|
|
now = int(self.clock())
|
|
verify_delegation_scope(delegation, self.policy, now=now)
|
|
if "task.recovery" not in delegation["allowed_capabilities"]:
|
|
fail("RECOVERY_CAPABILITY_NOT_DELEGATED")
|
|
if not SAFE_ID.fullmatch(decision_id) or not SAFE_ID.fullmatch(nonce):
|
|
fail("RECOVERY_DECISION_ID_INVALID")
|
|
if type(expected_revision) is not int or expected_revision < 0 or type(expected_attempt) is not int or expected_attempt < 0:
|
|
fail("RECOVERY_CAS_INVALID")
|
|
if type(original_deadline) is not int or type(new_deadline) is not int:
|
|
fail("RECOVERY_DEADLINE_INVALID")
|
|
if new_deadline <= now or new_deadline <= original_deadline:
|
|
fail("RECOVERY_DEADLINE_INVALID")
|
|
if new_deadline - now > self.policy.maximum_task_deadline_extension_seconds:
|
|
fail("RECOVERY_DEADLINE_WIDENING")
|
|
if type(ttl_seconds) is not int or not 15 <= ttl_seconds <= self.policy.maximum_recovery_decision_ttl_seconds:
|
|
fail("RECOVERY_DECISION_TTL_INVALID")
|
|
payload = {
|
|
"schema": RECOVERY_DECISION_SCHEMA,
|
|
"decision_id": decision_id,
|
|
"nonce": nonce,
|
|
"parent_delegation_id": delegation["delegation_id"],
|
|
"parent_delegation_digest": hashlib.sha256(canonical_json_bytes(dict(delegation))).hexdigest(),
|
|
"task_id": self.policy.task_id,
|
|
"task_identity_sha256": self.policy.task_identity_sha256,
|
|
"expected_state": ALLOWED_EXPECTED_STATE,
|
|
"expected_revision": expected_revision,
|
|
"expected_attempt": expected_attempt,
|
|
"original_deadline": original_deadline,
|
|
"new_deadline": new_deadline,
|
|
"repository": self.policy.repository,
|
|
"target_branch": self.policy.target_branch,
|
|
"base_oid": self.policy.base_oid,
|
|
"generation": self.policy.generation,
|
|
"execution_plan_sha256": self.policy.execution_plan_sha256,
|
|
"external_job_identity": self.policy.external_job_identity,
|
|
"policy_digest": self.policy.digest,
|
|
"one_time_use": True,
|
|
"issued_at": now,
|
|
"not_before": max(0, now - 5),
|
|
"expires_at": now + ttl_seconds,
|
|
}
|
|
return self._sign(payload)
|
|
|
|
|
|
class OperationalMachineAuthority:
|
|
def __init__(self, key_path: Path, policy: OperationalPolicy, *, require_root_owner: bool = True, clock: Any = time.time):
|
|
policy.validate()
|
|
self.policy = policy
|
|
self.signer = LocalFileSigner(
|
|
Path(key_path),
|
|
signer_id=DEFAULT_MACHINE_SIGNER_ID,
|
|
namespace=MACHINE_NAMESPACE,
|
|
require_root_owner=require_root_owner,
|
|
)
|
|
self.clock = clock
|
|
|
|
@property
|
|
def fingerprint(self) -> str:
|
|
return self.signer.fingerprint
|
|
|
|
def _sign(self, payload: Mapping[str, Any]) -> SignedOperationalArtifact:
|
|
full = _signed_payload_dict(payload, key_id=self.fingerprint, namespace=MACHINE_NAMESPACE)
|
|
art = self.signer._sign_canonical(full)
|
|
return SignedOperationalArtifact(dict(art.payload), art.signature, art.key_id, art.namespace)
|
|
|
|
def issue_runtime_binding(
|
|
self,
|
|
delegation: Mapping[str, Any],
|
|
*,
|
|
binding_id: str,
|
|
nonce: str,
|
|
worker_id: str,
|
|
runtime_id: str,
|
|
runtime_sha256: str,
|
|
task_contract_version: str,
|
|
supported_contract_versions: Sequence[str],
|
|
required_capabilities: Sequence[str],
|
|
worker_capabilities: Sequence[str],
|
|
max_concurrency: int,
|
|
ttl_seconds: int,
|
|
) -> SignedOperationalArtifact:
|
|
now = int(self.clock())
|
|
p = self.policy
|
|
verify_delegation_scope(delegation, p, now=now)
|
|
if "worker.runtime.bind" not in delegation["allowed_capabilities"]:
|
|
fail("RUNTIME_BIND_CAPABILITY_NOT_DELEGATED")
|
|
if worker_id not in p.allowed_worker_ids:
|
|
fail("WORKER_ID_NOT_DELEGATED")
|
|
for val, code in [(binding_id, "BINDING_ID_INVALID"), (nonce, "BINDING_NONCE_INVALID"), (worker_id, "WORKER_ID_INVALID"), (runtime_id, "RUNTIME_ID_INVALID"), (task_contract_version, "CONTRACT_VERSION_INVALID")]:
|
|
_safe_id(val, code)
|
|
_hex(runtime_sha256, 64, "RUNTIME_SHA256_INVALID")
|
|
supported = tuple(sorted(set(supported_contract_versions)))
|
|
if task_contract_version not in supported or not supported or any(not SAFE_ID.fullmatch(x) for x in supported):
|
|
fail("CONTRACT_INCOMPATIBLE")
|
|
required = _caps(required_capabilities, "REQUIRED_CAPABILITIES_INVALID")
|
|
worker_caps = _caps(worker_capabilities, "WORKER_CAPABILITIES_INVALID")
|
|
if not set(required).issubset(set(worker_caps)):
|
|
fail("CAPABILITY_MISMATCH")
|
|
if type(max_concurrency) is not int or not 1 <= max_concurrency <= 64:
|
|
fail("MAX_CONCURRENCY_INVALID")
|
|
if type(ttl_seconds) is not int or not 30 <= ttl_seconds <= p.maximum_binding_ttl_seconds:
|
|
fail("BINDING_TTL_INVALID")
|
|
if now + ttl_seconds > int(delegation["expires_at"]):
|
|
fail("BINDING_EXCEEDS_PARENT")
|
|
payload = {
|
|
"schema": WORKER_BINDING_SCHEMA,
|
|
"binding_id": binding_id,
|
|
"nonce": nonce,
|
|
"parent_delegation_id": delegation["delegation_id"],
|
|
"parent_delegation_digest": hashlib.sha256(canonical_json_bytes(dict(delegation))).hexdigest(),
|
|
"repository": p.repository,
|
|
"task_id": p.task_id,
|
|
"task_identity_sha256": p.task_identity_sha256,
|
|
"target_branch": p.target_branch,
|
|
"base_oid": p.base_oid,
|
|
"generation": p.generation,
|
|
"execution_plan_sha256": p.execution_plan_sha256,
|
|
"external_job_identity": p.external_job_identity,
|
|
"worker_id": worker_id,
|
|
"runtime_id": runtime_id,
|
|
"runtime_sha256": runtime_sha256,
|
|
"task_contract_version": task_contract_version,
|
|
"supported_contract_versions": list(supported),
|
|
"required_capabilities": list(required),
|
|
"worker_capabilities": list(worker_caps),
|
|
"max_concurrency": max_concurrency,
|
|
"policy_digest": p.digest,
|
|
"issued_at": now,
|
|
"not_before": max(0, now - 5),
|
|
"expires_at": now + ttl_seconds,
|
|
}
|
|
return self._sign(payload)
|
|
|
|
def issue_runtime_proof(
|
|
self,
|
|
delegation: Mapping[str, Any],
|
|
binding: Mapping[str, Any],
|
|
*,
|
|
proof_id: str,
|
|
nonce: str,
|
|
heartbeat_at: int,
|
|
adapter_readiness: bool,
|
|
resource_readiness: bool,
|
|
active_claim_count: int,
|
|
ttl_seconds: int,
|
|
) -> SignedOperationalArtifact:
|
|
now = int(self.clock())
|
|
p = self.policy
|
|
verify_delegation_scope(delegation, p, now=now)
|
|
verify_binding_scope(binding, delegation, p, now=now)
|
|
if "worker.runtime.proof" not in delegation["allowed_capabilities"]:
|
|
fail("RUNTIME_PROOF_CAPABILITY_NOT_DELEGATED")
|
|
_safe_id(proof_id, "PROOF_ID_INVALID")
|
|
_safe_id(nonce, "PROOF_NONCE_INVALID")
|
|
if type(heartbeat_at) is not int or heartbeat_at > now + 5 or now - heartbeat_at > p.maximum_heartbeat_age_seconds:
|
|
fail("HEARTBEAT_STALE")
|
|
if adapter_readiness is not True or resource_readiness is not True:
|
|
fail("RUNTIME_NOT_READY")
|
|
if type(active_claim_count) is not int or active_claim_count < 0 or active_claim_count >= int(binding["max_concurrency"]):
|
|
fail("MAX_CONCURRENCY_REACHED")
|
|
if type(ttl_seconds) is not int or not 10 <= ttl_seconds <= p.maximum_proof_ttl_seconds:
|
|
fail("PROOF_TTL_INVALID")
|
|
if now + ttl_seconds > min(int(binding["expires_at"]), int(delegation["expires_at"])):
|
|
fail("PROOF_EXCEEDS_PARENT")
|
|
payload = {
|
|
"schema": WORKER_PROOF_SCHEMA,
|
|
"proof_id": proof_id,
|
|
"nonce": nonce,
|
|
"parent_delegation_id": delegation["delegation_id"],
|
|
"parent_delegation_digest": hashlib.sha256(canonical_json_bytes(dict(delegation))).hexdigest(),
|
|
"binding_id": binding["binding_id"],
|
|
"binding_digest": hashlib.sha256(canonical_json_bytes(dict(binding))).hexdigest(),
|
|
"repository": p.repository,
|
|
"task_id": p.task_id,
|
|
"task_identity_sha256": p.task_identity_sha256,
|
|
"generation": p.generation,
|
|
"execution_plan_sha256": p.execution_plan_sha256,
|
|
"worker_id": binding["worker_id"],
|
|
"runtime_id": binding["runtime_id"],
|
|
"runtime_sha256": binding["runtime_sha256"],
|
|
"heartbeat_at": heartbeat_at,
|
|
"adapter_readiness": True,
|
|
"resource_readiness": True,
|
|
"active_claim_count": active_claim_count,
|
|
"max_concurrency": binding["max_concurrency"],
|
|
"policy_digest": p.digest,
|
|
"issued_at": now,
|
|
"expires_at": now + ttl_seconds,
|
|
}
|
|
return self._sign(payload)
|
|
|
|
|
|
DELEGATION_FIELDS = {
|
|
"schema","delegation_id","nonce","owner_identity","repository","task_id",
|
|
"task_identity_sha256","target_branch","base_oid","generation","execution_plan_sha256",
|
|
"external_job_identity","allowed_capabilities","allowed_worker_ids","policy_digest",
|
|
"issued_at","not_before","expires_at","revocation_epoch","status",
|
|
"signature_namespace","signature_key_id","canonical_payload_digest",
|
|
}
|
|
BINDING_FIELDS = {
|
|
"schema","binding_id","nonce","parent_delegation_id","parent_delegation_digest",
|
|
"repository","task_id","task_identity_sha256","target_branch","base_oid","generation",
|
|
"execution_plan_sha256","external_job_identity","worker_id","runtime_id","runtime_sha256",
|
|
"task_contract_version","supported_contract_versions","required_capabilities","worker_capabilities",
|
|
"max_concurrency","policy_digest","issued_at","not_before","expires_at",
|
|
"signature_namespace","signature_key_id","canonical_payload_digest",
|
|
}
|
|
PROOF_FIELDS = {
|
|
"schema","proof_id","nonce","parent_delegation_id","parent_delegation_digest",
|
|
"binding_id","binding_digest","repository","task_id","task_identity_sha256","generation",
|
|
"execution_plan_sha256","worker_id","runtime_id","runtime_sha256","heartbeat_at",
|
|
"adapter_readiness","resource_readiness","active_claim_count","max_concurrency",
|
|
"policy_digest","issued_at","expires_at","signature_namespace","signature_key_id",
|
|
"canonical_payload_digest",
|
|
}
|
|
RECOVERY_FIELDS = {
|
|
"schema","decision_id","nonce","parent_delegation_id","parent_delegation_digest",
|
|
"task_id","task_identity_sha256","expected_state","expected_revision","expected_attempt",
|
|
"original_deadline","new_deadline","repository","target_branch","base_oid","generation",
|
|
"execution_plan_sha256","external_job_identity","policy_digest","one_time_use",
|
|
"issued_at","not_before","expires_at","signature_namespace","signature_key_id",
|
|
"canonical_payload_digest",
|
|
}
|
|
|
|
|
|
def verify_delegation_scope(value: Mapping[str, Any], policy: OperationalPolicy, *, now: int | None = None) -> None:
|
|
policy.validate()
|
|
_exact_fields(value, DELEGATION_FIELDS, "OPERATIONAL_DELEGATION_FIELDS_INVALID")
|
|
now = int(time.time()) if now is None else int(now)
|
|
if (
|
|
value["schema"] != OPERATIONAL_DELEGATION_SCHEMA
|
|
or value["status"] != "ACTIVE"
|
|
or value["revocation_epoch"] != 0
|
|
or value["owner_identity"] != OWNER_IDENTITY
|
|
or value["repository"] != policy.repository
|
|
or value["task_id"] != policy.task_id
|
|
or value["task_identity_sha256"] != policy.task_identity_sha256
|
|
or value["target_branch"] != policy.target_branch
|
|
or value["base_oid"] != policy.base_oid
|
|
or value["generation"] != policy.generation
|
|
or value["execution_plan_sha256"] != policy.execution_plan_sha256
|
|
or value["external_job_identity"] != policy.external_job_identity
|
|
or value["policy_digest"] != policy.digest
|
|
or not set(value["allowed_capabilities"]).issubset(set(policy.allowed_capabilities))
|
|
or not set(value["allowed_worker_ids"]).issubset(set(policy.allowed_worker_ids))
|
|
or value["signature_namespace"] != OWNER_NAMESPACE
|
|
or not _verify_digest_field(value)
|
|
or not int(value["not_before"]) <= now < int(value["expires_at"])
|
|
):
|
|
fail("OPERATIONAL_DELEGATION_SCOPE_INVALID")
|
|
|
|
|
|
def verify_binding_scope(binding: Mapping[str, Any], delegation: Mapping[str, Any], policy: OperationalPolicy, *, now: int | None = None) -> None:
|
|
_exact_fields(binding, BINDING_FIELDS, "WORKER_BINDING_FIELDS_INVALID")
|
|
now = int(time.time()) if now is None else int(now)
|
|
expected_parent = hashlib.sha256(canonical_json_bytes(dict(delegation))).hexdigest()
|
|
if (
|
|
binding["schema"] != WORKER_BINDING_SCHEMA
|
|
or binding["parent_delegation_id"] != delegation["delegation_id"]
|
|
or binding["parent_delegation_digest"] != expected_parent
|
|
or binding["repository"] != policy.repository
|
|
or binding["task_id"] != policy.task_id
|
|
or binding["task_identity_sha256"] != policy.task_identity_sha256
|
|
or binding["target_branch"] != policy.target_branch
|
|
or binding["base_oid"] != policy.base_oid
|
|
or binding["generation"] != policy.generation
|
|
or binding["execution_plan_sha256"] != policy.execution_plan_sha256
|
|
or binding["external_job_identity"] != policy.external_job_identity
|
|
or binding["worker_id"] not in policy.allowed_worker_ids
|
|
or binding["policy_digest"] != policy.digest
|
|
or binding["signature_namespace"] != MACHINE_NAMESPACE
|
|
or not _verify_digest_field(binding)
|
|
or not int(binding["not_before"]) <= now < int(binding["expires_at"])
|
|
):
|
|
fail("WORKER_BINDING_SCOPE_INVALID")
|
|
required = set(binding["required_capabilities"])
|
|
worker = set(binding["worker_capabilities"])
|
|
if not required.issubset(worker) or binding["task_contract_version"] not in binding["supported_contract_versions"]:
|
|
fail("WORKER_BINDING_COMPATIBILITY_INVALID")
|
|
|
|
|
|
def verify_proof_scope(proof: Mapping[str, Any], binding: Mapping[str, Any], delegation: Mapping[str, Any], policy: OperationalPolicy, *, now: int | None = None) -> None:
|
|
_exact_fields(proof, PROOF_FIELDS, "WORKER_PROOF_FIELDS_INVALID")
|
|
now = int(time.time()) if now is None else int(now)
|
|
if (
|
|
proof["schema"] != WORKER_PROOF_SCHEMA
|
|
or proof["parent_delegation_id"] != delegation["delegation_id"]
|
|
or proof["parent_delegation_digest"] != hashlib.sha256(canonical_json_bytes(dict(delegation))).hexdigest()
|
|
or proof["binding_id"] != binding["binding_id"]
|
|
or proof["binding_digest"] != hashlib.sha256(canonical_json_bytes(dict(binding))).hexdigest()
|
|
or proof["repository"] != policy.repository
|
|
or proof["task_id"] != policy.task_id
|
|
or proof["task_identity_sha256"] != policy.task_identity_sha256
|
|
or proof["generation"] != policy.generation
|
|
or proof["execution_plan_sha256"] != policy.execution_plan_sha256
|
|
or proof["worker_id"] != binding["worker_id"]
|
|
or proof["runtime_id"] != binding["runtime_id"]
|
|
or proof["runtime_sha256"] != binding["runtime_sha256"]
|
|
or proof["max_concurrency"] != binding["max_concurrency"]
|
|
or proof["policy_digest"] != policy.digest
|
|
or proof["signature_namespace"] != MACHINE_NAMESPACE
|
|
or proof["adapter_readiness"] is not True
|
|
or proof["resource_readiness"] is not True
|
|
or not _verify_digest_field(proof)
|
|
or not now < int(proof["expires_at"])
|
|
or now - int(proof["heartbeat_at"]) > policy.maximum_heartbeat_age_seconds
|
|
or int(proof["active_claim_count"]) >= int(proof["max_concurrency"])
|
|
):
|
|
fail("WORKER_PROOF_SCOPE_INVALID")
|
|
|
|
|
|
def verify_recovery_scope(decision: Mapping[str, Any], delegation: Mapping[str, Any], policy: OperationalPolicy, *, now: int | None = None) -> None:
|
|
_exact_fields(decision, RECOVERY_FIELDS, "RECOVERY_DECISION_FIELDS_INVALID")
|
|
now = int(time.time()) if now is None else int(now)
|
|
if (
|
|
decision["schema"] != RECOVERY_DECISION_SCHEMA
|
|
or decision["parent_delegation_id"] != delegation["delegation_id"]
|
|
or decision["parent_delegation_digest"] != hashlib.sha256(canonical_json_bytes(dict(delegation))).hexdigest()
|
|
or decision["task_id"] != policy.task_id
|
|
or decision["task_identity_sha256"] != policy.task_identity_sha256
|
|
or decision["expected_state"] != "EXPIRED"
|
|
or decision["repository"] != policy.repository
|
|
or decision["target_branch"] != policy.target_branch
|
|
or decision["base_oid"] != policy.base_oid
|
|
or decision["generation"] != policy.generation
|
|
or decision["execution_plan_sha256"] != policy.execution_plan_sha256
|
|
or decision["external_job_identity"] != policy.external_job_identity
|
|
or decision["policy_digest"] != policy.digest
|
|
or decision["one_time_use"] is not True
|
|
or decision["signature_namespace"] != OWNER_NAMESPACE
|
|
or not _verify_digest_field(decision)
|
|
or not int(decision["not_before"]) <= now < int(decision["expires_at"])
|
|
or int(decision["new_deadline"]) <= now
|
|
):
|
|
fail("RECOVERY_DECISION_SCOPE_INVALID")
|
|
|
|
|
|
class OperationalVerifier:
|
|
"""Verify signatures through the public root-owned SignerRegistry."""
|
|
|
|
def __init__(self, registry_path: Path, *, require_root_owner: bool = True):
|
|
self.registry_path = Path(registry_path)
|
|
self.require_root_owner = require_root_owner
|
|
|
|
def _verify(self, artifact: SignedOperationalArtifact | Mapping[str, Any], *, role: str, signer_id: str, namespace: str) -> dict[str, Any]:
|
|
if isinstance(artifact, SignedOperationalArtifact):
|
|
payload, signature, key_id, artifact_namespace = artifact.payload, artifact.signature, artifact.key_id, artifact.namespace
|
|
else:
|
|
_exact_fields(artifact, {"payload","signature","key_id","namespace"}, "SIGNED_ARTIFACT_FIELDS_INVALID")
|
|
payload, signature, key_id, artifact_namespace = artifact["payload"], artifact["signature"], artifact["key_id"], artifact["namespace"]
|
|
if artifact_namespace != namespace:
|
|
fail("SIGNED_ARTIFACT_NAMESPACE_INVALID")
|
|
registry = SignerRegistry(self.registry_path, require_root_owner=self.require_root_owner)
|
|
rec = registry.require(signer_id, role=role)
|
|
if rec.fingerprint != key_id:
|
|
fail("SIGNED_ARTIFACT_KEY_ID_INVALID")
|
|
if role == "MACHINE_OPERATIONAL":
|
|
owner_rec = registry.require(DEFAULT_OWNER_SIGNER_ID, role="OWNER_AUTHORITY")
|
|
if rec.parent_signer_id != owner_rec.signer_id or rec.parent_fingerprint != owner_rec.fingerprint:
|
|
fail("MACHINE_PARENT_BINDING_INVALID")
|
|
# RegistryOwnerApprovalVerifier handles Owner namespace; generic machine uses registry public key.
|
|
if role == "OWNER_AUTHORITY":
|
|
verifier = RegistryOwnerApprovalVerifier(self.registry_path, require_root_owner=self.require_root_owner, signer_id=signer_id)
|
|
ok = verifier.verify(namespace=namespace, payload=canonical_json_bytes(payload), signature=signature, key_id=key_id)
|
|
else:
|
|
from aios_single_host_authority import _load_public_key_line
|
|
pub = _load_public_key_line(rec.public_key)
|
|
ok = verify_sshsig(pub, canonical_json_bytes(payload), signature, namespace=namespace, expected_key_id=key_id)
|
|
if not ok:
|
|
fail("SIGNED_ARTIFACT_SIGNATURE_INVALID")
|
|
return dict(payload)
|
|
|
|
def owner(self, artifact: SignedOperationalArtifact | Mapping[str, Any]) -> dict[str, Any]:
|
|
return self._verify(artifact, role="OWNER_AUTHORITY", signer_id=DEFAULT_OWNER_SIGNER_ID, namespace=OWNER_NAMESPACE)
|
|
|
|
def machine(self, artifact: SignedOperationalArtifact | Mapping[str, Any]) -> dict[str, Any]:
|
|
return self._verify(artifact, role="MACHINE_OPERATIONAL", signer_id=DEFAULT_MACHINE_SIGNER_ID, namespace=MACHINE_NAMESPACE)
|