832 lines
31 KiB
Python
832 lines
31 KiB
Python
"""AIOS V2 可信 Run Provenance 签发、持久化与受控写入。
|
|
|
|
生产策略要求 Provenance 同时覆盖七类路径:代码变更、Git 操作、部署、
|
|
Runtime 执行、Evidence 写入、回滚和审计。任何缺失、篡改、过期、撤销、
|
|
重放或上下文不匹配都默认拒绝。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
import stat
|
|
import threading
|
|
import uuid
|
|
from dataclasses import asdict, dataclass, field, replace
|
|
from datetime import datetime, timedelta, timezone
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from typing import Any, Mapping, Protocol, Sequence
|
|
|
|
PROVENANCE_VERSION = "aios.v2.run-provenance.v1"
|
|
PRODUCTION_POLICY_VERSION = "aios.v2.production.v1"
|
|
|
|
|
|
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 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 canonical_json_bytes(value: Mapping[str, Any]) -> bytes:
|
|
return json.dumps(
|
|
dict(value),
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
default=str,
|
|
).encode("utf-8")
|
|
|
|
|
|
def digest_payload(value: Mapping[str, Any]) -> str:
|
|
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
|
|
|
|
|
|
def _validate_digest(value: str) -> str:
|
|
normalized = required(value, "request_digest").lower()
|
|
if len(normalized) != 64 or any(ch not in "0123456789abcdef" for ch in normalized):
|
|
raise ValueError("request_digest must be a SHA-256 hexadecimal digest")
|
|
return normalized
|
|
|
|
|
|
class ExecutionPath(str, Enum):
|
|
CODE_CHANGE = "code_change"
|
|
GIT_OPERATION = "git_operation"
|
|
DEPLOYMENT = "deployment"
|
|
RUNTIME_EXECUTE = "runtime_execute"
|
|
EVIDENCE_WRITE = "evidence_write"
|
|
ROLLBACK = "rollback"
|
|
AUDIT = "audit"
|
|
|
|
|
|
REQUIRED_PRODUCTION_PATHS = frozenset(item.value for item in ExecutionPath)
|
|
|
|
|
|
class GitOperation(str, Enum):
|
|
READ_REF = "read_ref"
|
|
FETCH = "fetch"
|
|
CREATE_BRANCH = "create_branch"
|
|
COMMIT = "commit"
|
|
MERGE = "merge"
|
|
PUSH = "push"
|
|
UPDATE_EVIDENCE = "update_evidence"
|
|
|
|
|
|
class ProvenanceStatus(str, Enum):
|
|
ISSUED = "issued"
|
|
VERIFIED = "verified"
|
|
USED = "used"
|
|
EXPIRED = "expired"
|
|
REVOKED = "revoked"
|
|
|
|
|
|
class ProvenanceError(RuntimeError):
|
|
"""Run Provenance 基础错误。"""
|
|
|
|
|
|
class ProvenanceDenied(ProvenanceError):
|
|
"""策略拒绝当前操作。"""
|
|
|
|
|
|
class ProvenanceNotFound(ProvenanceDenied):
|
|
"""未找到 Provenance。"""
|
|
|
|
|
|
class ProvenanceSignatureError(ProvenanceDenied):
|
|
"""签名无效。"""
|
|
|
|
|
|
class ProvenanceExpired(ProvenanceDenied):
|
|
"""Provenance 已过期。"""
|
|
|
|
|
|
class ProvenanceRevoked(ProvenanceDenied):
|
|
"""Provenance 已撤销。"""
|
|
|
|
|
|
class ProvenanceReplay(ProvenanceDenied):
|
|
"""检测到相同授权摘要重放。"""
|
|
|
|
|
|
class Signer(Protocol):
|
|
key_id: str
|
|
|
|
def sign(self, payload: bytes) -> str: ...
|
|
|
|
|
|
class SignatureVerifier(Protocol):
|
|
key_id: str
|
|
fingerprint: str
|
|
|
|
def verify(self, payload: bytes, signature: str) -> bool: ...
|
|
|
|
|
|
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 - depends on deployment image
|
|
raise RuntimeError(
|
|
"cryptography package with Ed25519 support is required"
|
|
) from exc
|
|
return serialization, Ed25519PrivateKey, Ed25519PublicKey
|
|
|
|
|
|
def _check_private_key_file(path: Path) -> bytes:
|
|
resolved = path.resolve()
|
|
metadata = resolved.stat()
|
|
if not stat.S_ISREG(metadata.st_mode):
|
|
raise RuntimeError("private key reference is not a regular file")
|
|
if metadata.st_mode & 0o077:
|
|
raise RuntimeError("private key file permissions must be 0600 or stricter")
|
|
return resolved.read_bytes()
|
|
|
|
|
|
def _check_public_key_file(path: Path) -> bytes:
|
|
resolved = path.resolve()
|
|
metadata = resolved.stat()
|
|
if not stat.S_ISREG(metadata.st_mode):
|
|
raise RuntimeError("public key reference is not a regular file")
|
|
if metadata.st_mode & 0o022:
|
|
raise RuntimeError("public key file must not be group/world writable")
|
|
return resolved.read_bytes()
|
|
|
|
|
|
class Ed25519PrivateKeySigner:
|
|
"""Ed25519 Owner/Control Plane 签发器。"""
|
|
|
|
def __init__(self, private_key: Any, *, key_id: str) -> None:
|
|
self.private_key = private_key
|
|
self.key_id = required(key_id, "key_id")
|
|
|
|
@classmethod
|
|
def from_file(cls, path: str | Path, *, key_id: str) -> "Ed25519PrivateKeySigner":
|
|
serialization, private_cls, _ = _load_crypto()
|
|
value = _check_private_key_file(Path(path))
|
|
if value.startswith(b"-----BEGIN"):
|
|
key = serialization.load_pem_private_key(value, password=None)
|
|
elif len(value) == 32:
|
|
key = private_cls.from_private_bytes(value)
|
|
else:
|
|
raise RuntimeError("private key must be PEM or a raw 32-byte Ed25519 seed")
|
|
if not isinstance(key, private_cls):
|
|
raise RuntimeError("private key is not Ed25519")
|
|
return cls(key, key_id=key_id)
|
|
|
|
def sign(self, payload: bytes) -> str:
|
|
return base64.b64encode(self.private_key.sign(bytes(payload))).decode("ascii")
|
|
|
|
def public_verifier(self) -> "Ed25519PublicKeyVerifier":
|
|
return Ed25519PublicKeyVerifier(self.private_key.public_key(), key_id=self.key_id)
|
|
|
|
|
|
class Ed25519PublicKeyVerifier:
|
|
"""Ed25519 签名验证器。"""
|
|
|
|
def __init__(self, public_key: Any, *, key_id: str) -> None:
|
|
serialization, _, public_cls = _load_crypto()
|
|
if not isinstance(public_key, public_cls):
|
|
raise RuntimeError("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_file(cls, path: str | Path, *, key_id: str) -> "Ed25519PublicKeyVerifier":
|
|
serialization, _, public_cls = _load_crypto()
|
|
value = _check_public_key_file(Path(path))
|
|
if value.startswith(b"-----BEGIN"):
|
|
key = serialization.load_pem_public_key(value)
|
|
elif len(value) == 32:
|
|
key = public_cls.from_public_bytes(value)
|
|
else:
|
|
raise RuntimeError("public key must be PEM or raw 32-byte Ed25519 key")
|
|
if not isinstance(key, public_cls):
|
|
raise RuntimeError("public key is not Ed25519")
|
|
return cls(key, key_id=key_id)
|
|
|
|
def verify(self, payload: bytes, signature: str) -> bool:
|
|
try:
|
|
decoded = base64.b64decode(required(signature, "signature"), validate=True)
|
|
self.public_key.verify(decoded, bytes(payload))
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RunProvenance:
|
|
provenance_id: str
|
|
run_id: str
|
|
issuer: str
|
|
tenant_id: str
|
|
task_id: str
|
|
agent_id: str
|
|
principal_id: str
|
|
allowed_paths: Sequence[str]
|
|
allowed_git_operations: Sequence[str]
|
|
issued_at: str
|
|
expires_at: str
|
|
policy_version: str
|
|
key_id: str
|
|
nonce: str
|
|
signature: str = ""
|
|
schema_version: str = PROVENANCE_VERSION
|
|
|
|
def __post_init__(self) -> None:
|
|
for name in (
|
|
"provenance_id",
|
|
"run_id",
|
|
"issuer",
|
|
"tenant_id",
|
|
"task_id",
|
|
"agent_id",
|
|
"principal_id",
|
|
"issued_at",
|
|
"expires_at",
|
|
"policy_version",
|
|
"key_id",
|
|
"nonce",
|
|
"schema_version",
|
|
):
|
|
object.__setattr__(self, name, required(getattr(self, name), name))
|
|
if self.schema_version != PROVENANCE_VERSION:
|
|
raise ValueError(f"unsupported schema_version: {self.schema_version}")
|
|
issued = parse_timestamp(self.issued_at)
|
|
expires = parse_timestamp(self.expires_at)
|
|
if expires <= issued:
|
|
raise ValueError("expires_at must be later than issued_at")
|
|
paths = tuple(sorted({ExecutionPath(item).value for item in self.allowed_paths}))
|
|
operations = tuple(
|
|
sorted({GitOperation(item).value for item in self.allowed_git_operations})
|
|
)
|
|
object.__setattr__(self, "allowed_paths", paths)
|
|
object.__setattr__(self, "allowed_git_operations", operations)
|
|
object.__setattr__(self, "signature", str(self.signature or "").strip())
|
|
|
|
def unsigned_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"schema_version": self.schema_version,
|
|
"provenance_id": self.provenance_id,
|
|
"run_id": self.run_id,
|
|
"issuer": self.issuer,
|
|
"tenant_id": self.tenant_id,
|
|
"task_id": self.task_id,
|
|
"agent_id": self.agent_id,
|
|
"principal_id": self.principal_id,
|
|
"allowed_paths": list(self.allowed_paths),
|
|
"allowed_git_operations": list(self.allowed_git_operations),
|
|
"issued_at": self.issued_at,
|
|
"expires_at": self.expires_at,
|
|
"policy_version": self.policy_version,
|
|
"key_id": self.key_id,
|
|
"nonce": self.nonce,
|
|
}
|
|
|
|
def canonical_bytes(self) -> bytes:
|
|
return canonical_json_bytes(self.unsigned_dict())
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
value = self.unsigned_dict()
|
|
value["signature"] = self.signature
|
|
return value
|
|
|
|
@classmethod
|
|
def from_dict(cls, value: Mapping[str, Any]) -> "RunProvenance":
|
|
return cls(**dict(value))
|
|
|
|
|
|
class ProvenanceIssuer:
|
|
"""只由 Owner 或 AIOS Control Plane 持有的签发服务。"""
|
|
|
|
def __init__(self, signer: Signer, store: "SQLiteProvenanceStore") -> None:
|
|
self.signer = signer
|
|
self.store = store
|
|
|
|
def issue(
|
|
self,
|
|
*,
|
|
run_id: str,
|
|
issuer: str,
|
|
tenant_id: str,
|
|
task_id: str,
|
|
agent_id: str,
|
|
principal_id: str,
|
|
allowed_paths: Sequence[str] = tuple(REQUIRED_PRODUCTION_PATHS),
|
|
allowed_git_operations: Sequence[str] = tuple(item.value for item in GitOperation),
|
|
ttl_seconds: int = 3600,
|
|
policy_version: str = PRODUCTION_POLICY_VERSION,
|
|
provenance_id: str | None = None,
|
|
nonce: str | None = None,
|
|
now: datetime | None = None,
|
|
) -> RunProvenance:
|
|
if ttl_seconds <= 0:
|
|
raise ValueError("ttl_seconds must be positive")
|
|
normalized_paths = frozenset(ExecutionPath(item).value for item in allowed_paths)
|
|
if policy_version == PRODUCTION_POLICY_VERSION and normalized_paths != REQUIRED_PRODUCTION_PATHS:
|
|
missing = sorted(REQUIRED_PRODUCTION_PATHS - normalized_paths)
|
|
extra = sorted(normalized_paths - REQUIRED_PRODUCTION_PATHS)
|
|
raise ProvenanceDenied(
|
|
f"production provenance must cover exactly seven paths; missing={missing}, extra={extra}"
|
|
)
|
|
normalized_operations = tuple(
|
|
sorted({GitOperation(item).value for item in allowed_git_operations})
|
|
)
|
|
if ExecutionPath.GIT_OPERATION.value in normalized_paths and not normalized_operations:
|
|
raise ProvenanceDenied("git_operation path requires explicit allowed Git operations")
|
|
issued = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
|
|
unsigned = RunProvenance(
|
|
provenance_id=provenance_id or f"prov_{uuid.uuid4().hex}",
|
|
run_id=run_id,
|
|
issuer=issuer,
|
|
tenant_id=tenant_id,
|
|
task_id=task_id,
|
|
agent_id=agent_id,
|
|
principal_id=principal_id,
|
|
allowed_paths=tuple(normalized_paths),
|
|
allowed_git_operations=normalized_operations,
|
|
issued_at=issued.isoformat().replace("+00:00", "Z"),
|
|
expires_at=(issued + timedelta(seconds=ttl_seconds))
|
|
.isoformat()
|
|
.replace("+00:00", "Z"),
|
|
policy_version=policy_version,
|
|
key_id=self.signer.key_id,
|
|
nonce=nonce or uuid.uuid4().hex,
|
|
)
|
|
signed = replace(unsigned, signature=self.signer.sign(unsigned.canonical_bytes()))
|
|
self.store.save_issued(signed)
|
|
return signed
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProvenanceAuthorization:
|
|
authorization_id: str
|
|
provenance_id: str
|
|
run_id: str
|
|
tenant_id: str
|
|
task_id: str
|
|
agent_id: str
|
|
principal_id: str
|
|
execution_path: str
|
|
git_operation: str | None
|
|
request_digest: str
|
|
policy_version: str
|
|
key_id: str
|
|
public_key_fingerprint: str
|
|
authorized_at: str = field(default_factory=utc_now)
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
|
|
class SQLiteProvenanceStore:
|
|
"""持久化 Provenance、状态、授权使用和不可变事件。"""
|
|
|
|
def __init__(self, path: str | Path) -> None:
|
|
self.path = Path(path).resolve()
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
self._lock = threading.RLock()
|
|
self._connection = sqlite3.connect(
|
|
str(self.path), timeout=30.0, check_same_thread=False
|
|
)
|
|
self._connection.row_factory = sqlite3.Row
|
|
with self._lock:
|
|
self._connection.execute("PRAGMA journal_mode=WAL")
|
|
self._connection.execute("PRAGMA synchronous=FULL")
|
|
self._connection.execute("PRAGMA foreign_keys=ON")
|
|
self._connection.executescript(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS provenance_records (
|
|
provenance_id TEXT PRIMARY KEY,
|
|
payload_json TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
signature_verified INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS provenance_usage (
|
|
authorization_id TEXT PRIMARY KEY,
|
|
provenance_id TEXT NOT NULL,
|
|
execution_path TEXT NOT NULL,
|
|
git_operation TEXT NOT NULL DEFAULT '',
|
|
request_digest TEXT NOT NULL,
|
|
authorized_at TEXT NOT NULL,
|
|
receipt_json TEXT NOT NULL,
|
|
UNIQUE(provenance_id, execution_path, git_operation, request_digest),
|
|
FOREIGN KEY(provenance_id) REFERENCES provenance_records(provenance_id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS provenance_events (
|
|
event_id TEXT PRIMARY KEY,
|
|
provenance_id TEXT NOT NULL,
|
|
event_type TEXT NOT NULL,
|
|
details_json TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
FOREIGN KEY(provenance_id) REFERENCES provenance_records(provenance_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_provenance_usage_id
|
|
ON provenance_usage(provenance_id, authorized_at);
|
|
CREATE INDEX IF NOT EXISTS idx_provenance_events_id
|
|
ON provenance_events(provenance_id, created_at);
|
|
"""
|
|
)
|
|
self._connection.commit()
|
|
|
|
def close(self) -> None:
|
|
with self._lock:
|
|
self._connection.close()
|
|
|
|
def _event(
|
|
self,
|
|
provenance_id: str,
|
|
event_type: str,
|
|
details: Mapping[str, Any] | None = None,
|
|
) -> None:
|
|
self._connection.execute(
|
|
"INSERT INTO provenance_events VALUES (?, ?, ?, ?, ?)",
|
|
(
|
|
f"pevt_{uuid.uuid4().hex}",
|
|
provenance_id,
|
|
event_type,
|
|
json.dumps(dict(details or {}), ensure_ascii=False, sort_keys=True),
|
|
utc_now(),
|
|
),
|
|
)
|
|
|
|
def save_issued(self, provenance: RunProvenance) -> None:
|
|
payload = json.dumps(
|
|
provenance.as_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
)
|
|
now = utc_now()
|
|
with self._lock:
|
|
self._connection.execute("BEGIN IMMEDIATE")
|
|
try:
|
|
prior = self._connection.execute(
|
|
"SELECT payload_json FROM provenance_records WHERE provenance_id = ?",
|
|
(provenance.provenance_id,),
|
|
).fetchone()
|
|
if prior is not None:
|
|
if prior["payload_json"] != payload:
|
|
raise ProvenanceDenied("provenance_id reused with different payload")
|
|
self._connection.rollback()
|
|
return
|
|
self._connection.execute(
|
|
"INSERT INTO provenance_records VALUES (?, ?, ?, 0, ?, ?)",
|
|
(
|
|
provenance.provenance_id,
|
|
payload,
|
|
ProvenanceStatus.ISSUED.value,
|
|
now,
|
|
now,
|
|
),
|
|
)
|
|
self._event(provenance.provenance_id, "issued")
|
|
self._connection.commit()
|
|
except Exception:
|
|
self._connection.rollback()
|
|
raise
|
|
|
|
def get(self, provenance_id: str) -> RunProvenance:
|
|
with self._lock:
|
|
row = self._connection.execute(
|
|
"SELECT payload_json FROM provenance_records WHERE provenance_id = ?",
|
|
(required(provenance_id, "provenance_id"),),
|
|
).fetchone()
|
|
if row is None:
|
|
raise ProvenanceNotFound("provenance not found")
|
|
value = json.loads(row["payload_json"])
|
|
if not isinstance(value, Mapping):
|
|
raise ProvenanceError("stored provenance payload is invalid")
|
|
return RunProvenance.from_dict(value)
|
|
|
|
def status(self, provenance_id: str) -> ProvenanceStatus:
|
|
with self._lock:
|
|
row = self._connection.execute(
|
|
"SELECT status FROM provenance_records WHERE provenance_id = ?",
|
|
(required(provenance_id, "provenance_id"),),
|
|
).fetchone()
|
|
if row is None:
|
|
raise ProvenanceNotFound("provenance not found")
|
|
return ProvenanceStatus(row["status"])
|
|
|
|
def mark_verified(self, provenance_id: str, *, fingerprint: str) -> None:
|
|
with self._lock:
|
|
self._connection.execute("BEGIN IMMEDIATE")
|
|
try:
|
|
current = self.status(provenance_id)
|
|
if current in {ProvenanceStatus.REVOKED, ProvenanceStatus.EXPIRED}:
|
|
raise ProvenanceDenied(f"cannot verify provenance in state {current.value}")
|
|
next_status = (
|
|
ProvenanceStatus.USED.value
|
|
if current == ProvenanceStatus.USED
|
|
else ProvenanceStatus.VERIFIED.value
|
|
)
|
|
self._connection.execute(
|
|
"UPDATE provenance_records SET status = ?, signature_verified = 1, updated_at = ? WHERE provenance_id = ?",
|
|
(next_status, utc_now(), provenance_id),
|
|
)
|
|
self._event(
|
|
provenance_id,
|
|
"verified",
|
|
{"public_key_fingerprint": fingerprint},
|
|
)
|
|
self._connection.commit()
|
|
except Exception:
|
|
self._connection.rollback()
|
|
raise
|
|
|
|
def mark_expired(self, provenance_id: str) -> None:
|
|
with self._lock:
|
|
self._connection.execute("BEGIN IMMEDIATE")
|
|
try:
|
|
current = self.status(provenance_id)
|
|
if current == ProvenanceStatus.REVOKED:
|
|
self._connection.rollback()
|
|
return
|
|
self._connection.execute(
|
|
"UPDATE provenance_records SET status = ?, updated_at = ? WHERE provenance_id = ?",
|
|
(ProvenanceStatus.EXPIRED.value, utc_now(), provenance_id),
|
|
)
|
|
self._event(provenance_id, "expired")
|
|
self._connection.commit()
|
|
except Exception:
|
|
self._connection.rollback()
|
|
raise
|
|
|
|
def revoke(self, provenance_id: str, *, reason: str, principal_id: str) -> None:
|
|
with self._lock:
|
|
self._connection.execute("BEGIN IMMEDIATE")
|
|
try:
|
|
self.get(provenance_id)
|
|
self._connection.execute(
|
|
"UPDATE provenance_records SET status = ?, updated_at = ? WHERE provenance_id = ?",
|
|
(ProvenanceStatus.REVOKED.value, utc_now(), provenance_id),
|
|
)
|
|
self._event(
|
|
provenance_id,
|
|
"revoked",
|
|
{
|
|
"reason": required(reason, "reason"),
|
|
"principal_id": required(principal_id, "principal_id"),
|
|
},
|
|
)
|
|
self._connection.commit()
|
|
except Exception:
|
|
self._connection.rollback()
|
|
raise
|
|
|
|
def record_authorization(self, authorization: ProvenanceAuthorization) -> None:
|
|
receipt = json.dumps(
|
|
authorization.as_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
)
|
|
with self._lock:
|
|
self._connection.execute("BEGIN IMMEDIATE")
|
|
try:
|
|
current = self.status(authorization.provenance_id)
|
|
if current in {ProvenanceStatus.REVOKED, ProvenanceStatus.EXPIRED}:
|
|
raise ProvenanceDenied(f"provenance state is {current.value}")
|
|
try:
|
|
self._connection.execute(
|
|
"INSERT INTO provenance_usage VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
(
|
|
authorization.authorization_id,
|
|
authorization.provenance_id,
|
|
authorization.execution_path,
|
|
authorization.git_operation or "",
|
|
authorization.request_digest,
|
|
authorization.authorized_at,
|
|
receipt,
|
|
),
|
|
)
|
|
except sqlite3.IntegrityError as exc:
|
|
raise ProvenanceReplay(
|
|
"identical provenance path/operation/request digest already used"
|
|
) from exc
|
|
self._connection.execute(
|
|
"UPDATE provenance_records SET status = ?, updated_at = ? WHERE provenance_id = ?",
|
|
(
|
|
ProvenanceStatus.USED.value,
|
|
authorization.authorized_at,
|
|
authorization.provenance_id,
|
|
),
|
|
)
|
|
self._event(
|
|
authorization.provenance_id,
|
|
"used",
|
|
{
|
|
"authorization_id": authorization.authorization_id,
|
|
"execution_path": authorization.execution_path,
|
|
"git_operation": authorization.git_operation,
|
|
"request_digest": authorization.request_digest,
|
|
},
|
|
)
|
|
self._connection.commit()
|
|
except Exception:
|
|
self._connection.rollback()
|
|
raise
|
|
|
|
def authorization_count(self, provenance_id: str) -> int:
|
|
with self._lock:
|
|
row = self._connection.execute(
|
|
"SELECT COUNT(*) AS total FROM provenance_usage WHERE provenance_id = ?",
|
|
(required(provenance_id, "provenance_id"),),
|
|
).fetchone()
|
|
return int(row["total"])
|
|
|
|
def events(self, provenance_id: str) -> list[dict[str, Any]]:
|
|
with self._lock:
|
|
rows = self._connection.execute(
|
|
"SELECT event_type, details_json, created_at FROM provenance_events WHERE provenance_id = ? ORDER BY created_at, event_id",
|
|
(required(provenance_id, "provenance_id"),),
|
|
).fetchall()
|
|
return [
|
|
{
|
|
"event_type": row["event_type"],
|
|
"details": json.loads(row["details_json"]),
|
|
"created_at": row["created_at"],
|
|
}
|
|
for row in rows
|
|
]
|
|
|
|
|
|
class ProvenanceVerifier:
|
|
"""验证 Owner 签名、上下文、路径、Git 操作、时效和重放。"""
|
|
|
|
def __init__(
|
|
self,
|
|
store: SQLiteProvenanceStore,
|
|
verifiers: Mapping[str, SignatureVerifier],
|
|
) -> None:
|
|
if not verifiers:
|
|
raise ValueError("at least one public key verifier is required")
|
|
self.store = store
|
|
self.verifiers = {required(key, "key_id"): value for key, value in verifiers.items()}
|
|
|
|
def authorize(
|
|
self,
|
|
*,
|
|
provenance_id: str,
|
|
run_id: str,
|
|
tenant_id: str,
|
|
task_id: str,
|
|
agent_id: str,
|
|
principal_id: str,
|
|
execution_path: str,
|
|
request_digest: str,
|
|
git_operation: str | None = None,
|
|
now: datetime | None = None,
|
|
) -> ProvenanceAuthorization:
|
|
provenance = self.store.get(provenance_id)
|
|
status = self.store.status(provenance.provenance_id)
|
|
if status == ProvenanceStatus.REVOKED:
|
|
raise ProvenanceRevoked("provenance is revoked")
|
|
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
|
|
if current >= parse_timestamp(provenance.expires_at):
|
|
self.store.mark_expired(provenance.provenance_id)
|
|
raise ProvenanceExpired("provenance is expired")
|
|
verifier = self.verifiers.get(provenance.key_id)
|
|
if verifier is None:
|
|
raise ProvenanceSignatureError("no trusted public key for provenance key_id")
|
|
if not provenance.signature or not verifier.verify(
|
|
provenance.canonical_bytes(), provenance.signature
|
|
):
|
|
raise ProvenanceSignatureError("invalid provenance signature")
|
|
expected = {
|
|
"run_id": required(run_id, "run_id"),
|
|
"tenant_id": required(tenant_id, "tenant_id"),
|
|
"task_id": required(task_id, "task_id"),
|
|
"agent_id": required(agent_id, "agent_id"),
|
|
"principal_id": required(principal_id, "principal_id"),
|
|
}
|
|
for field_name, expected_value in expected.items():
|
|
if getattr(provenance, field_name) != expected_value:
|
|
raise ProvenanceDenied(f"provenance {field_name} mismatch")
|
|
path = ExecutionPath(execution_path).value
|
|
if path not in provenance.allowed_paths:
|
|
raise ProvenanceDenied(f"execution path is not allowed: {path}")
|
|
operation: str | None = None
|
|
if path == ExecutionPath.GIT_OPERATION.value:
|
|
if git_operation is None:
|
|
raise ProvenanceDenied("git operation is required for git_operation path")
|
|
operation = GitOperation(git_operation).value
|
|
if operation not in provenance.allowed_git_operations:
|
|
raise ProvenanceDenied(f"Git operation is not allowed: {operation}")
|
|
elif git_operation is not None:
|
|
raise ProvenanceDenied("git_operation is only valid for git_operation path")
|
|
digest = _validate_digest(request_digest)
|
|
self.store.mark_verified(
|
|
provenance.provenance_id, fingerprint=verifier.fingerprint
|
|
)
|
|
authorization = ProvenanceAuthorization(
|
|
authorization_id=f"pauth_{uuid.uuid4().hex}",
|
|
provenance_id=provenance.provenance_id,
|
|
run_id=provenance.run_id,
|
|
tenant_id=provenance.tenant_id,
|
|
task_id=provenance.task_id,
|
|
agent_id=provenance.agent_id,
|
|
principal_id=provenance.principal_id,
|
|
execution_path=path,
|
|
git_operation=operation,
|
|
request_digest=digest,
|
|
policy_version=provenance.policy_version,
|
|
key_id=provenance.key_id,
|
|
public_key_fingerprint=verifier.fingerprint,
|
|
)
|
|
self.store.record_authorization(authorization)
|
|
return authorization
|
|
|
|
|
|
class ProvenanceControlledWriter:
|
|
"""七路径统一策略入口并持久写入授权回执。"""
|
|
|
|
def __init__(
|
|
self,
|
|
verifier: ProvenanceVerifier,
|
|
receipt_root: str | Path,
|
|
*,
|
|
writer_id: str = "aios-v2-provenance-writer",
|
|
) -> None:
|
|
self.verifier = verifier
|
|
self.receipt_root = Path(receipt_root).resolve()
|
|
self.receipt_root.mkdir(parents=True, exist_ok=True)
|
|
self.writer_id = required(writer_id, "writer_id")
|
|
self._lock = threading.RLock()
|
|
|
|
@staticmethod
|
|
def _component(value: str) -> str:
|
|
normalized = required(value, "path component")
|
|
if normalized in {".", ".."} or "/" in normalized or "\\" in normalized:
|
|
raise ProvenanceDenied("invalid receipt path component")
|
|
return normalized
|
|
|
|
def authorize(self, **values: Any) -> ProvenanceAuthorization:
|
|
authorization = self.verifier.authorize(**values)
|
|
tenant = self._component(authorization.tenant_id)
|
|
provenance_id = self._component(authorization.provenance_id)
|
|
target_dir = self.receipt_root / tenant / provenance_id
|
|
target = target_dir / f"{authorization.authorization_id}.json"
|
|
payload = authorization.as_dict()
|
|
payload["writer_id"] = self.writer_id
|
|
with self._lock:
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
temporary = target.with_suffix(f".tmp-{os.getpid()}")
|
|
temporary.write_text(
|
|
json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
os.chmod(temporary, 0o640)
|
|
os.replace(temporary, target)
|
|
return authorization
|
|
|
|
def authorize_code_change(self, **values: Any) -> ProvenanceAuthorization:
|
|
return self.authorize(execution_path=ExecutionPath.CODE_CHANGE.value, **values)
|
|
|
|
def authorize_git_operation(
|
|
self, *, git_operation: str, **values: Any
|
|
) -> ProvenanceAuthorization:
|
|
return self.authorize(
|
|
execution_path=ExecutionPath.GIT_OPERATION.value,
|
|
git_operation=git_operation,
|
|
**values,
|
|
)
|
|
|
|
def authorize_deployment(self, **values: Any) -> ProvenanceAuthorization:
|
|
return self.authorize(execution_path=ExecutionPath.DEPLOYMENT.value, **values)
|
|
|
|
def authorize_runtime_execute(self, **values: Any) -> ProvenanceAuthorization:
|
|
return self.authorize(execution_path=ExecutionPath.RUNTIME_EXECUTE.value, **values)
|
|
|
|
def authorize_evidence_write(self, **values: Any) -> ProvenanceAuthorization:
|
|
return self.authorize(execution_path=ExecutionPath.EVIDENCE_WRITE.value, **values)
|
|
|
|
def authorize_rollback(self, **values: Any) -> ProvenanceAuthorization:
|
|
return self.authorize(execution_path=ExecutionPath.ROLLBACK.value, **values)
|
|
|
|
def authorize_audit(self, **values: Any) -> ProvenanceAuthorization:
|
|
return self.authorize(execution_path=ExecutionPath.AUDIT.value, **values)
|