Files
aios/active-runtime/app/aios_openclaw/contract.py
T

384 lines
13 KiB
Python

"""AIOS 到 OpenClaw 的 Ed25519 执行合同。"""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import uuid
from dataclasses import dataclass, field, replace
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Mapping, Protocol
CONTRACT_VERSION = "aios.openclaw.command.v2"
SIGNATURE_ALGORITHM = "Ed25519"
DEFAULT_CLOCK_SKEW_SECONDS = 300
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def _parse_timestamp(value: str) -> datetime:
normalized = str(value or "").strip()
if not normalized:
raise ValueError("timestamp is required")
if normalized.endswith("Z"):
normalized = normalized[:-1] + "+00:00"
parsed = datetime.fromisoformat(normalized)
if parsed.tzinfo is None:
raise ValueError("timestamp must include a timezone")
return parsed.astimezone(timezone.utc)
def _required(value: str, label: str) -> str:
normalized = str(value or "").strip()
if not normalized:
raise ValueError(f"{label} is required")
return normalized
def _safe_mapping(value: Mapping[str, Any] | None) -> dict[str, Any]:
return json.loads(
json.dumps(dict(value or {}), ensure_ascii=False, sort_keys=True, default=str)
)
def _load_crypto() -> tuple[Any, Any, Any]:
try:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey,
Ed25519PublicKey,
)
except ImportError as exc: # pragma: no cover
raise RuntimeError("cryptography with Ed25519 support is required") from exc
return serialization, Ed25519PrivateKey, Ed25519PublicKey
class CommandSigner(Protocol):
key_id: str
fingerprint: str
def sign(self, payload: bytes) -> bytes: ...
class CommandVerifier(Protocol):
key_id: str
fingerprint: str
def verify(self, payload: bytes, signature: bytes) -> bool: ...
class Ed25519CommandSigner:
def __init__(self, private_key: Any, *, key_id: str) -> None:
serialization, private_cls, _ = _load_crypto()
if not isinstance(private_key, private_cls):
raise ValueError("private key is not Ed25519")
self.private_key = private_key
self.key_id = _required(key_id, "key_id")
raw = private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
self.fingerprint = hashlib.sha256(raw).hexdigest()
@classmethod
def from_bytes(cls, value: bytes | str, *, key_id: str) -> "Ed25519CommandSigner":
serialization, private_cls, _ = _load_crypto()
raw = value.encode("utf-8") if isinstance(value, str) else bytes(value)
if raw.startswith(b"-----BEGIN"):
key = serialization.load_pem_private_key(raw, password=None)
elif len(raw) == 32:
key = private_cls.from_private_bytes(raw)
else:
raise ValueError("Ed25519 private key must be PEM or a raw 32-byte seed")
return cls(key, key_id=key_id)
def sign(self, payload: bytes) -> bytes:
return self.private_key.sign(bytes(payload))
def verifier(self) -> "Ed25519CommandVerifier":
return Ed25519CommandVerifier(self.private_key.public_key(), key_id=self.key_id)
class Ed25519CommandVerifier:
def __init__(self, public_key: Any, *, key_id: str) -> None:
serialization, _, public_cls = _load_crypto()
if not isinstance(public_key, public_cls):
raise ValueError("public key is not Ed25519")
self.public_key = public_key
self.key_id = _required(key_id, "key_id")
raw = public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
self.fingerprint = hashlib.sha256(raw).hexdigest()
@classmethod
def from_bytes(cls, value: bytes | str, *, key_id: str) -> "Ed25519CommandVerifier":
serialization, private_cls, public_cls = _load_crypto()
raw = value.encode("utf-8") if isinstance(value, str) else bytes(value)
if raw.startswith(b"-----BEGIN"):
try:
key = serialization.load_pem_public_key(raw)
except ValueError:
private = serialization.load_pem_private_key(raw, password=None)
if not isinstance(private, private_cls):
raise ValueError("key is not Ed25519")
key = private.public_key()
elif len(raw) == 32:
key = public_cls.from_public_bytes(raw)
else:
raise ValueError("Ed25519 public key must be PEM or raw 32-byte key")
return cls(key, key_id=key_id)
def verify(self, payload: bytes, signature: bytes) -> bool:
try:
self.public_key.verify(bytes(signature), bytes(payload))
return True
except Exception:
return False
class CommandState(str, Enum):
ACCEPTED = "ACCEPTED"
RUNNING = "RUNNING"
COMPLETED = "COMPLETED"
FAILED = "FAILED"
CANCELLED = "CANCELLED"
TIMED_OUT = "TIMED_OUT"
REJECTED = "REJECTED"
@dataclass(frozen=True)
class OpenClawCommand:
command_id: str
tenant_id: str
task_id: str
trace_id: str
action: str
payload: Mapping[str, Any] = field(default_factory=dict)
idempotency_key: str = ""
created_at: str = field(default_factory=utc_now)
expires_at: str | None = None
contract_version: str = CONTRACT_VERSION
signature_algorithm: str = SIGNATURE_ALGORITHM
key_id: str = "default"
public_key_fingerprint: str = ""
signature: str = ""
def __post_init__(self) -> None:
for field_name in (
"command_id",
"tenant_id",
"task_id",
"trace_id",
"action",
"idempotency_key",
"created_at",
"contract_version",
"signature_algorithm",
"key_id",
):
object.__setattr__(
self, field_name, _required(getattr(self, field_name), field_name)
)
if self.contract_version != CONTRACT_VERSION:
raise ValueError(f"unsupported contract_version: {self.contract_version}")
if self.signature_algorithm != SIGNATURE_ALGORITHM:
raise ValueError(
f"unsupported signature_algorithm: {self.signature_algorithm}"
)
object.__setattr__(self, "payload", _safe_mapping(self.payload))
object.__setattr__(
self, "public_key_fingerprint", str(self.public_key_fingerprint or "").strip()
)
object.__setattr__(self, "signature", str(self.signature or "").strip())
_parse_timestamp(self.created_at)
if self.expires_at is not None:
object.__setattr__(self, "expires_at", _required(self.expires_at, "expires_at"))
if _parse_timestamp(self.expires_at) <= _parse_timestamp(self.created_at):
raise ValueError("expires_at must be later than created_at")
@classmethod
def create(
cls,
*,
tenant_id: str,
task_id: str,
trace_id: str,
action: str,
payload: Mapping[str, Any] | None,
idempotency_key: str,
expires_at: str | None = None,
key_id: str = "default",
command_id: str | None = None,
) -> "OpenClawCommand":
return cls(
command_id=command_id or f"cmd_{uuid.uuid4().hex}",
tenant_id=tenant_id,
task_id=task_id,
trace_id=trace_id,
action=action,
payload=_safe_mapping(payload),
idempotency_key=idempotency_key,
expires_at=expires_at,
key_id=key_id,
)
def unsigned_dict(self) -> dict[str, Any]:
return {
"command_id": self.command_id,
"tenant_id": self.tenant_id,
"task_id": self.task_id,
"trace_id": self.trace_id,
"action": self.action,
"payload": _safe_mapping(self.payload),
"idempotency_key": self.idempotency_key,
"created_at": self.created_at,
"expires_at": self.expires_at,
"contract_version": self.contract_version,
"signature_algorithm": self.signature_algorithm,
"key_id": self.key_id,
"public_key_fingerprint": self.public_key_fingerprint,
}
@dataclass(frozen=True)
class OpenClawResult:
command_id: str
tenant_id: str
task_id: str
trace_id: str
state: CommandState
output: Mapping[str, Any] | None = None
error: Mapping[str, Any] | None = None
started_at: str | None = None
finished_at: str | None = None
runtime_id: str | None = None
attempts: int = 1
def __post_init__(self) -> None:
for field_name in ("command_id", "tenant_id", "task_id", "trace_id"):
object.__setattr__(
self, field_name, _required(getattr(self, field_name), field_name)
)
object.__setattr__(self, "state", CommandState(self.state))
if self.output is not None:
object.__setattr__(self, "output", _safe_mapping(self.output))
if self.error is not None:
object.__setattr__(self, "error", _safe_mapping(self.error))
if self.started_at is not None:
_parse_timestamp(self.started_at)
if self.finished_at is not None:
_parse_timestamp(self.finished_at)
if self.attempts < 1:
raise ValueError("attempts must be >= 1")
@dataclass(frozen=True)
class RuntimeHealth:
runtime: str
runtime_id: str
status: str
version: str
heartbeat_at: str
active_tasks: int = 0
workspace_isolated: bool = False
credential_ref_configured: bool = False
details: Mapping[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
for field_name in (
"runtime",
"runtime_id",
"status",
"version",
"heartbeat_at",
):
object.__setattr__(
self, field_name, _required(getattr(self, field_name), field_name)
)
_parse_timestamp(self.heartbeat_at)
if self.active_tasks < 0:
raise ValueError("active_tasks must be >= 0")
object.__setattr__(self, "details", _safe_mapping(self.details))
def canonical_command_bytes(command: OpenClawCommand) -> bytes:
return json.dumps(
command.unsigned_dict(),
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
def _as_signer(command: OpenClawCommand, value: Any) -> CommandSigner:
if hasattr(value, "sign") and hasattr(value, "fingerprint") and hasattr(value, "key_id"):
return value
return Ed25519CommandSigner.from_bytes(value, key_id=command.key_id)
def _as_verifier(command: OpenClawCommand, value: Any) -> CommandVerifier:
if isinstance(value, Ed25519CommandSigner):
return value.verifier()
if hasattr(value, "verify") and hasattr(value, "fingerprint") and hasattr(value, "key_id"):
return value
return Ed25519CommandVerifier.from_bytes(value, key_id=command.key_id)
def sign_command(command: OpenClawCommand, signing_key: Any) -> OpenClawCommand:
signer = _as_signer(command, signing_key)
if signer.key_id != command.key_id:
raise ValueError("command key_id does not match signer")
prepared = replace(
command,
signature_algorithm=SIGNATURE_ALGORITHM,
public_key_fingerprint=signer.fingerprint,
signature="",
)
signature = base64.b64encode(
signer.sign(canonical_command_bytes(prepared))
).decode("ascii")
return replace(prepared, signature=signature)
def verify_command_signature(
command: OpenClawCommand,
verification_key: Any,
*,
now: datetime | None = None,
clock_skew_seconds: int = DEFAULT_CLOCK_SKEW_SECONDS,
) -> bool:
if (
not command.signature
or command.contract_version != CONTRACT_VERSION
or command.signature_algorithm != SIGNATURE_ALGORITHM
):
return False
try:
verifier = _as_verifier(command, verification_key)
signature = base64.b64decode(command.signature, validate=True)
except Exception:
return False
if verifier.key_id != command.key_id:
return False
if not command.public_key_fingerprint or not hmac.compare_digest(
command.public_key_fingerprint, verifier.fingerprint
):
return False
if not verifier.verify(canonical_command_bytes(command), signature):
return False
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
created = _parse_timestamp(command.created_at)
if created.timestamp() - current.timestamp() > clock_skew_seconds:
return False
if command.expires_at is not None:
expires = _parse_timestamp(command.expires_at)
if current.timestamp() - expires.timestamp() > clock_skew_seconds:
return False
return True