Apply the approved Implementation Baseline 1.1 architecture revisions onto the current mainline, verified faithful to the delivered change bundle: - Add aios_baseline: signed owner-delegation-grant.v1 / operational-epoch.v1 / task-principal.v1 auth chain, runtime contract + deployment-truth evaluator, declarative operation registry (effect-class + fail-closed dispatch), lineage ledger, release/recipe/saga ledger, bounded optimization controller, authenticated HTTP transport, resident worker loop and systemd host probe. - Extend existing Job authority (aios_gitops_job_controller/executor) with durable intent/receipt/reconcile and fail-closed admission; harden task_plane/store heartbeat CAS and terminal-state fencing; make reconciliation_controller fail-closed on incomplete/drift evidence. - Add isolated-validation profile templates (deploy/baseline-v1.1, systemd .example), source change/rollback tooling (tools/baseline_v1_1), the AT-01..AT-23 acceptance matrix, M0-M5 runbook, installation/rollback docs and the baseline_v1_1 test suite. - Keep legacy write authorities untouched; isolated-validation profile only. Staged blobs verified byte-identical to the delivered source-changes bundle. Cross-platform baseline tests pass; systemd/fcntl-dependent paths require the declared Linux x86_64 + CPython 3.12 + systemd runtime (site-bound).
205 lines
8.7 KiB
Python
205 lines
8.7 KiB
Python
"""Verification-only Ed25519 boundaries for release and acceptance evidence."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import re
|
|
from types import MappingProxyType
|
|
from typing import Any, Callable, Mapping
|
|
|
|
from cryptography.exceptions import InvalidSignature
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
|
|
from .lineage import VerifiedAcceptanceEvidence, canonical_artifact
|
|
from .release import VerifiedReceipt
|
|
|
|
|
|
RELEASE_RECEIPT_DOMAIN = b"aios.release-operation-receipt.v1"
|
|
ACCEPTANCE_EVIDENCE_DOMAIN = b"aios.release-acceptance-evidence.v1"
|
|
|
|
_ENVELOPE_FIELDS = {"schema", "key_id", "payload", "signature"}
|
|
_RECEIPT_FIELDS = {
|
|
"receipt_id", "tenant_id", "subject_id", "component_id", "task_id",
|
|
"source_task_id", "source_effect_id", "grant_id", "job_id", "effect_id", "operation_id",
|
|
"deployment_id", "release_id", "artifact", "outcome", "observed", "observed_at",
|
|
}
|
|
_ACCEPTANCE_FIELDS = {
|
|
"acceptance_id", "tenant_id", "subject_id", "component_id", "deployment_id", "release_id",
|
|
"artifact", "accepted", "evidence_id", "observed", "observed_at",
|
|
}
|
|
_B64URL = re.compile(r"[A-Za-z0-9_-]+")
|
|
|
|
|
|
class SignatureVerificationError(ValueError):
|
|
"""A configured trust root did not authenticate an exact evidence payload."""
|
|
|
|
|
|
def _canonical(value: Any) -> bytes:
|
|
try:
|
|
return json.dumps(
|
|
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"),
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
except (TypeError, ValueError) as exc:
|
|
raise SignatureVerificationError("signed payload is not canonical JSON") from exc
|
|
|
|
|
|
def _decode_urlsafe(value: Any, name: str, expected_size: int) -> bytes:
|
|
if not isinstance(value, str) or not value or not _B64URL.fullmatch(value) or "=" in value:
|
|
raise SignatureVerificationError(f"invalid {name} encoding")
|
|
try:
|
|
raw = base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
|
|
except (ValueError, TypeError) as exc:
|
|
raise SignatureVerificationError(f"invalid {name} encoding") from exc
|
|
if len(raw) != expected_size:
|
|
raise SignatureVerificationError(f"invalid {name} size")
|
|
return raw
|
|
|
|
|
|
def _public_key(value: Any) -> Ed25519PublicKey:
|
|
if isinstance(value, Ed25519PublicKey):
|
|
return value
|
|
if isinstance(value, bytes):
|
|
raw = value
|
|
elif isinstance(value, str):
|
|
raw = _decode_urlsafe(value, "trusted public key", 32)
|
|
else:
|
|
raise ValueError("trusted public key must be Ed25519PublicKey, raw bytes, or base64url")
|
|
if len(raw) != 32:
|
|
raise ValueError("trusted Ed25519 public key must be 32 bytes")
|
|
return Ed25519PublicKey.from_public_bytes(raw)
|
|
|
|
|
|
def _trusted_keys(values: Mapping[str, Any]) -> Mapping[str, Ed25519PublicKey]:
|
|
if not isinstance(values, Mapping) or not values:
|
|
raise ValueError("trusted_public_keys must be a non-empty mapping")
|
|
keys: dict[str, Ed25519PublicKey] = {}
|
|
for key_id, value in values.items():
|
|
if not isinstance(key_id, str) or not key_id or len(key_id) > 128:
|
|
raise ValueError("invalid trusted key_id")
|
|
keys[key_id] = _public_key(value)
|
|
return MappingProxyType(keys)
|
|
|
|
|
|
def _verified_payload(
|
|
envelope: Any,
|
|
keys: Mapping[str, Ed25519PublicKey],
|
|
*,
|
|
schema: str,
|
|
domain: bytes,
|
|
payload_fields: set[str],
|
|
) -> Mapping[str, Any]:
|
|
if not isinstance(envelope, Mapping) or set(envelope) != _ENVELOPE_FIELDS:
|
|
raise SignatureVerificationError("signed envelope fields invalid")
|
|
if envelope.get("schema") != schema:
|
|
raise SignatureVerificationError("signed envelope schema mismatch")
|
|
key_id = envelope.get("key_id")
|
|
if not isinstance(key_id, str) or key_id not in keys:
|
|
raise SignatureVerificationError("untrusted signing key_id")
|
|
payload = envelope.get("payload")
|
|
if not isinstance(payload, Mapping) or set(payload) != payload_fields:
|
|
raise SignatureVerificationError("signed payload fields invalid")
|
|
signature = _decode_urlsafe(envelope.get("signature"), "signature", 64)
|
|
try:
|
|
keys[key_id].verify(signature, domain + b"\x00" + _canonical(dict(payload)))
|
|
except InvalidSignature as exc:
|
|
raise SignatureVerificationError("Ed25519 signature invalid") from exc
|
|
return payload
|
|
|
|
|
|
def _text(payload: Mapping[str, Any], name: str) -> str:
|
|
value = payload.get(name)
|
|
if not isinstance(value, str) or not value or len(value) > 512:
|
|
raise SignatureVerificationError(f"signed {name} invalid")
|
|
return value
|
|
|
|
|
|
def _expected(payload: Mapping[str, Any], expected: Mapping[str, Any], fields: tuple[str, ...]) -> None:
|
|
for name in fields:
|
|
if name not in expected:
|
|
raise SignatureVerificationError(f"verifier expected {name} missing")
|
|
actual = canonical_artifact(payload[name]) if name == "artifact" else payload[name]
|
|
wanted = canonical_artifact(expected[name]) if name == "artifact" else expected[name]
|
|
if actual != wanted:
|
|
raise SignatureVerificationError(f"signed {name} binding mismatch")
|
|
|
|
|
|
def ed25519_release_receipt_verifier(
|
|
trusted_public_keys: Mapping[str, Any],
|
|
) -> Callable[[Any, Mapping[str, Any]], VerifiedReceipt]:
|
|
"""Build a receipt verifier from local/configured public keys only."""
|
|
|
|
keys = _trusted_keys(trusted_public_keys)
|
|
|
|
def verify(envelope: Any, expected: Mapping[str, Any]) -> VerifiedReceipt:
|
|
payload = _verified_payload(
|
|
envelope, keys,
|
|
schema="aios.release-operation-receipt-envelope.v1",
|
|
domain=RELEASE_RECEIPT_DOMAIN,
|
|
payload_fields=_RECEIPT_FIELDS,
|
|
)
|
|
_expected(
|
|
payload, expected,
|
|
("tenant_id", "subject_id", "component_id", "task_id", "source_task_id", "source_effect_id",
|
|
"grant_id", "job_id", "effect_id", "operation_id", "deployment_id", "release_id", "artifact"),
|
|
)
|
|
if payload.get("outcome") != "SUCCEEDED":
|
|
raise SignatureVerificationError("signed receipt outcome is not SUCCEEDED")
|
|
if not isinstance(payload.get("observed"), Mapping):
|
|
raise SignatureVerificationError("signed receipt observation invalid")
|
|
return VerifiedReceipt(
|
|
receipt_id=_text(payload, "receipt_id"), tenant_id=_text(payload, "tenant_id"),
|
|
subject_id=_text(payload, "subject_id"), effect_id=_text(payload, "effect_id"),
|
|
component_id=_text(payload,"component_id"), task_id=_text(payload,"task_id"),
|
|
source_task_id=_text(payload,"source_task_id"), source_effect_id=_text(payload,"source_effect_id"),
|
|
grant_id=_text(payload,"grant_id"), job_id=_text(payload,"job_id"),
|
|
operation_id=_text(payload, "operation_id"), deployment_id=_text(payload, "deployment_id"),
|
|
release_id=_text(payload, "release_id"), artifact=payload["artifact"],
|
|
observed=dict(payload["observed"]), outcome="SUCCEEDED",
|
|
observed_at=_text(payload, "observed_at"),
|
|
)
|
|
|
|
return verify
|
|
|
|
|
|
def ed25519_acceptance_evidence_verifier(
|
|
trusted_public_keys: Mapping[str, Any],
|
|
) -> Callable[[Any, Mapping[str, Any]], VerifiedAcceptanceEvidence]:
|
|
"""Build an acceptance verifier in a domain separate from stage receipts."""
|
|
|
|
keys = _trusted_keys(trusted_public_keys)
|
|
|
|
def verify(envelope: Any, expected: Mapping[str, Any]) -> VerifiedAcceptanceEvidence:
|
|
payload = _verified_payload(
|
|
envelope, keys,
|
|
schema="aios.release-acceptance-evidence-envelope.v1",
|
|
domain=ACCEPTANCE_EVIDENCE_DOMAIN,
|
|
payload_fields=_ACCEPTANCE_FIELDS,
|
|
)
|
|
_expected(
|
|
payload, expected,
|
|
("acceptance_id", "tenant_id", "subject_id", "component_id", "deployment_id", "release_id", "artifact", "accepted"),
|
|
)
|
|
if type(payload.get("accepted")) is not bool:
|
|
raise SignatureVerificationError("signed accepted decision invalid")
|
|
if not isinstance(payload.get("observed"), Mapping):
|
|
raise SignatureVerificationError("signed acceptance observation invalid")
|
|
return VerifiedAcceptanceEvidence(
|
|
acceptance_id=_text(payload, "acceptance_id"), tenant_id=_text(payload, "tenant_id"),
|
|
subject_id=_text(payload, "subject_id"), deployment_id=_text(payload, "deployment_id"),
|
|
component_id=_text(payload,"component_id"),
|
|
release_id=_text(payload, "release_id"), artifact=payload["artifact"],
|
|
accepted=payload["accepted"], evidence_id=_text(payload, "evidence_id"),
|
|
observed=dict(payload["observed"]), observed_at=_text(payload, "observed_at"),
|
|
)
|
|
|
|
return verify
|
|
|
|
|
|
__all__ = [
|
|
"ACCEPTANCE_EVIDENCE_DOMAIN", "RELEASE_RECEIPT_DOMAIN",
|
|
"SignatureVerificationError", "ed25519_acceptance_evidence_verifier",
|
|
"ed25519_release_receipt_verifier",
|
|
]
|