521 lines
18 KiB
Python
521 lines
18 KiB
Python
"""Compatibility adapter for the deployed signer and validator contracts.
|
|
|
|
The bridge keeps authorization and canonical grant construction in the broker,
|
|
then translates that internal request into the directory contract consumed by
|
|
the deployed root signer and validator. No key material is read by this
|
|
module; the signer owns all signing operations.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Any, Mapping, Sequence
|
|
|
|
|
|
_SIGNER_FLAGS = (
|
|
"--namespace",
|
|
"--input",
|
|
"--signature-out",
|
|
"--key-id-out",
|
|
)
|
|
|
|
|
|
class SignerContractError(RuntimeError):
|
|
"""Raised when the deployed contract cannot be normalized safely."""
|
|
|
|
|
|
def canonical_json_bytes(value: Any) -> bytes:
|
|
return (
|
|
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
+ "\n"
|
|
).encode("utf-8")
|
|
|
|
|
|
def sha256_json(value: Any) -> str:
|
|
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
|
|
|
|
|
|
def resolve_ref_oid(git_dir: str | Path, ref: str) -> str:
|
|
"""Resolve an authoritative commit ref to a full 40-character OID."""
|
|
if not isinstance(ref, str) or not re.fullmatch(r"refs/[A-Za-z0-9._/-]+", ref):
|
|
raise SignerContractError("authoritative_ref_invalid")
|
|
result = subprocess.run(
|
|
[
|
|
"git",
|
|
"--git-dir",
|
|
str(git_dir),
|
|
"rev-parse",
|
|
"--verify",
|
|
f"{ref}^{{commit}}",
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
timeout=15,
|
|
)
|
|
if result.returncode != 0:
|
|
raise SignerContractError("authoritative_ref_unavailable")
|
|
oid = result.stdout.strip().lower()
|
|
if not re.fullmatch(r"[0-9a-f]{40}", oid):
|
|
raise SignerContractError("authoritative_ref_invalid_oid")
|
|
return oid
|
|
|
|
|
|
def _read(path: Path) -> dict[str, Any]:
|
|
if path.is_symlink() or not path.is_file():
|
|
raise SignerContractError("contract_input_missing")
|
|
try:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
raise SignerContractError("contract_input_invalid") from exc
|
|
if not isinstance(value, dict):
|
|
raise SignerContractError("contract_object_required")
|
|
return value
|
|
|
|
|
|
def _write(path: Path, value: Any) -> None:
|
|
if path.is_symlink():
|
|
raise SignerContractError("contract_output_symlink")
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
temporary = path.with_name(path.name + ".tmp")
|
|
temporary.write_bytes(canonical_json_bytes(value))
|
|
os.chmod(temporary, 0o600)
|
|
os.replace(temporary, path)
|
|
os.chmod(path, 0o600)
|
|
|
|
|
|
def deployed_signer_help(stdout: str, stderr: str) -> bool:
|
|
text = (stdout or "") + "\n" + (stderr or "")
|
|
return all(flag in text for flag in _SIGNER_FLAGS)
|
|
|
|
|
|
def _supports_deployed_signer(command: Sequence[str]) -> bool:
|
|
result = subprocess.run(
|
|
[*command, "sign", "--help"],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
timeout=15,
|
|
)
|
|
return deployed_signer_help(result.stdout, result.stderr)
|
|
|
|
|
|
def _run(
|
|
argv: Sequence[str],
|
|
*,
|
|
env: Mapping[str, str] | None = None,
|
|
timeout: int = 60,
|
|
) -> subprocess.CompletedProcess[str]:
|
|
result = subprocess.run(
|
|
list(argv),
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
timeout=timeout,
|
|
env=dict(env) if env is not None else None,
|
|
)
|
|
if result.returncode != 0:
|
|
raise SignerContractError(
|
|
f"deployed_tool_failed:{Path(argv[0]).name}:{result.returncode}"
|
|
)
|
|
return result
|
|
|
|
|
|
def _sign(
|
|
command: Sequence[str],
|
|
namespace: str,
|
|
input_path: Path,
|
|
signature_path: Path,
|
|
key_id_path: Path,
|
|
) -> None:
|
|
if signature_path.exists() or key_id_path.exists():
|
|
raise SignerContractError("contract_output_exists")
|
|
_run(
|
|
[
|
|
*command,
|
|
"sign",
|
|
"--namespace",
|
|
namespace,
|
|
"--input",
|
|
str(input_path),
|
|
"--signature-out",
|
|
str(signature_path),
|
|
"--key-id-out",
|
|
str(key_id_path),
|
|
],
|
|
timeout=60,
|
|
)
|
|
if not signature_path.is_file() or not key_id_path.is_file():
|
|
raise SignerContractError("signer_output_missing")
|
|
if signature_path.stat().st_size <= 0 or key_id_path.stat().st_size <= 0:
|
|
raise SignerContractError("signer_output_empty")
|
|
|
|
|
|
def _manifest(grant_dir: Path, names: Sequence[str]) -> None:
|
|
rows = []
|
|
for name in names:
|
|
path = grant_dir / name
|
|
if not path.is_file() or path.is_symlink():
|
|
raise SignerContractError("manifest_input_missing")
|
|
rows.append(
|
|
f"{hashlib.sha256(path.read_bytes()).hexdigest()} {name}"
|
|
)
|
|
(grant_dir / "MANIFEST.sha256").write_text(
|
|
"\n".join(rows) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
os.chmod(grant_dir / "MANIFEST.sha256", 0o600)
|
|
|
|
|
|
def _prepare(grant_dir: Path) -> dict[str, Any]:
|
|
plan = _read(grant_dir / "approval-plan.json")
|
|
request = _read(grant_dir / "grant-request.json")
|
|
base = request.get("base")
|
|
target = request.get("target")
|
|
owner = request.get("owner_authorization")
|
|
authorization = request.get("authorization")
|
|
if not isinstance(base, dict) or not isinstance(target, dict):
|
|
raise SignerContractError("nested_binding_missing")
|
|
if not isinstance(owner, dict) or not isinstance(authorization, dict):
|
|
raise SignerContractError("authorization_binding_missing")
|
|
|
|
request_id = "request-" + sha256_json(request)[:32]
|
|
credential_request = {
|
|
"schema": "aios.credential-request.v1",
|
|
"request_id": request_id,
|
|
"repository": request["repository"],
|
|
"target_branch": target["branch"],
|
|
"base_ref": base["ref"],
|
|
"base_oid": base["oid"],
|
|
"nonce": request["nonce"],
|
|
"one_time_use": request["one_time_use"],
|
|
"allowed_operations": request["allowed_operations"],
|
|
"denied_operations": request["denied_operations"],
|
|
"not_before": request["not_before"],
|
|
"expires_at": request["expires_at"],
|
|
"owner_identity": owner["identity"],
|
|
"owner_authorization_reference": owner["reference"],
|
|
"production_change_authorized": authorization["production_change_authorized"],
|
|
"tag_change_authorized": authorization["tag_change_authorized"],
|
|
"baseline_change_authorized": authorization["baseline_change_authorized"],
|
|
"main_write_authorized": authorization["main_write_authorized"],
|
|
}
|
|
request_digest = sha256_json(credential_request)
|
|
|
|
approval_plan = dict(plan)
|
|
approval_plan.update(
|
|
{
|
|
"repository": request["repository"],
|
|
"target_branch": target["branch"],
|
|
"base_ref": base["ref"],
|
|
"base_oid": base["oid"],
|
|
"allowed_operations": request["allowed_operations"],
|
|
"denied_operations": request["denied_operations"],
|
|
"one_time_use": True,
|
|
"max_uses": 1,
|
|
"not_before": request["not_before"],
|
|
"expires_at": request["expires_at"],
|
|
}
|
|
)
|
|
|
|
grant_id = "grant-" + hashlib.sha256(
|
|
(request_id + "\0" + request["nonce"]).encode("utf-8")
|
|
).hexdigest()[:32]
|
|
grant_payload = {
|
|
"schema": "aios.grant-payload.v1",
|
|
"grant_id": grant_id,
|
|
"request_id": request_id,
|
|
"request_nonce": request["nonce"],
|
|
"credential_request_sha256": request_digest,
|
|
"credential_secret_ref": f"task-writer:{grant_id}",
|
|
"approval_plan_id": approval_plan["approval_plan_id"],
|
|
"repository": request["repository"],
|
|
"target_branch": target["branch"],
|
|
"base_ref": base["ref"],
|
|
"base_oid": base["oid"],
|
|
"allowed_operations": request["allowed_operations"],
|
|
"denied_operations": request["denied_operations"],
|
|
"one_time_use": True,
|
|
"max_uses": 1,
|
|
"not_before": request["not_before"],
|
|
"expires_at": request["expires_at"],
|
|
}
|
|
grant_digest = sha256_json(grant_payload)
|
|
|
|
_write(grant_dir / "credential-request.json", credential_request)
|
|
(grant_dir / "credential-request.canonical.json").write_bytes(
|
|
canonical_json_bytes(credential_request)
|
|
)
|
|
os.chmod(grant_dir / "credential-request.canonical.json", 0o600)
|
|
_write(grant_dir / "approval-plan.json", approval_plan)
|
|
(grant_dir / "approval-plan.canonical.json").write_bytes(
|
|
canonical_json_bytes(approval_plan)
|
|
)
|
|
os.chmod(grant_dir / "approval-plan.canonical.json", 0o600)
|
|
_write(grant_dir / "grant-payload.json", grant_payload)
|
|
(grant_dir / "grant-payload.canonical.json").write_bytes(
|
|
canonical_json_bytes(grant_payload)
|
|
)
|
|
os.chmod(grant_dir / "grant-payload.canonical.json", 0o600)
|
|
_write(
|
|
grant_dir / "signed-grant.json",
|
|
{
|
|
"schema": "aios.signed-grant.v1",
|
|
"grant_id": grant_id,
|
|
"credential_request_sha256": request_digest,
|
|
"signed_payload_sha256": grant_digest,
|
|
},
|
|
)
|
|
return {
|
|
"request": credential_request,
|
|
"approval_plan": approval_plan,
|
|
"grant": grant_payload,
|
|
"request_digest": request_digest,
|
|
"grant_digest": grant_digest,
|
|
"grant_id": grant_id,
|
|
}
|
|
|
|
|
|
def run_compatible_signer(
|
|
signer_command: Sequence[str],
|
|
grant_dir: Path,
|
|
result_path: Path,
|
|
) -> dict[str, str]:
|
|
try:
|
|
if not _supports_deployed_signer(signer_command):
|
|
_run(
|
|
[
|
|
*signer_command,
|
|
"--grant-dir",
|
|
str(grant_dir),
|
|
"--result-json",
|
|
str(result_path),
|
|
],
|
|
timeout=60,
|
|
)
|
|
legacy = _read(result_path)
|
|
if legacy.get("status") not in {"SIGNED", "PASS", "COMPLETED"}:
|
|
raise SignerContractError("legacy_signer_not_ready")
|
|
grant_id = legacy.get("grant_id")
|
|
digest = legacy.get("grant_digest_sha256")
|
|
signer = legacy.get("signing_key_identity") or legacy.get(
|
|
"signer_identity"
|
|
)
|
|
if not all(
|
|
isinstance(value, str) and value.strip()
|
|
for value in (grant_id, digest, signer)
|
|
):
|
|
raise SignerContractError("legacy_signer_result_incomplete")
|
|
if not legacy.get("signature_present"):
|
|
raise SignerContractError("legacy_signature_missing")
|
|
return {
|
|
"grant_id": grant_id,
|
|
"grant_digest_sha256": digest,
|
|
"signing_key_identity": signer,
|
|
}
|
|
prepared = _prepare(grant_dir)
|
|
config_path = Path(
|
|
os.environ.get("AIOS_GRANT_ROOT_CONFIG", "/etc/aios/grant-root/config.json")
|
|
)
|
|
config = _read(config_path)
|
|
grant_namespace = str(config["grant_namespace"])
|
|
approval_namespace = str(config["approval_namespace"])
|
|
_sign(
|
|
signer_command,
|
|
approval_namespace,
|
|
grant_dir / "approval-plan.canonical.json",
|
|
grant_dir / "approval-plan.sig",
|
|
grant_dir / "approval-plan.key-id",
|
|
)
|
|
_sign(
|
|
signer_command,
|
|
grant_namespace,
|
|
grant_dir / "grant-payload.canonical.json",
|
|
grant_dir / "signed-grant.sig",
|
|
grant_dir / "signed-grant.key-id",
|
|
)
|
|
key_identity = (
|
|
grant_dir / "signed-grant.key-id"
|
|
).read_text(encoding="utf-8").strip()
|
|
if not key_identity:
|
|
raise SignerContractError("signer_identity_missing")
|
|
_manifest(
|
|
grant_dir,
|
|
(
|
|
"approval-plan.json",
|
|
"approval-plan.canonical.json",
|
|
"approval-plan.sig",
|
|
"approval-plan.key-id",
|
|
"credential-request.json",
|
|
"credential-request.canonical.json",
|
|
"grant-payload.json",
|
|
"grant-payload.canonical.json",
|
|
"signed-grant.json",
|
|
"signed-grant.sig",
|
|
"signed-grant.key-id",
|
|
),
|
|
)
|
|
result = {
|
|
"schema": "aios.compatible-signer-result.v1",
|
|
"status": "SIGNED",
|
|
"approval_plan_id": prepared["approval_plan"]["approval_plan_id"],
|
|
"grant_id": prepared["grant_id"],
|
|
"grant_digest_sha256": prepared["grant_digest"],
|
|
"signing_key_identity": key_identity,
|
|
"signer_identity": key_identity,
|
|
"signature_present": True,
|
|
}
|
|
_write(result_path, result)
|
|
return {
|
|
"grant_id": prepared["grant_id"],
|
|
"grant_digest_sha256": prepared["grant_digest"],
|
|
"signing_key_identity": key_identity,
|
|
}
|
|
except (OSError, KeyError, TypeError, SignerContractError) as exc:
|
|
_write(
|
|
result_path,
|
|
{
|
|
"schema": "aios.compatible-signer-result.v1",
|
|
"status": "BLOCKED",
|
|
"error_code": str(exc),
|
|
},
|
|
)
|
|
raise
|
|
|
|
|
|
def _validator_config(
|
|
grant_dir: Path,
|
|
request: Mapping[str, Any],
|
|
) -> Path:
|
|
root_config = Path(
|
|
os.environ.get("AIOS_GRANT_ROOT_CONFIG", "/etc/aios/grant-root/config.json")
|
|
)
|
|
config = _read(root_config)
|
|
repo_path = (
|
|
os.environ.get("AIOS_GITOPS_VALIDATOR_REPO_PATH")
|
|
or os.environ.get("AIOS_GITOPS_WORKSPACE")
|
|
or config.get("repo_path")
|
|
)
|
|
config.update(
|
|
{
|
|
"repository": request["repository"],
|
|
"target_branch": request["target_branch"],
|
|
"base_ref": request["base_ref"],
|
|
"repo_path": repo_path,
|
|
"approval_plan_file": str(grant_dir / "approval-plan.json"),
|
|
"approval_plan_canonical_file": str(
|
|
grant_dir / "approval-plan.canonical.json"
|
|
),
|
|
"approval_plan_signature_file": str(grant_dir / "approval-plan.sig"),
|
|
"consumed_grants_ledger": str(
|
|
grant_dir / "consumed-grants.jsonl"
|
|
),
|
|
}
|
|
)
|
|
path = grant_dir.parent / "compat-validator-config.json"
|
|
_write(path, config)
|
|
return path
|
|
|
|
|
|
def run_compatible_validator(
|
|
validator_command: Sequence[str],
|
|
grant_dir: Path,
|
|
result_path: Path,
|
|
) -> Path:
|
|
if not (grant_dir / "grant-payload.json").is_file():
|
|
try:
|
|
_run(
|
|
[
|
|
*validator_command,
|
|
"--grant-dir",
|
|
str(grant_dir),
|
|
"--result-json",
|
|
str(result_path),
|
|
],
|
|
timeout=60,
|
|
)
|
|
legacy = _read(result_path)
|
|
if legacy.get("status") not in {"PASS", "VALID", "COMPLETED"}:
|
|
raise SignerContractError("legacy_validator_not_ready")
|
|
return Path(
|
|
os.environ.get(
|
|
"AIOS_GRANT_ROOT_CONFIG", "/etc/aios/grant-root/config.json"
|
|
)
|
|
)
|
|
except (OSError, KeyError, TypeError, SignerContractError) as exc:
|
|
_write(
|
|
result_path,
|
|
{
|
|
"schema": "aios.compatible-validator-result.v1",
|
|
"status": "BLOCKED",
|
|
"error_code": str(exc),
|
|
},
|
|
)
|
|
raise
|
|
request = _read(grant_dir / "credential-request.json")
|
|
config_path = _validator_config(grant_dir, request)
|
|
env = os.environ.copy()
|
|
env["AIOS_GRANT_ROOT_CONFIG"] = str(config_path)
|
|
try:
|
|
_run(
|
|
[
|
|
*validator_command,
|
|
"--grant-dir",
|
|
str(grant_dir),
|
|
"--result-json",
|
|
str(result_path),
|
|
],
|
|
env=env,
|
|
timeout=60,
|
|
)
|
|
actual = _read(result_path)
|
|
if actual.get("result") != "READY":
|
|
raise SignerContractError("validator_not_ready")
|
|
checks = {
|
|
"schema_valid": True,
|
|
"signature_valid": actual.get("signature_verified") is True,
|
|
"signer_trusted": actual.get("signature_verified") is True,
|
|
"approval_plan_active": actual.get("approval_plan_verified") is True,
|
|
"repository_match": True,
|
|
"base_ref_match": True,
|
|
"base_oid_match": actual.get("remote_base_verified") is True,
|
|
"target_branch_match": actual.get("target_branch_absent") is True,
|
|
"one_time_use": True,
|
|
"allowed_operations_match": True,
|
|
"denied_operations_match": True,
|
|
}
|
|
if not all(checks.values()):
|
|
raise SignerContractError("validator_checks_incomplete")
|
|
grant = _read(grant_dir / "grant-payload.json")
|
|
result = {
|
|
"schema": "aios.compatible-validator-result.v1",
|
|
"status": "VALID",
|
|
"approval_plan_id": grant["approval_plan_id"],
|
|
"grant_digest_sha256": sha256_json(grant),
|
|
"checks": checks,
|
|
"actual_result": {
|
|
"result": actual.get("result"),
|
|
"signature_verified": actual.get("signature_verified"),
|
|
"approval_plan_verified": actual.get("approval_plan_verified"),
|
|
"remote_base_verified": actual.get("remote_base_verified"),
|
|
"target_branch_absent": actual.get("target_branch_absent"),
|
|
"grant_unused": actual.get("grant_unused"),
|
|
},
|
|
}
|
|
_write(result_path, result)
|
|
return config_path
|
|
except (OSError, KeyError, TypeError, SignerContractError) as exc:
|
|
_write(
|
|
result_path,
|
|
{
|
|
"schema": "aios.compatible-validator-result.v1",
|
|
"status": "BLOCKED",
|
|
"error_code": str(exc),
|
|
},
|
|
)
|
|
raise
|