Files
aios/active-runtime/app/aios_single_host_authority.py
T

1539 lines
52 KiB
Python

"""Single-host Owner authority and machine signer for AIOS.
This module deliberately removes ambient signer discovery from the authority
path. Runtime code addresses signers by stable signer_id in a root-owned
registry; private key paths are implementation details of the local authority
service and never appear in that registry.
Threat model
============
* One physical/logical AIOS server is the host trust anchor.
* Host root compromise is therefore a trust-root compromise. This module does
not pretend otherwise when no TPM/HSM/external signer exists.
* Agent, Worker, Bridge, Task context, Git and logs must never contain private
key bytes.
* The Owner key may sign only structured AIOS authorization objects. There is
no arbitrary-byte signing API exposed by ``LocalOwnerAuthority``.
* High-frequency operational capabilities are signed by a distinct machine key
and must monotonically narrow an already-valid OwnerDelegationEpoch.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
import base64
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import secrets
import stat
import struct
import tempfile
import time
from typing import Any, Mapping, Sequence
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey,
Ed25519PublicKey,
)
OWNER_NAMESPACE = "aios-owner-approval-v1"
MACHINE_NAMESPACE = "aios-machine-capability-v1"
OWNER_IDENTITY = "aios-task-grant-signer"
REGISTRY_SCHEMA = "aios.single-host-signer-registry.v1"
POLICY_SCHEMA = "aios.single-host-owner-policy.v1"
AUTHORIZATION_BUNDLE_SCHEMA = "aios.owner-authorization-bundle.v1"
MACHINE_CAPABILITY_SCHEMA = "aios.machine-derived-capability.v1"
BOOTSTRAP_STATE_SCHEMA = "aios.single-host-bootstrap-state.v1"
OWNER_EPOCH_SCHEMA = "aios.owner-delegation-epoch.v2"
DEFAULT_REGISTRY_PATH = Path("/etc/aios/single-host-signer-registry.json")
DEFAULT_OWNER_SIGNER_ID = "local-owner-authority"
DEFAULT_MACHINE_SIGNER_ID = "machine-operational"
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
_CAP = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/-]{2,255}$")
_RISK = {"low": 0, "medium": 1, "high": 2}
_MAX_JSON = 128 * 1024
_MAX_KEY = 64 * 1024
_MAGIC = b"SSHSIG"
_VERSION = 1
_HASH_ALG = "sha512"
class SingleHostAuthorityError(RuntimeError):
"""Stable fail-closed authority error."""
def __init__(self, code: str) -> None:
super().__init__(code)
self.code = code
def _fail(code: str) -> None:
raise SingleHostAuthorityError(code)
def canonical_json_bytes(value: Any) -> bytes:
return json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
def sha256_json(value: Any) -> str:
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
def _ssh_string(value: bytes) -> bytes:
return struct.pack(">I", len(value)) + value
def _read_ssh_string(blob: bytes, offset: int) -> tuple[bytes, int]:
if offset + 4 > len(blob):
_fail("SSHSIG_TRUNCATED")
length = struct.unpack(">I", blob[offset : offset + 4])[0]
offset += 4
if length > len(blob) - offset:
_fail("SSHSIG_TRUNCATED")
return blob[offset : offset + length], offset + length
def _public_blob(public_key: Ed25519PublicKey) -> bytes:
raw = public_key.public_bytes(
serialization.Encoding.Raw,
serialization.PublicFormat.Raw,
)
return _ssh_string(b"ssh-ed25519") + _ssh_string(raw)
def ssh_public_line(public_key: Ed25519PublicKey, comment: str = "") -> str:
blob = _public_blob(public_key)
suffix = f" {comment}" if comment else ""
return f"ssh-ed25519 {base64.b64encode(blob).decode('ascii')}{suffix}"
def ssh_fingerprint(public_key: Ed25519PublicKey) -> str:
digest = hashlib.sha256(_public_blob(public_key)).digest()
return "SHA256:" + base64.b64encode(digest).decode("ascii").rstrip("=")
def _signed_data(namespace: str, payload: bytes, *, hash_algorithm: str) -> bytes:
if not namespace or any(ord(ch) < 32 or ord(ch) == 127 for ch in namespace):
_fail("SIGNATURE_NAMESPACE_INVALID")
if hash_algorithm not in {"sha256", "sha512"}:
_fail("SIGNATURE_HASH_ALGORITHM_INVALID")
hashed = hashlib.new(hash_algorithm, payload).digest()
return (
_MAGIC
+ _ssh_string(namespace.encode("utf-8"))
+ _ssh_string(b"")
+ _ssh_string(hash_algorithm.encode("ascii"))
+ _ssh_string(hashed)
)
def sign_sshsig(
private_key: Ed25519PrivateKey,
payload: bytes,
*,
namespace: str,
hash_algorithm: str = _HASH_ALG,
) -> str:
"""Return an OpenSSH-compatible armored detached SSHSIG."""
public_key = private_key.public_key()
signed = _signed_data(namespace, payload, hash_algorithm=hash_algorithm)
raw_signature = private_key.sign(signed)
signature_blob = _ssh_string(b"ssh-ed25519") + _ssh_string(raw_signature)
envelope = (
_MAGIC
+ struct.pack(">I", _VERSION)
+ _ssh_string(_public_blob(public_key))
+ _ssh_string(namespace.encode("utf-8"))
+ _ssh_string(b"")
+ _ssh_string(hash_algorithm.encode("ascii"))
+ _ssh_string(signature_blob)
)
encoded = base64.b64encode(envelope).decode("ascii")
lines = [encoded[i : i + 76] for i in range(0, len(encoded), 76)]
return (
"-----BEGIN SSH SIGNATURE-----\n"
+ "\n".join(lines)
+ "\n-----END SSH SIGNATURE-----\n"
)
def _parse_public_blob(blob: bytes) -> Ed25519PublicKey:
algorithm, offset = _read_ssh_string(blob, 0)
raw, offset = _read_ssh_string(blob, offset)
if offset != len(blob) or algorithm != b"ssh-ed25519" or len(raw) != 32:
_fail("SSHSIG_PUBLIC_KEY_INVALID")
try:
return Ed25519PublicKey.from_public_bytes(raw)
except ValueError as exc:
raise SingleHostAuthorityError("SSHSIG_PUBLIC_KEY_INVALID") from exc
def verify_sshsig(
public_key: Ed25519PublicKey,
payload: bytes,
signature: str,
*,
namespace: str,
expected_key_id: str,
) -> bool:
"""Strictly verify one armored Ed25519 SSHSIG."""
try:
if not signature.startswith("-----BEGIN SSH SIGNATURE-----\n"):
return False
if not signature.endswith("-----END SSH SIGNATURE-----\n"):
return False
middle = signature[
len("-----BEGIN SSH SIGNATURE-----\n") :
-len("-----END SSH SIGNATURE-----\n")
]
compact = "".join(middle.splitlines())
if not compact or any(ch.isspace() for ch in compact):
return False
envelope = base64.b64decode(compact, validate=True)
if not envelope.startswith(_MAGIC) or len(envelope) < 10:
return False
offset = len(_MAGIC)
version = struct.unpack(">I", envelope[offset : offset + 4])[0]
offset += 4
if version != _VERSION:
return False
public_blob, offset = _read_ssh_string(envelope, offset)
envelope_namespace, offset = _read_ssh_string(envelope, offset)
reserved, offset = _read_ssh_string(envelope, offset)
hash_algorithm, offset = _read_ssh_string(envelope, offset)
signature_blob, offset = _read_ssh_string(envelope, offset)
if offset != len(envelope):
return False
if reserved != b"":
return False
if envelope_namespace.decode("utf-8") != namespace:
return False
hash_name = hash_algorithm.decode("ascii")
if hash_name not in {"sha256", "sha512"}:
return False
embedded = _parse_public_blob(public_blob)
embedded_raw = embedded.public_bytes(
serialization.Encoding.Raw, serialization.PublicFormat.Raw
)
expected_raw = public_key.public_bytes(
serialization.Encoding.Raw, serialization.PublicFormat.Raw
)
if not secrets.compare_digest(embedded_raw, expected_raw):
return False
if ssh_fingerprint(public_key) != expected_key_id:
return False
algorithm, sig_offset = _read_ssh_string(signature_blob, 0)
raw_signature, sig_offset = _read_ssh_string(signature_blob, sig_offset)
if (
sig_offset != len(signature_blob)
or algorithm != b"ssh-ed25519"
or len(raw_signature) != 64
):
return False
public_key.verify(
raw_signature,
_signed_data(namespace, payload, hash_algorithm=hash_name),
)
return True
except (
SingleHostAuthorityError,
ValueError,
UnicodeDecodeError,
base64.binascii.Error,
):
return False
except Exception:
# cryptography InvalidSignature deliberately maps to fail-closed False
return False
def _strict_json_object(raw: bytes, *, maximum: int = _MAX_JSON) -> dict[str, Any]:
if not raw or len(raw) > maximum:
_fail("JSON_INPUT_INVALID")
def hook(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
value: dict[str, Any] = {}
for key, item in pairs:
if key in value:
_fail("JSON_DUPLICATE_FIELD")
value[key] = item
return value
try:
value = json.loads(
raw.decode("utf-8"),
object_pairs_hook=hook,
parse_constant=lambda _value: _fail("JSON_CONSTANT_INVALID"),
)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise SingleHostAuthorityError("JSON_INPUT_INVALID") from exc
if not isinstance(value, dict):
_fail("JSON_OBJECT_REQUIRED")
return value
def _open_secure(
path: Path,
*,
private: bool,
require_root_owner: bool,
maximum: int,
) -> bytes:
path = Path(path)
if not path.is_absolute():
_fail("SECURE_PATH_NOT_ABSOLUTE")
flags = os.O_RDONLY
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
try:
fd = os.open(path, flags)
except OSError as exc:
raise SingleHostAuthorityError("SECURE_FILE_OPEN_FAILED") from exc
try:
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode):
_fail("SECURE_FILE_NOT_REGULAR")
if os.name == "posix":
mode = stat.S_IMODE(info.st_mode)
if mode & 0o022:
_fail("SECURE_FILE_WRITABLE_BY_GROUP_OR_OTHER")
if private and mode & 0o077:
_fail("SECURE_PRIVATE_FILE_PERMISSIONS_INVALID")
if require_root_owner and info.st_uid != 0:
_fail("SECURE_FILE_NOT_ROOT_OWNED")
data = bytearray()
while len(data) <= maximum:
chunk = os.read(fd, min(65536, maximum + 1 - len(data)))
if not chunk:
break
data.extend(chunk)
if not data or len(data) > maximum:
_fail("SECURE_FILE_SIZE_INVALID")
return bytes(data)
finally:
os.close(fd)
def _ensure_secure_directory(
path: Path,
*,
require_root_owner: bool,
create: bool,
private: bool = True,
) -> None:
path = Path(path)
if not path.is_absolute():
_fail("SECURE_DIRECTORY_NOT_ABSOLUTE")
if create and not path.exists():
mode = 0o700 if private else 0o755
try:
path.mkdir(parents=True, mode=mode)
except OSError as exc:
raise SingleHostAuthorityError("SECURE_DIRECTORY_CREATE_FAILED") from exc
if os.name == "posix":
os.chmod(path, mode)
try:
info = path.lstat()
except OSError as exc:
raise SingleHostAuthorityError("SECURE_DIRECTORY_MISSING") from exc
if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode):
_fail("SECURE_DIRECTORY_INVALID")
if os.name == "posix":
mode = stat.S_IMODE(info.st_mode)
if (private and mode & 0o077) or ((not private) and mode & 0o022):
_fail("SECURE_DIRECTORY_PERMISSIONS_INVALID")
if require_root_owner and info.st_uid != 0:
_fail("SECURE_DIRECTORY_NOT_ROOT_OWNED")
def _atomic_write(
path: Path,
data: bytes,
*,
mode: int,
require_root_owner: bool,
create_only: bool,
private_parent: bool = True,
) -> None:
path = Path(path)
_ensure_secure_directory(
path.parent,
require_root_owner=require_root_owner,
create=True,
private=private_parent,
)
fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
temp = Path(temp_name)
try:
os.fchmod(fd, mode)
view = memoryview(data)
while view:
written = os.write(fd, view)
view = view[written:]
os.fsync(fd)
os.close(fd)
fd = -1
if create_only and path.exists():
_fail("SECURE_FILE_ALREADY_EXISTS")
os.replace(temp, path)
directory_fd = os.open(path.parent, os.O_RDONLY)
try:
os.fsync(directory_fd)
finally:
os.close(directory_fd)
finally:
if fd >= 0:
os.close(fd)
try:
temp.unlink()
except FileNotFoundError:
pass
def _load_private_key(
path: Path,
*,
require_root_owner: bool,
) -> Ed25519PrivateKey:
raw = _open_secure(
path,
private=True,
require_root_owner=require_root_owner,
maximum=_MAX_KEY,
)
try:
key = serialization.load_pem_private_key(raw, password=None)
except (TypeError, ValueError) as exc:
raise SingleHostAuthorityError("PRIVATE_KEY_INVALID") from exc
if not isinstance(key, Ed25519PrivateKey):
_fail("PRIVATE_KEY_ALGORITHM_INVALID")
return key
def _load_public_key_line(value: str) -> Ed25519PublicKey:
parts = value.strip().split()
if len(parts) < 2 or parts[0] != "ssh-ed25519":
_fail("PUBLIC_KEY_INVALID")
try:
blob = base64.b64decode(parts[1], validate=True)
except ValueError as exc:
raise SingleHostAuthorityError("PUBLIC_KEY_INVALID") from exc
return _parse_public_blob(blob)
@dataclass(frozen=True, slots=True)
class SignerRecord:
signer_id: str
role: str
provider: str
fingerprint: str
public_key: str
socket: str
secret_ref: str
status: str
generation: int
parent_signer_id: str = ""
parent_fingerprint: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SignerRegistry:
"""Root-owned public signer registry. Private paths are forbidden."""
def __init__(
self,
path: Path,
*,
require_root_owner: bool = True,
) -> None:
self.path = Path(path)
raw = _open_secure(
self.path,
private=False,
require_root_owner=require_root_owner,
maximum=_MAX_JSON,
)
value = _strict_json_object(raw)
if set(value) != {"schema", "authority_generation", "signers"}:
_fail("SIGNER_REGISTRY_FIELDS_INVALID")
if value["schema"] != REGISTRY_SCHEMA:
_fail("SIGNER_REGISTRY_SCHEMA_INVALID")
if type(value["authority_generation"]) is not int or value["authority_generation"] < 1:
_fail("SIGNER_REGISTRY_GENERATION_INVALID")
if not isinstance(value["signers"], list) or not value["signers"]:
_fail("SIGNER_REGISTRY_EMPTY")
self.authority_generation = value["authority_generation"]
self._records: dict[str, SignerRecord] = {}
allowed = set(SignerRecord.__dataclass_fields__)
for item in value["signers"]:
if not isinstance(item, Mapping) or set(item) != allowed:
_fail("SIGNER_REGISTRY_RECORD_INVALID")
if any(
key in item
for key in ("private_key", "private_key_path", "passphrase", "password")
):
_fail("SIGNER_REGISTRY_SECRET_MATERIAL_FORBIDDEN")
try:
record = SignerRecord(**dict(item))
except TypeError as exc:
raise SingleHostAuthorityError("SIGNER_REGISTRY_RECORD_INVALID") from exc
if (
record.signer_id in self._records
or not _ID.fullmatch(record.signer_id)
or record.role not in {"OWNER_AUTHORITY", "MACHINE_OPERATIONAL"}
or record.provider != "local_unix_service"
or record.status != "ACTIVE"
or type(record.generation) is not int
or record.generation < 1
or not record.socket.startswith("/run/")
or not record.secret_ref.startswith("aios-secret://")
):
_fail("SIGNER_REGISTRY_RECORD_INVALID")
public = _load_public_key_line(record.public_key)
if ssh_fingerprint(public) != record.fingerprint:
_fail("SIGNER_REGISTRY_FINGERPRINT_MISMATCH")
self._records[record.signer_id] = record
def require(self, signer_id: str, *, role: str) -> SignerRecord:
record = self._records.get(signer_id)
if record is None or record.role != role or record.status != "ACTIVE":
_fail("SIGNER_NOT_ACTIVE")
return record
def records(self) -> tuple[SignerRecord, ...]:
return tuple(self._records[key] for key in sorted(self._records))
@dataclass(frozen=True, slots=True)
class SingleHostOwnerPolicy:
repository: str
trust_domain: str
allowed_branch_prefixes: tuple[str, ...]
allowed_capabilities: frozenset[str]
allowed_file_prefixes: tuple[str, ...]
risk_ceiling: str
production_scope: str
destructive_policy: str
maximum_epoch_ttl_seconds: int
maximum_machine_capability_ttl_seconds: int
allowed_caller_uids: tuple[int, ...]
schema: str = POLICY_SCHEMA
@classmethod
def from_path(
cls,
path: Path,
*,
require_root_owner: bool = True,
) -> "SingleHostOwnerPolicy":
value = _strict_json_object(
_open_secure(
Path(path),
private=False,
require_root_owner=require_root_owner,
maximum=_MAX_JSON,
)
)
fields = set(cls.__dataclass_fields__)
if set(value) != fields:
_fail("OWNER_POLICY_FIELDS_INVALID")
if value.get("schema") != POLICY_SCHEMA:
_fail("OWNER_POLICY_SCHEMA_INVALID")
list_fields = (
"allowed_branch_prefixes",
"allowed_capabilities",
"allowed_file_prefixes",
"allowed_caller_uids",
)
if any(not isinstance(value[name], list) for name in list_fields):
_fail("OWNER_POLICY_FIELDS_INVALID")
try:
policy = cls(
repository=value["repository"],
trust_domain=value["trust_domain"],
allowed_branch_prefixes=tuple(value["allowed_branch_prefixes"]),
allowed_capabilities=frozenset(value["allowed_capabilities"]),
allowed_file_prefixes=tuple(value["allowed_file_prefixes"]),
risk_ceiling=value["risk_ceiling"],
production_scope=value["production_scope"],
destructive_policy=value["destructive_policy"],
maximum_epoch_ttl_seconds=value["maximum_epoch_ttl_seconds"],
maximum_machine_capability_ttl_seconds=value[
"maximum_machine_capability_ttl_seconds"
],
allowed_caller_uids=tuple(value["allowed_caller_uids"]),
schema=value["schema"],
)
except (KeyError, TypeError) as exc:
raise SingleHostAuthorityError("OWNER_POLICY_FIELDS_INVALID") from exc
policy.validate()
return policy
def validate(self) -> None:
if (
self.schema != POLICY_SCHEMA
or not self.repository
or not self.trust_domain
or not self.allowed_branch_prefixes
or not self.allowed_capabilities
or not self.allowed_file_prefixes
or self.risk_ceiling not in _RISK
or not self.production_scope
or self.destructive_policy not in {"forbidden", "rollback-only"}
or type(self.maximum_epoch_ttl_seconds) is not int
or not 3600 <= self.maximum_epoch_ttl_seconds <= 366 * 24 * 3600
or type(self.maximum_machine_capability_ttl_seconds) is not int
or not 60 <= self.maximum_machine_capability_ttl_seconds <= 3600
or not self.allowed_caller_uids
or any(type(uid) is not int or uid < 0 for uid in self.allowed_caller_uids)
or any(not _CAP.fullmatch(item) for item in self.allowed_capabilities)
or any(
not item.startswith("agent/")
or "\\" in item
or ".." in PurePosixPath(item).parts
for item in self.allowed_branch_prefixes
)
or any(
PurePosixPath(item).is_absolute()
or "\\" in item
or ".." in PurePosixPath(item).parts
or item.startswith(".git")
for item in self.allowed_file_prefixes
)
):
_fail("OWNER_POLICY_INVALID")
def to_dict(self) -> dict[str, Any]:
return {
"repository": self.repository,
"trust_domain": self.trust_domain,
"allowed_branch_prefixes": list(self.allowed_branch_prefixes),
"allowed_capabilities": sorted(self.allowed_capabilities),
"allowed_file_prefixes": list(self.allowed_file_prefixes),
"risk_ceiling": self.risk_ceiling,
"production_scope": self.production_scope,
"destructive_policy": self.destructive_policy,
"maximum_epoch_ttl_seconds": self.maximum_epoch_ttl_seconds,
"maximum_machine_capability_ttl_seconds": self.maximum_machine_capability_ttl_seconds,
"allowed_caller_uids": list(self.allowed_caller_uids),
"schema": self.schema,
}
@property
def digest(self) -> str:
return sha256_json(self.to_dict())
@dataclass(frozen=True, slots=True)
class SignedArtifact:
payload: Mapping[str, Any]
canonical_payload: bytes
signature: str
key_id: str
namespace: str
@property
def payload_sha256(self) -> str:
return hashlib.sha256(self.canonical_payload).hexdigest()
@dataclass(frozen=True, slots=True)
class OwnerEpochRequest:
epoch_id: str
allowed_branch_prefixes: tuple[str, ...]
allowed_capabilities: frozenset[str]
allowed_file_prefixes: tuple[str, ...]
risk_ceiling: str
business_goal: str
production_scope: str
destructive_policy: str
ttl_seconds: int
@dataclass(frozen=True, slots=True)
class OwnerAuthorizationBundle:
bundle_id: str
nonce: str
repository: str
trust_domain: str
policy_digest: str
release_epoch_digest: str
action_epoch_digest: str
bootstrap_digest: str
issued_at: int
not_before: int
expires_at: int
revocation_epoch: int
status: str
schema: str = AUTHORIZATION_BUNDLE_SCHEMA
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True, slots=True)
class MachineCapabilityRequest:
capability_id: str
capability: str
resource_kind: str
resource: str
task_id: str
release_session_id: str
generation: int
risk: str
ttl_seconds: int
nonce: str
class LocalFileSigner:
def __init__(
self,
key_path: Path,
*,
signer_id: str,
namespace: str,
require_root_owner: bool = True,
) -> None:
self.key_path = Path(key_path)
self.signer_id = signer_id
self.namespace = namespace
self.require_root_owner = require_root_owner
def _key(self) -> Ed25519PrivateKey:
return _load_private_key(
self.key_path,
require_root_owner=self.require_root_owner,
)
@property
def public_key(self) -> Ed25519PublicKey:
return self._key().public_key()
@property
def fingerprint(self) -> str:
return ssh_fingerprint(self.public_key)
def _sign_canonical(self, value: Mapping[str, Any]) -> SignedArtifact:
payload = canonical_json_bytes(dict(value))
key = self._key()
signature = sign_sshsig(
key,
payload,
namespace=self.namespace,
)
return SignedArtifact(
payload=dict(value),
canonical_payload=payload,
signature=signature,
key_id=ssh_fingerprint(key.public_key()),
namespace=self.namespace,
)
class LocalOwnerAuthority(LocalFileSigner):
"""Structured Owner signer; arbitrary-byte signing is intentionally absent."""
def __init__(
self,
key_path: Path,
policy: SingleHostOwnerPolicy,
*,
require_root_owner: bool = True,
clock: Any = time.time,
) -> None:
super().__init__(
key_path,
signer_id=DEFAULT_OWNER_SIGNER_ID,
namespace=OWNER_NAMESPACE,
require_root_owner=require_root_owner,
)
self.policy = policy
self.clock = clock
@staticmethod
def _prefix_narrows(requested: str, allowed: Sequence[str]) -> bool:
base = requested.rstrip("/")
return any(
base == prefix.rstrip("/")
or base.startswith(prefix.rstrip("/") + "/")
or requested.startswith(prefix)
for prefix in allowed
)
def issue_epoch(self, request: OwnerEpochRequest) -> SignedArtifact:
now = int(self.clock())
if (
not _ID.fullmatch(request.epoch_id)
or not request.allowed_capabilities
or not request.allowed_branch_prefixes
or not request.allowed_file_prefixes
or not request.allowed_capabilities.issubset(self.policy.allowed_capabilities)
or any(
not self._prefix_narrows(item, self.policy.allowed_branch_prefixes)
for item in request.allowed_branch_prefixes
)
or any(
not self._prefix_narrows(item, self.policy.allowed_file_prefixes)
for item in request.allowed_file_prefixes
)
or request.risk_ceiling not in _RISK
or _RISK[request.risk_ceiling] > _RISK[self.policy.risk_ceiling]
or request.production_scope != self.policy.production_scope
or request.destructive_policy != self.policy.destructive_policy
or type(request.ttl_seconds) is not int
or not 60 <= request.ttl_seconds <= self.policy.maximum_epoch_ttl_seconds
or not request.business_goal
):
_fail("OWNER_EPOCH_REQUEST_WIDENS_POLICY")
value: dict[str, Any] = {
"epoch_id": request.epoch_id,
"owner_identity": OWNER_IDENTITY,
"trust_domain": self.policy.trust_domain,
"repository": self.policy.repository,
"allowed_branch_prefixes": list(request.allowed_branch_prefixes),
"allowed_capabilities": sorted(request.allowed_capabilities),
"allowed_file_prefixes": list(request.allowed_file_prefixes),
"risk_ceiling": request.risk_ceiling,
"issued_at": now,
"not_before": max(0, now - 30),
"expires_at": now + request.ttl_seconds,
"revocation_epoch": 0,
"policy_digest": self.policy.digest,
"business_goal": request.business_goal,
"production_scope": request.production_scope,
"destructive_policy": request.destructive_policy,
"signature_namespace": OWNER_NAMESPACE,
"signature_key_id": self.fingerprint,
"status": "ACTIVE",
"schema": OWNER_EPOCH_SCHEMA,
}
value["canonical_payload_digest"] = sha256_json(value)
return self._sign_canonical(value)
def issue_authorization_bundle(
self,
*,
bundle_id: str,
nonce: str,
release_epoch_digest: str,
action_epoch_digest: str,
bootstrap_digest: str,
ttl_seconds: int,
) -> SignedArtifact:
now = int(self.clock())
if (
not _ID.fullmatch(bundle_id)
or not _ID.fullmatch(nonce)
or any(
not _HEX64.fullmatch(item)
for item in (
release_epoch_digest,
action_epoch_digest,
bootstrap_digest,
)
)
or type(ttl_seconds) is not int
or not 60 <= ttl_seconds <= self.policy.maximum_epoch_ttl_seconds
):
_fail("OWNER_AUTHORIZATION_BUNDLE_INVALID")
bundle = OwnerAuthorizationBundle(
bundle_id=bundle_id,
nonce=nonce,
repository=self.policy.repository,
trust_domain=self.policy.trust_domain,
policy_digest=self.policy.digest,
release_epoch_digest=release_epoch_digest,
action_epoch_digest=action_epoch_digest,
bootstrap_digest=bootstrap_digest,
issued_at=now,
not_before=max(0, now - 30),
expires_at=now + ttl_seconds,
revocation_epoch=0,
status="ACTIVE",
)
return self._sign_canonical(bundle.to_dict())
class RegistryOwnerApprovalVerifier:
"""Verifier compatible with current Task admission verifier dependency."""
def __init__(
self,
registry_path: Path,
*,
require_root_owner: bool = True,
signer_id: str = DEFAULT_OWNER_SIGNER_ID,
) -> None:
self.registry_path = Path(registry_path)
self.require_root_owner = require_root_owner
self.signer_id = signer_id
def verify(
self,
*,
namespace: str,
payload: bytes,
signature: str,
key_id: str,
) -> bool:
if namespace != OWNER_NAMESPACE:
return False
try:
registry = SignerRegistry(
self.registry_path,
require_root_owner=self.require_root_owner,
)
record = registry.require(self.signer_id, role="OWNER_AUTHORITY")
if record.fingerprint != key_id:
return False
public = _load_public_key_line(record.public_key)
return verify_sshsig(
public,
payload,
signature,
namespace=namespace,
expected_key_id=key_id,
)
except SingleHostAuthorityError:
return False
class GrantRootOwnerApprovalVerifier(RegistryOwnerApprovalVerifier):
"""Zero-argument live adapter loaded by Task admission.
The registry path is a fixed machine contract, not an environment variable.
"""
def __init__(self) -> None:
super().__init__(
DEFAULT_REGISTRY_PATH,
require_root_owner=True,
signer_id=DEFAULT_OWNER_SIGNER_ID,
)
class MachineOperationalSigner(LocalFileSigner):
def __init__(
self,
key_path: Path,
*,
require_root_owner: bool = True,
clock: Any = time.time,
) -> None:
super().__init__(
key_path,
signer_id=DEFAULT_MACHINE_SIGNER_ID,
namespace=MACHINE_NAMESPACE,
require_root_owner=require_root_owner,
)
self.clock = clock
def derive_capability(
self,
parent_epoch: Mapping[str, Any],
request: MachineCapabilityRequest,
*,
maximum_ttl_seconds: int,
) -> SignedArtifact:
now = int(self.clock())
required = {
"epoch_id", "owner_identity", "trust_domain", "repository",
"allowed_branch_prefixes", "allowed_capabilities",
"allowed_file_prefixes", "risk_ceiling", "issued_at", "not_before",
"expires_at", "revocation_epoch", "policy_digest", "business_goal",
"production_scope", "destructive_policy", "signature_namespace",
"signature_key_id", "canonical_payload_digest", "status", "schema",
}
if not isinstance(parent_epoch, Mapping) or set(parent_epoch) != required:
_fail("MACHINE_PARENT_EPOCH_INVALID")
expected_digest = dict(parent_epoch)
supplied = expected_digest.pop("canonical_payload_digest")
if (
parent_epoch["schema"] != OWNER_EPOCH_SCHEMA
or parent_epoch["status"] != "ACTIVE"
or parent_epoch["revocation_epoch"] != 0
or supplied != sha256_json(expected_digest)
or not int(parent_epoch["not_before"]) <= now < int(parent_epoch["expires_at"])
):
_fail("MACHINE_PARENT_EPOCH_INVALID")
if (
not _ID.fullmatch(request.capability_id)
or not _CAP.fullmatch(request.capability)
or request.capability not in parent_epoch["allowed_capabilities"]
or request.resource_kind not in {"branch", "file", "production", "action"}
or not request.resource
or request.risk not in _RISK
or _RISK[request.risk] > _RISK[parent_epoch["risk_ceiling"]]
or type(request.generation) is not int
or request.generation < 1
or type(request.ttl_seconds) is not int
or not 30 <= request.ttl_seconds <= maximum_ttl_seconds
or now + request.ttl_seconds > int(parent_epoch["expires_at"])
or not _ID.fullmatch(request.nonce)
):
_fail("MACHINE_CAPABILITY_REQUEST_INVALID")
if request.resource_kind == "branch":
if not LocalOwnerAuthority._prefix_narrows(
request.resource,
parent_epoch["allowed_branch_prefixes"],
):
_fail("MACHINE_CAPABILITY_SCOPE_WIDENING")
elif request.resource_kind == "file":
if not LocalOwnerAuthority._prefix_narrows(
request.resource,
parent_epoch["allowed_file_prefixes"],
):
_fail("MACHINE_CAPABILITY_SCOPE_WIDENING")
else:
if request.resource != parent_epoch["production_scope"]:
_fail("MACHINE_CAPABILITY_SCOPE_WIDENING")
value = {
"schema": MACHINE_CAPABILITY_SCHEMA,
"capability_id": request.capability_id,
"parent_epoch_id": parent_epoch["epoch_id"],
"parent_epoch_digest": hashlib.sha256(
canonical_json_bytes(dict(parent_epoch))
).hexdigest(),
"signer_id": self.signer_id,
"repository": parent_epoch["repository"],
"trust_domain": parent_epoch["trust_domain"],
"capability": request.capability,
"resource_kind": request.resource_kind,
"resource": request.resource,
"task_id": request.task_id,
"release_session_id": request.release_session_id,
"generation": request.generation,
"risk": request.risk,
"policy_digest": parent_epoch["policy_digest"],
"issued_at": now,
"expires_at": now + request.ttl_seconds,
"nonce": request.nonce,
}
return self._sign_canonical(value)
@dataclass(frozen=True, slots=True)
class ReadinessResult:
ready: bool
blockers: tuple[str, ...]
owner_key_id: str
machine_key_id: str
registry_generation: int
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SignerReadinessGate:
def __init__(
self,
registry: SignerRegistry,
owner: LocalOwnerAuthority,
machine: MachineOperationalSigner,
) -> None:
self.registry = registry
self.owner = owner
self.machine = machine
def evaluate(self) -> ReadinessResult:
blockers: list[str] = []
owner_record: SignerRecord | None = None
machine_record: SignerRecord | None = None
try:
owner_record = self.registry.require(
DEFAULT_OWNER_SIGNER_ID,
role="OWNER_AUTHORITY",
)
except SingleHostAuthorityError:
blockers.append("OWNER_SIGNER_REGISTRY_MISSING")
try:
machine_record = self.registry.require(
DEFAULT_MACHINE_SIGNER_ID,
role="MACHINE_OPERATIONAL",
)
except SingleHostAuthorityError:
blockers.append("MACHINE_SIGNER_REGISTRY_MISSING")
owner_fp = ""
machine_fp = ""
try:
owner_fp = self.owner.fingerprint
except SingleHostAuthorityError:
blockers.append("OWNER_SIGNER_KEY_UNAVAILABLE")
try:
machine_fp = self.machine.fingerprint
except SingleHostAuthorityError:
blockers.append("MACHINE_SIGNER_KEY_UNAVAILABLE")
if owner_record is not None and owner_fp and owner_record.fingerprint != owner_fp:
blockers.append("OWNER_SIGNER_IDENTITY_DRIFT")
if machine_record is not None and machine_fp and machine_record.fingerprint != machine_fp:
blockers.append("MACHINE_SIGNER_IDENTITY_DRIFT")
if owner_fp and machine_fp and owner_fp == machine_fp:
blockers.append("OWNER_MACHINE_KEY_REUSE_FORBIDDEN")
if (
owner_record is not None
and machine_record is not None
and (
machine_record.parent_signer_id != owner_record.signer_id
or machine_record.parent_fingerprint != owner_record.fingerprint
)
):
blockers.append("MACHINE_PARENT_BINDING_INVALID")
challenge = canonical_json_bytes(
{
"schema": "aios.signer-readiness-challenge.v1",
"registry_generation": self.registry.authority_generation,
"nonce": "readiness-" + sha256_json(
{"owner": owner_fp, "machine": machine_fp}
)[:32],
}
)
if owner_record is not None and owner_fp:
try:
signature = sign_sshsig(
self.owner._key(),
challenge,
namespace=OWNER_NAMESPACE,
)
if not verify_sshsig(
_load_public_key_line(owner_record.public_key),
challenge,
signature,
namespace=OWNER_NAMESPACE,
expected_key_id=owner_record.fingerprint,
):
blockers.append("OWNER_SIGNER_CHALLENGE_FAILED")
except SingleHostAuthorityError:
blockers.append("OWNER_SIGNER_CHALLENGE_FAILED")
if machine_record is not None and machine_fp:
try:
signature = sign_sshsig(
self.machine._key(),
challenge,
namespace=MACHINE_NAMESPACE,
)
if not verify_sshsig(
_load_public_key_line(machine_record.public_key),
challenge,
signature,
namespace=MACHINE_NAMESPACE,
expected_key_id=machine_record.fingerprint,
):
blockers.append("MACHINE_SIGNER_CHALLENGE_FAILED")
except SingleHostAuthorityError:
blockers.append("MACHINE_SIGNER_CHALLENGE_FAILED")
ordered = tuple(dict.fromkeys(blockers))
return ReadinessResult(
ready=not ordered,
blockers=ordered,
owner_key_id=owner_fp,
machine_key_id=machine_fp,
registry_generation=self.registry.authority_generation,
)
class BootstrapRetirementStore:
def __init__(
self,
path: Path,
*,
require_root_owner: bool = True,
clock: Any = time.time,
) -> None:
self.path = Path(path)
self.require_root_owner = require_root_owner
self.clock = clock
def state(self) -> dict[str, Any]:
if not self.path.exists():
return {
"schema": BOOTSTRAP_STATE_SCHEMA,
"retired": False,
"consumed_generation": 0,
"retired_at": 0,
"receipt_digest": "",
}
return _strict_json_object(
_open_secure(
self.path,
private=False,
require_root_owner=self.require_root_owner,
maximum=_MAX_JSON,
)
)
def retire(self, *, generation: int, receipt_digest: str) -> dict[str, Any]:
if type(generation) is not int or generation < 1 or not _HEX64.fullmatch(receipt_digest):
_fail("BOOTSTRAP_RETIREMENT_INPUT_INVALID")
current = self.state()
expected_fields = {
"schema", "retired", "consumed_generation", "retired_at",
"receipt_digest",
}
if set(current) != expected_fields or current.get("schema") != BOOTSTRAP_STATE_SCHEMA:
_fail("BOOTSTRAP_STATE_INVALID")
if current["retired"]:
if (
current["consumed_generation"] == generation
and current["receipt_digest"] == receipt_digest
):
return current
_fail("BOOTSTRAP_ALREADY_RETIRED")
value = {
"schema": BOOTSTRAP_STATE_SCHEMA,
"retired": True,
"consumed_generation": generation,
"retired_at": int(self.clock()),
"receipt_digest": receipt_digest,
}
_atomic_write(
self.path,
canonical_json_bytes(value),
mode=0o600,
require_root_owner=self.require_root_owner,
create_only=False,
)
readback = self.state()
if readback != value:
_fail("BOOTSTRAP_RETIREMENT_READBACK_MISMATCH")
return value
def assert_legacy_allowed(self) -> None:
if self.state().get("retired") is True:
_fail("LEGACY_BOOTSTRAP_RETIRED")
def bootstrap_key(
path: Path,
*,
require_root_owner: bool = True,
) -> Ed25519PublicKey:
"""Create one Ed25519 key exactly once, or validate the existing key."""
path = Path(path)
if path.exists():
return _load_private_key(
path,
require_root_owner=require_root_owner,
).public_key()
_ensure_secure_directory(
path.parent,
require_root_owner=require_root_owner,
create=True,
)
key = Ed25519PrivateKey.generate()
pem = key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
)
_atomic_write(
path,
pem,
mode=0o600,
require_root_owner=require_root_owner,
create_only=True,
private_parent=True,
)
loaded = _load_private_key(
path,
require_root_owner=require_root_owner,
)
if loaded.public_key().public_bytes(
serialization.Encoding.Raw,
serialization.PublicFormat.Raw,
) != key.public_key().public_bytes(
serialization.Encoding.Raw,
serialization.PublicFormat.Raw,
):
_fail("KEY_BOOTSTRAP_READBACK_MISMATCH")
return loaded.public_key()
def write_signer_registry(
path: Path,
*,
owner_public_key: Ed25519PublicKey,
machine_public_key: Ed25519PublicKey,
authority_generation: int,
owner_socket: str = "/run/aios/owner-authority.sock",
machine_socket: str = "/run/aios/machine-signer.sock",
require_root_owner: bool = True,
) -> None:
if (
type(authority_generation) is not int
or authority_generation < 1
or ssh_fingerprint(owner_public_key) == ssh_fingerprint(machine_public_key)
):
_fail("SIGNER_REGISTRY_BOOTSTRAP_INVALID")
owner_record = SignerRecord(
signer_id=DEFAULT_OWNER_SIGNER_ID,
role="OWNER_AUTHORITY",
provider="local_unix_service",
fingerprint=ssh_fingerprint(owner_public_key),
public_key=ssh_public_line(owner_public_key, DEFAULT_OWNER_SIGNER_ID),
socket=owner_socket,
secret_ref="aios-secret://single-host/local-owner-authority",
status="ACTIVE",
generation=authority_generation,
)
machine_record = SignerRecord(
signer_id=DEFAULT_MACHINE_SIGNER_ID,
role="MACHINE_OPERATIONAL",
provider="local_unix_service",
fingerprint=ssh_fingerprint(machine_public_key),
public_key=ssh_public_line(machine_public_key, DEFAULT_MACHINE_SIGNER_ID),
socket=machine_socket,
secret_ref="aios-secret://single-host/machine-operational",
status="ACTIVE",
generation=authority_generation,
parent_signer_id=owner_record.signer_id,
parent_fingerprint=owner_record.fingerprint,
)
value = {
"schema": REGISTRY_SCHEMA,
"authority_generation": authority_generation,
"signers": [owner_record.to_dict(), machine_record.to_dict()],
}
path = Path(path)
desired = canonical_json_bytes(value)
if path.exists():
existing = _open_secure(
path,
private=False,
require_root_owner=require_root_owner,
maximum=_MAX_JSON,
)
if existing != desired:
_fail("SIGNER_REGISTRY_EXISTING_MISMATCH")
return
_atomic_write(
path,
desired,
mode=0o644,
require_root_owner=require_root_owner,
create_only=True,
private_parent=False,
)
def install_owner_policy(
source: Path,
destination: Path,
*,
expected_policy_digest: str,
require_root_owner: bool = True,
) -> SingleHostOwnerPolicy:
"""Install one exact policy once; never silently replace an existing policy."""
source = Path(source)
try:
raw = source.read_bytes()
except OSError as exc:
raise SingleHostAuthorityError("OWNER_POLICY_SOURCE_UNAVAILABLE") from exc
value = _strict_json_object(raw)
# Validate through a temporary private file so policy parsing uses the same
# implementation as production while allowing an explicit candidate source.
with tempfile.TemporaryDirectory() as directory:
temp_root = Path(directory)
if os.name == "posix":
os.chmod(temp_root, 0o700)
temp_policy = temp_root / "policy.json"
temp_policy.write_bytes(canonical_json_bytes(value))
if os.name == "posix":
os.chmod(temp_policy, 0o644)
candidate = SingleHostOwnerPolicy.from_path(
temp_policy,
require_root_owner=False,
)
if candidate.digest != expected_policy_digest:
_fail("OWNER_POLICY_DIGEST_MISMATCH")
destination = Path(destination)
desired = canonical_json_bytes(candidate.to_dict())
if destination.exists():
existing = _open_secure(
destination,
private=False,
require_root_owner=require_root_owner,
maximum=_MAX_JSON,
)
if existing != desired:
_fail("OWNER_POLICY_EXISTING_MISMATCH")
return SingleHostOwnerPolicy.from_path(
destination,
require_root_owner=require_root_owner,
)
_atomic_write(
destination,
desired,
mode=0o644,
require_root_owner=require_root_owner,
create_only=True,
private_parent=False,
)
return SingleHostOwnerPolicy.from_path(
destination,
require_root_owner=require_root_owner,
)
@dataclass(frozen=True, slots=True)
class SingleHostBootstrapResult:
owner_key_id: str
machine_key_id: str
policy_digest: str
registry_generation: int
readiness: Mapping[str, Any]
def to_dict(self) -> dict[str, Any]:
return {
"owner_key_id": self.owner_key_id,
"machine_key_id": self.machine_key_id,
"policy_digest": self.policy_digest,
"registry_generation": self.registry_generation,
"readiness": dict(self.readiness),
"private_key_exported": False,
"ambient_signer_dependencies": False,
}
def bootstrap_single_host_authority(
*,
owner_state_directory: Path,
machine_state_directory: Path,
registry_path: Path,
policy_source: Path,
policy_destination: Path,
expected_policy_digest: str,
authority_generation: int,
require_root_owner: bool = True,
) -> SingleHostBootstrapResult:
"""Create/validate local authority state and prove readiness.
The operation is idempotent for exact existing state. Any identity or policy
mismatch fails closed instead of rotating keys during a release.
"""
if not _HEX64.fullmatch(expected_policy_digest):
_fail("OWNER_POLICY_DIGEST_INVALID")
policy = install_owner_policy(
policy_source,
policy_destination,
expected_policy_digest=expected_policy_digest,
require_root_owner=require_root_owner,
)
owner_key_path = Path(owner_state_directory) / "owner-authority.key"
machine_key_path = Path(machine_state_directory) / "machine-operational.key"
owner_public = bootstrap_key(
owner_key_path,
require_root_owner=require_root_owner,
)
machine_public = bootstrap_key(
machine_key_path,
require_root_owner=require_root_owner,
)
write_signer_registry(
registry_path,
owner_public_key=owner_public,
machine_public_key=machine_public,
authority_generation=authority_generation,
require_root_owner=require_root_owner,
)
registry = SignerRegistry(
registry_path,
require_root_owner=require_root_owner,
)
owner = LocalOwnerAuthority(
owner_key_path,
policy,
require_root_owner=require_root_owner,
)
machine = MachineOperationalSigner(
machine_key_path,
require_root_owner=require_root_owner,
)
readiness = SignerReadinessGate(registry, owner, machine).evaluate()
if not readiness.ready:
_fail("SIGNER_READINESS_FAILED:" + ",".join(readiness.blockers))
return SingleHostBootstrapResult(
owner_key_id=readiness.owner_key_id,
machine_key_id=readiness.machine_key_id,
policy_digest=policy.digest,
registry_generation=readiness.registry_generation,
readiness=readiness.to_dict(),
)
def write_signed_epoch_directory(
root: Path,
artifact: SignedArtifact,
*,
require_root_owner: bool = True,
) -> None:
"""Write the exact files already consumed by SignedEpochDirectoryProvider."""
root = Path(root)
_ensure_secure_directory(
root,
require_root_owner=require_root_owner,
create=True,
private=False,
)
for name, data, mode in (
("owner-delegation-epoch.canonical.json", artifact.canonical_payload, 0o644),
("owner-delegation-epoch.sig", artifact.signature.encode("utf-8"), 0o644),
("owner-delegation-epoch.key-id", (artifact.key_id + "\n").encode("utf-8"), 0o644),
):
_atomic_write(
root / name,
data,
mode=mode,
require_root_owner=require_root_owner,
create_only=False,
private_parent=False,
)
def module_has_ambient_signer_dependencies() -> bool:
"""Static self-check used by readiness/tests."""
forbidden = {
"SSH_AUTH_SOCK",
"USERPROFILE",
"HOME",
"ssh-agent",
"ssh-add",
}
source = Path(__file__).read_text(encoding="utf-8")
# Ignore this function's own literal names by limiting the scan to the
# portion before its definition.
source = source.split("def module_has_ambient_signer_dependencies", 1)[0]
return any(item in source for item in forbidden)