Bind credential exec to exact owner inputs and approval digest
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-10 09:29:38 +02:00
parent 9eb07fd8fc
commit 42b48aa54f
13 changed files with 626 additions and 7 deletions

View file

@ -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")