Bind credential exec to exact owner inputs and approval digest
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
9eb07fd8fc
commit
42b48aa54f
13 changed files with 626 additions and 7 deletions
|
|
@ -12,11 +12,13 @@ import base64
|
|||
import copy
|
||||
import hashlib
|
||||
import http.server
|
||||
import io
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import shutil
|
||||
import socket
|
||||
import ssl
|
||||
import subprocess
|
||||
|
|
@ -24,6 +26,7 @@ import sys
|
|||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from contextlib import contextmanager, redirect_stdout
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlencode
|
||||
|
|
@ -83,7 +86,7 @@ def exercise(args):
|
|||
"keycape_contract_commit": commit(args.keycape_source),
|
||||
"consumer_source_sha256": {
|
||||
name: hashlib.sha256((ROOT / "src/secrets_engine" / name).read_bytes()).hexdigest()
|
||||
for name in ("approval_auth.py", "approval_consume.py", "approval_claim.py", "authorization.py", "config.py", "service_auth.py", "cli.py")
|
||||
for name in ("approval_auth.py", "approval_consume.py", "approval_claim.py", "authorization.py", "config.py", "service_auth.py", "cli.py", "catalog.py", "exec_owner.py", "exec_delivery.py", "routing.py")
|
||||
},
|
||||
"limitations": ["standalone Flex Auth source, not deployed pin", "local Approval Engine source, not deployed image",
|
||||
"no live custody or client-side read grant", "no OpenBao effect or model execution"],
|
||||
|
|
@ -327,6 +330,88 @@ def exercise(args):
|
|||
cli._require_lane_approval(cfg, destroy_entry, "destroy")
|
||||
assert engine.claim("synthetic-destroy")["consumed"]
|
||||
receipt["checks"]["real_dual_control_denial_then_claim_check_consume"] = True
|
||||
if args.exec_owner:
|
||||
from secrets_engine.errors import DeliveryError
|
||||
from secrets_engine.exec_owner import owner_digest
|
||||
from types import SimpleNamespace
|
||||
|
||||
# Synthetic recipient and backend, real engine CLI, exact
|
||||
# producer digest, JWT claim/consume, spawn and environment.
|
||||
owner_dir = root / "exec-owner"
|
||||
owner_dir.mkdir(mode=0o700)
|
||||
executable = owner_dir / "dash"
|
||||
shutil.copyfile("/bin/dash", executable)
|
||||
executable.chmod(0o700)
|
||||
script = owner_dir / "owner.sh"
|
||||
private_write(script, 'test "$API_TOKEN" = synthetic-owner-value || exit 10\n'
|
||||
'test -z "$SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE" || exit 11\n'
|
||||
'test -z "$BAO_TOKEN" || exit 12\n'
|
||||
'printf "owner-delivery-ok\\n"\n')
|
||||
raw["approval"]["authorization_id"] = "synthetic-owner-delivery"
|
||||
raw["delivery_config"] = {"exec_owner": {
|
||||
"status": "configured", "owner": "synthetic-metered-owner",
|
||||
"command": [str(executable), str(script)], "cwd": str(owner_dir),
|
||||
"environment": {"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8"},
|
||||
"files": {str(p): {"sha256": hashlib.sha256(p.read_bytes()).hexdigest(), "private": True}
|
||||
for p in [executable, script]},
|
||||
}}
|
||||
owner_entry = validate_entry(raw)
|
||||
req = _expected_request(cfg, owner_entry, "exec", fields=("api_token",))
|
||||
owner_request_digest = pdp.bind_request(req)
|
||||
operator_request("/v1/approvals", {
|
||||
"id": "synthetic-owner-delivery", "binding": {"action": "secrets.exec", "target": {"id": owner_entry.id, "stage": "prod"},
|
||||
"actor": "service:approval-engine-operator", "principal": "synthetic-operator", "purpose": "disposable identity proof"},
|
||||
"validity": {"not_before": (now - timedelta(minutes=1)).isoformat(), "expires_at": (now + timedelta(minutes=10)).isoformat()},
|
||||
"pdp_digest": owner_request_digest, "pdp_path": True}, "approval:create")
|
||||
operator_request("/v1/approvals/synthetic-owner-delivery/entries", {}, "approval:approve")
|
||||
backend_calls = []
|
||||
class Backend:
|
||||
@contextmanager
|
||||
def approle_session(self, role):
|
||||
assert role == owner_entry.role_name
|
||||
yield SimpleNamespace(client=self)
|
||||
def _run(self, command):
|
||||
assert command == ["kv", "get", "-format=json", f"{owner_entry.mount}/{owner_entry.path}"]
|
||||
return SimpleNamespace(returncode=0, stdout=json.dumps({"data": {"data": {"api_token": "synthetic-owner-value"}}}))
|
||||
@contextmanager
|
||||
def open_fixture_backend(*_args):
|
||||
assert engine.claim("synthetic-owner-delivery")["consumed"]
|
||||
backend_calls.append("after-consume")
|
||||
yield Backend()
|
||||
command = raw["delivery_config"]["exec_owner"]["command"]
|
||||
cli_args = SimpleNamespace(catalog=owner_entry.id, field="api_token", mode="exec-env", command=command)
|
||||
with patch.object(cli, "get_entry", return_value=owner_entry), patch.object(cli, "_open_backend", open_fixture_backend):
|
||||
substituted = SimpleNamespace(**vars(cli_args))
|
||||
substituted.command = ["/bin/echo", "substitute"]
|
||||
try:
|
||||
cli.cmd_exec(cfg, substituted)
|
||||
except DeliveryError:
|
||||
assert not engine.claim("synthetic-owner-delivery")["consumed"] and not backend_calls
|
||||
else:
|
||||
raise AssertionError("substitute recipient accepted")
|
||||
original_env = raw["delivery_config"]["exec_owner"]["environment"]["LANG"]
|
||||
raw["delivery_config"]["exec_owner"]["environment"]["LANG"] = "C"
|
||||
try:
|
||||
cli.cmd_exec(cfg, cli_args)
|
||||
except DecisionError:
|
||||
assert not engine.claim("synthetic-owner-delivery")["consumed"] and not backend_calls
|
||||
else:
|
||||
raise AssertionError("changed owner replay accepted")
|
||||
raw["delivery_config"]["exec_owner"]["environment"]["LANG"] = original_env
|
||||
output = io.StringIO()
|
||||
with redirect_stdout(output), patch.dict(os.environ, {"BAO_TOKEN": "synthetic-parent-only", "SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE": str(root / "approval.secret")}):
|
||||
assert cli.cmd_exec(cfg, cli_args) == 0
|
||||
assert "owner-delivery-ok" in output.getvalue() and "synthetic-parent-only" not in output.getvalue()
|
||||
assert backend_calls == ["after-consume"]
|
||||
assert pdp.last_decision["binding"]["context"]["exec_owner_sha256"] == owner_digest(owner_entry)
|
||||
receipt["checks"].update({
|
||||
"exec_owner_substitution_refused_before_consume_backend": True,
|
||||
"exec_owner_changed_environment_replay_refused_by_real_pdp_join": True,
|
||||
"exec_owner_real_cli_consumes_before_fixture_backend": True,
|
||||
"exec_owner_actual_child_excludes_parent_credentials": True,
|
||||
"exec_owner_digest_preserved_by_real_evaluator": True,
|
||||
})
|
||||
receipt["exec_owner_scope"] = "Synthetic recipient/backend with real KeyCape, Approval Engine, Flex Auth and Secrets Engine CLI; not native custody or human approval proof"
|
||||
finally:
|
||||
if api_server:
|
||||
api_server.shutdown()
|
||||
|
|
@ -350,6 +435,7 @@ def main():
|
|||
parser.add_argument("--approval-engine-source", required=True, type=Path)
|
||||
parser.add_argument("--flex-auth-source", required=True, type=Path)
|
||||
parser.add_argument("--receipt", required=True, type=Path)
|
||||
parser.add_argument("--exec-owner", action="store_true", help="also prove catalog-bound child delivery against the actual approval/PDP chain")
|
||||
args = parser.parse_args()
|
||||
if args.receipt.exists():
|
||||
raise SystemExit("receipt path must be new")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue