feat: require strict signed task authorization before approval consumption
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 4s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
tegwick 2026-10-16 09:20:57 +02:00
parent ebca4f5adb
commit ff1a13dabd
5 changed files with 539 additions and 4 deletions

View file

@ -23,7 +23,7 @@ from secrets_engine.execution_contract import ExecutionContract, require_executi
from secrets_engine.approval_claim import observe_pdp_approval_claim, validate_approval_claim
from secrets_engine.catalog import human_control_required
from secrets_engine.decision_check import check_decision, require_supported_pdp_address
from secrets_engine.decision_signature import load_trust
from secrets_engine.decision_signature import load_trust, strict_json
from secrets_engine.authorization import (
build_action_request,
validate_decision_envelope,
@ -70,6 +70,8 @@ class AuthorizedAction:
binding: ConsumeBinding
decision_id: str
expires_at: str
# Detached original served artifacts for the narrow task-execution handoff.
artifacts: str = field(default="", repr=False, compare=False)
@property
def id(self) -> str:
@ -380,6 +382,8 @@ def authorize_action(
),
decision_id=validated.decision_id,
expires_at=validated.expires_at,
artifacts=json.dumps({"request":expected_request,"claim":observation.claim,"envelope":envelope},
sort_keys=True,separators=(",",":"),allow_nan=False),
)
@ -391,6 +395,7 @@ def consume_approval(
binding: ConsumeBinding,
timeout_seconds: float = 3,
opener: Callable[..., Any] = credential_urlopen,
strict_response: bool = False,
) -> ConsumedApproval:
"""POST /v1/approvals/{id}/consume. Fail closed on anything but confirmed use."""
if not base_url or not base_url.startswith(("http://", "https://")):
@ -438,8 +443,8 @@ def consume_approval(
if status != 200 or len(raw) > _MAX_BODY:
raise DecisionError(_status_message(status if status != 200 else 502))
try:
payload = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
payload = (strict_json if strict_response else json.loads)(raw.decode("utf-8"))
except (UnicodeDecodeError, ValueError, RecursionError) as exc:
raise DecisionError("approval consume returned invalid JSON") from exc
if not isinstance(payload, dict):
raise DecisionError("approval consume returned invalid payload")
@ -447,6 +452,19 @@ def consume_approval(
raise DecisionError("approval consumption was not confirmed")
if payload.get("request_digest") != binding.request_digest:
raise DecisionError("approval consume digest does not match the request")
if strict_response:
from datetime import datetime
if (payload.get("approval_id") != approval_id
or payload.get("decision_id") != binding.decision_id
or payload.get("idempotent") is not False
or any(key in payload for key in ("effect","allow","deny","decision"))):
raise DecisionError("strict task consumption correspondence refused")
try:
stamp = datetime.fromisoformat(payload["consumed_at"].replace("Z","+00:00"))
if stamp.tzinfo is None or stamp.utcoffset() is None:
raise ValueError("timezone required")
except (KeyError,TypeError,AttributeError,ValueError):
raise DecisionError("strict task consumption timestamp refused") from None
decision_id = payload.get("decision_id")
if decision_id is not None and (
not isinstance(decision_id, str) or not decision_id

View file

@ -0,0 +1,194 @@
"""Narrow service-task PEP handoff; records never grant native execution.
Platform calls preparation itself, binds its independently compiled owner Plan,
reserves/journals once, then calls strict consumption and independent Approval
GET confirmation. No inbound mapping/boolean or constructor creates authority.
Actual policy/server input mapping and runtime entitlement remain unadmitted.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
import json
from threading import Lock
from secrets_engine.approval_auth import approval_token, credential_urlopen
from secrets_engine.approval_claim import binding_from_check_request, claim_binding_digest, validate_approval_claim
from secrets_engine.approval_consume import authorize_action, consume_approval
from secrets_engine.application_time import read_window, validity_bounds
from secrets_engine.authorization import validate_decision_envelope, request_digest, approval_binding_digest
from secrets_engine.decision_signature import authenticate_response, load_trust, strict_json
from secrets_engine.errors import DecisionError
from secrets_engine.execution_contract import ExecutionContract, require_execution_deadline
_CALLER = 'system:serviceaccount:secrets-engine:secrets-engine'
_NATIVE_FIELDS = ('action','actor','principal','purpose','target')
_COORDINATES = ('approval_url','approval_token_file','approval_client_secret_file','keycape_token_url','keycape_issuer',
'pdp_url','pdp_token_file','pdp_trust_file','pdp_verifier','clock_trust_file',
'authorization_subject_id','authorization_subject_type','authorization_policy_package','authorization_policy_version')
_ISSUED = object()
def _refuse():
raise DecisionError('strict task execution handoff correspondence refused')
def _coordinates(cfg):
return tuple(str(getattr(cfg,name,'') or '') for name in _COORDINATES)
def _stamp(value):
try:
parsed=datetime.fromisoformat(value.replace('Z','+00:00'))
if parsed.tzinfo is None or parsed.utcoffset() is None:
_refuse()
return parsed
except (AttributeError,TypeError,ValueError):
_refuse()
class TaskExecutionAuthorization:
"""Opaque process-local prepared state; not a bearer credential or allow."""
__slots__=('_action','_contract','_coordinates','_handoff','_used','_lock')
def __init__(self, issuer=None, *, action=None, contract=None, coordinates=None, handoff=None):
if issuer is not _ISSUED:
_refuse()
object.__setattr__(self,'_action',action)
object.__setattr__(self,'_contract',contract)
object.__setattr__(self,'_coordinates',coordinates)
object.__setattr__(self,'_handoff',json.dumps(handoff,sort_keys=True,separators=(',',':'),allow_nan=False))
object.__setattr__(self,'_used',False)
object.__setattr__(self,'_lock',Lock())
def __setattr__(self,name,value):
raise AttributeError('prepared task handoff is immutable')
def as_owner_handoff(self):
return strict_json(self._handoff)
@dataclass(frozen=True)
class TaskExecutionConsumption:
approval_id: str
request_digest: str
decision_id: str
consumed_at: str
idempotent: bool
def _validate(cfg, action, contract):
if type(contract) is not ExecutionContract or not action.artifacts:
_refuse()
descriptor=contract.as_dict()
artifacts=strict_json(action.artifacts)
request,claim,envelope=(artifacts[name] for name in ('request','claim','envelope'))
# Rotation/removal and installed-helper changes cannot hide behind a cached
# prepared label. Reauthenticate before each strict CAS, without networking.
authenticated=authenticate_response(json.dumps(envelope,allow_nan=False).encode(),
trust=load_trust(getattr(cfg,'pdp_trust_file',None)),
verifier=getattr(cfg,'pdp_verifier',None))
if authenticated != envelope:
_refuse()
window=read_window(cfg)
lower,upper=validity_bounds(window=window)
validated=validate_decision_envelope(envelope,request,
accepted_policy_packages={str(cfg.authorization_policy_package)},
accepted_policy_versions={str(cfg.authorization_policy_version)},
expected_approval_binding_digest=claim['binding']['pdp_digest'],time_window=window)
validate_approval_claim(claim,approval_id=action.binding.approval_id,
expected_pdp_digest=envelope['binding']['approval_binding_digest'],
require_human_control=True,time_window=window)
require_execution_deadline(request,time_window=window,
bounds=(claim['validity']['expires_at'],envelope['lifetime']['expires_at']))
binding=envelope['binding']
if (envelope['lifetime'].get('kind')!='ttl'
or _stamp(envelope['lifetime'].get('not_before'))>lower):
_refuse()
if (request['action']!='execute-ccr' or binding.get('action')!='execute-ccr'
or request['tenant']!='tenant:platform' or binding.get('tenant')!='tenant:platform'
or binding.get('context')!=request['context']
or request['context'].get('execution_contract')!=descriptor
or binding.get('request_digest')!=request_digest(binding)
or binding.get('approval_binding_digest')!=approval_binding_digest(binding)
or action.binding.request_digest!=validated.request_digest
or action.binding.decision_id!=validated.decision_id):
_refuse()
subject=binding['subject']; resource=binding['resource']
if (request['subject']!={'id':'secrets-engine','type':'service'}
or subject.get('id')!='secrets-engine' or subject.get('type')!='service'
or subject.get('tenant') not in (None,'tenant:platform')
or 'principal' in subject.get('attributes',{})
or envelope['subject']!=subject or envelope['resource']!=resource
or descriptor['execution_actor']!='service:secrets-engine'
or any(resource.get(key)!=request['resource'].get(key) for key in ('id','type','system'))
or resource.get('tenant') not in (None,'tenant:platform')
or any(resource.get('attributes',{}).get(key)!=value for key,value in request['resource']['attributes'].items())
or any(key in binding['context'] for key in ('task_execution_projection','initiator','represented_principal','delegation','verified','owner_admission'))):
_refuse()
caller=envelope['provenance'].get('caller')
if (not isinstance(caller,dict) or caller.get('mode')!='enforce'
or caller.get('principal')!=_CALLER or caller.get('audience')!='flex-auth'
or _stamp(caller.get('not_after')) < _stamp(descriptor['operation_deadline'])
or upper >= _stamp(caller.get('not_after'))):
_refuse()
native=binding_from_check_request(binding)
supplied=claim['binding']
# Recompute the served native act too: Python equality alone conflates
# boolean/integer enrichment and cannot establish canonical correspondence.
if (any(supplied.get(key)!=native[key] for key in _NATIVE_FIELDS)
or supplied.get('digest')!=claim_binding_digest(**native)
or supplied.get('digest')!=claim_binding_digest(**{key:supplied.get(key) for key in _NATIVE_FIELDS})):
_refuse()
return {'execution_contract':descriptor,'approval_id':action.binding.approval_id,
'request_digest':validated.request_digest,'decision_id':validated.decision_id,
'native_binding':native,'approval_binding_digest':binding['approval_binding_digest'],
'submitted_request_digest':validated.submitted_request_digest,
'decision_not_before':envelope['lifetime'].get('not_before') or '',
'decision_expires_at':envelope['lifetime']['expires_at'],
'approval_expires_at':claim['validity']['expires_at']}
def authorize_task_execution(cfg, entry, *, execution_contract):
if type(execution_contract) is not ExecutionContract:
_refuse()
# No injected transports or policy callbacks: use the actual existing PIP/PDP
# clients and mandatory signature validators in the owner process.
action=authorize_action(cfg,entry,'execute-ccr',execution_contract=execution_contract)
if action is None:
_refuse()
try:
handoff=_validate(cfg,action,execution_contract)
return TaskExecutionAuthorization(_ISSUED,action=action,contract=ExecutionContract.parse(execution_contract.as_dict()),
coordinates=_coordinates(cfg),handoff=handoff)
except (ValueError,KeyError,TypeError,RecursionError) as exc:
raise DecisionError('strict task execution handoff malformed') from exc
def consume_task_execution(cfg, authorization):
if type(authorization) is not TaskExecutionAuthorization:
_refuse()
with authorization._lock:
if authorization._used or _coordinates(cfg)!=authorization._coordinates:
_refuse()
# Reserve once before all work; failures/unknown outcomes never retry this
# prepared object. Platform's durable journal is the cross-process guard.
object.__setattr__(authorization,'_used',True)
try:
handoff=_validate(cfg,authorization._action,authorization._contract)
if handoff!=authorization.as_owner_handoff():
_refuse()
result=consume_approval(base_url=str(cfg.approval_url),
token_provider=lambda:approval_token(cfg,scope='approval:consume'),
binding=authorization._action.binding,opener=credential_urlopen,strict_response=True)
stamp=_stamp(result.consumed_at)
window=read_window(cfg);_,upper=validity_bounds(window=window)
if (stamp>upper or (handoff['decision_not_before'] and stamp<_stamp(handoff['decision_not_before']))
or any(stamp>=_stamp(value) for value in (handoff['decision_expires_at'],handoff['approval_expires_at'],
handoff['execution_contract']['operation_deadline']))):
_refuse()
# Late CAS completion is historical evidence only; refuse native dispatch.
_validate(cfg,authorization._action,authorization._contract)
return TaskExecutionConsumption(result.approval_id,result.request_digest,result.decision_id,result.consumed_at,result.idempotent)
except (ValueError,KeyError,TypeError,RecursionError) as exc:
raise DecisionError('strict task execution consumption malformed') from exc