Verify normalized task approval facts and preserve signed source types
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
tegwick 2026-10-16 17:28:03 +02:00
parent 5a2099f400
commit 4dd004a225
18 changed files with 756 additions and 60 deletions

View file

@ -382,7 +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},
artifacts=json.dumps({"request":expected_request,"claim":observation.claim,"envelope":envelope,
"authenticated_envelope_json":getattr(envelope,"authenticated_json","")},
sort_keys=True,separators=(",",":"),allow_nan=False),
)

View file

@ -7,6 +7,7 @@ from __future__ import annotations
import base64
import json
import math
from pathlib import Path
import stat
import subprocess
@ -24,8 +25,15 @@ def _unique_object(pairs):
return result
def _finite_float(value):
parsed = float(value)
if not math.isfinite(parsed):
raise ValueError("nonfinite JSON number")
return parsed
def strict_json(data):
return json.loads(data, object_pairs_hook=_unique_object,
return json.loads(data, object_pairs_hook=_unique_object, parse_float=_finite_float,
parse_constant=lambda value: (_ for _ in ()).throw(ValueError("invalid number")))
@ -66,10 +74,18 @@ def load_trust(path: Path | None) -> dict[str, Any]:
raise DecisionError("access-engine responder trust is missing or invalid") from exc
class AuthenticatedEnvelope(dict):
"""Verified typed values with exact producer JSON retained for reauthentication."""
def __init__(self, values, authenticated_json):
super().__init__(values)
self.authenticated_json = authenticated_json
def authenticate_response(body: bytes, *, trust: dict, verifier: Path | None) -> dict:
if verifier is None or not Path(verifier).is_absolute():
raise DecisionError("access-engine signature verifier is not installed")
try:
strict_json(body.decode("utf-8"))
result = subprocess.run([str(verifier)], input=b'{"envelope":'+body+b',"keys":'+json.dumps(trust['keys']).encode()+b'}',
capture_output=True, env={}, timeout=3, check=False)
if result.returncode != 0 or len(result.stdout) > 524288:
@ -78,6 +94,6 @@ def authenticate_response(body: bytes, *, trust: dict, verifier: Path | None) ->
provenance = authenticated['provenance']
if any(provenance.get(k) != trust[k] for k in ('evaluator','mode','policy_package','policy_version','policy_package_digest')):
raise ValueError("issuer/pin mismatch")
return authenticated
except (OSError, subprocess.SubprocessError, ValueError, KeyError, TypeError, RecursionError) as exc:
return AuthenticatedEnvelope(authenticated, result.stdout.decode("utf-8"))
except (OSError, subprocess.SubprocessError, ValueError, KeyError, TypeError, RecursionError, UnicodeDecodeError) as exc:
raise DecisionError("access-engine responder authentication failed") from exc

View file

@ -153,6 +153,18 @@ def attach_execution_contract(request: dict, contract: ExecutionContract | None)
raise DecisionError('execute-ccr requires platform tenant')
if value['catalog_sha256'] != catalog_target_digest(request['context']['catalog_target']):
raise DecisionError('execution_contract catalog target does not match')
if request['subject'] != {'id':'secrets-engine','type':'service'}:
raise DecisionError('execute-ccr requires secrets-engine service subject')
# Match the producer's fixed tenant normalization before approval issue.
request['subject']['tenant'] = 'tenant:platform'
request['resource']['tenant'] = 'tenant:platform'
# Public declaration from Access ServicePilotInputs.DeclaredServiceBinding;
# bound before approval issuance, never an authentication assertion.
request['context']['service_actor_binding'] = {
'version':'proposed-service-task-caller/v1',
'transport_principal':'system:serviceaccount:secrets-engine:secrets-engine',
'subject_id':'secrets-engine','subject_type':'service','tenant':'tenant:platform',
}
request['context']['execution_contract'] = value
request['context']['human_control'] = True

View file

@ -10,14 +10,16 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
import json
import re
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.approval_consume import authorize_action, consume_approval, _expected_request
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.authorization import validate_decision_envelope, request_digest, approval_binding_digest, CONTRACT_VERSION
from secrets_engine.decision_signature import authenticate_response, load_trust, strict_json
from secrets_engine.decision_check import check_decision
from secrets_engine.errors import DecisionError
from secrets_engine.execution_contract import ExecutionContract, require_execution_deadline
@ -77,6 +79,93 @@ class TaskExecutionConsumption:
idempotent: bool
def _same_json(left, right):
# Type-sensitive semantic correspondence only; never signature material.
return json.dumps(left,sort_keys=True,allow_nan=False)==json.dumps(right,sort_keys=True,allow_nan=False)
def _task_identity(request,envelope,descriptor,upper):
binding=envelope['binding']
subject=binding['subject']; resource=binding['resource']
if (request['subject']!={'id':'secrets-engine','type':'service','tenant':'tenant:platform'}
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 not _same_json(envelope['subject'],subject) or not _same_json(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(not _same_json(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()
@dataclass(frozen=True)
class TaskApprovalPreparation:
"""Detached public normalization facts; never authorization or consumption."""
_serialized: str
def as_owner_handoff(self):
return strict_json(self._serialized)
def prepare_task_approval(cfg, entry, *, execution_contract):
"""Read signed DENY normalization before Approval issue; never permit an act.
This read-only normalization record has no independent permission window.
decision_time must be a valid nonfuture instant; original operation/caller
deadlines remain live. Final source Check independently revalidates current
registry/source facts; stale normalized facts cannot replace that Check.
Use returned authoritative native_binding and approval_binding_digest for
issue, preserving original_request. Final Check carries only the original
issued claim; producer renormalization independently refuses registry drift.
"""
if type(execution_contract) is not ExecutionContract:
_refuse()
request=_expected_request(cfg,entry,'execute-ccr',execution_contract=execution_contract)
window=read_window(cfg)
require_execution_deadline(request,time_window=window)
envelope=check_decision(base_url=str(cfg.pdp_url),token_file=cfg.pdp_token_file,request=request,
trust_file=cfg.pdp_trust_file,verifier=cfg.pdp_verifier)
try:
window=read_window(cfg);lower,upper=validity_bounds(window=window)
descriptor=execution_contract.as_dict();binding=envelope['binding'];provenance=envelope['provenance']
if (envelope.get('contract_version')!=CONTRACT_VERSION or envelope.get('effect')!='deny' or not isinstance(envelope.get('id'),str) or not envelope['id']
or provenance.get('policy_package')!=str(cfg.authorization_policy_package)
or provenance.get('policy_version')!=str(cfg.authorization_policy_version)
or not isinstance(provenance.get('registry_snapshot_digest'),str)
or re.fullmatch(r'sha256:[0-9a-f]{64}',provenance['registry_snapshot_digest']) is None
or request['action']!='execute-ccr' or binding.get('action')!='execute-ccr'
or request['tenant']!='tenant:platform' or binding.get('tenant')!='tenant:platform'
or 'approval' in request['context'] or 'approval' in binding['context']
or not _same_json(binding.get('context'),request['context'])
or binding.get('submitted_request_digest')!=request_digest(request)
or binding.get('request_digest')!=request_digest(binding)
or binding.get('approval_binding_digest')!=binding['request_digest']
or not _same_json(envelope.get('subject'),binding['subject']) or not _same_json(envelope.get('resource'),binding['resource'])
or _stamp(provenance.get('decision_time'))>upper):
_refuse()
_task_identity(request,envelope,descriptor,upper)
require_execution_deadline(request,time_window=window,bounds=(provenance['caller']['not_after'],))
native=binding_from_check_request(binding)
claim_binding_digest(**native) # Require native JSON profile, not a translated digest.
result={'original_request':request,'submitted_request_digest':binding['submitted_request_digest'],
'native_binding':native,'approval_binding_digest':binding['approval_binding_digest'],
'decision_id':envelope['id'],'execution_contract':descriptor,
'registry_snapshot_digest':provenance['registry_snapshot_digest']}
return TaskApprovalPreparation(json.dumps(result,sort_keys=True,separators=(',',':'),allow_nan=False))
except (ValueError,KeyError,TypeError,RecursionError,AttributeError) as exc:
raise DecisionError('task approval preparation malformed') from exc
def _validate(cfg, action, contract):
if type(contract) is not ExecutionContract or not action.artifacts:
_refuse()
@ -85,10 +174,10 @@ def _validate(cfg, action, contract):
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(),
authenticated=authenticate_response(artifacts["authenticated_envelope_json"].encode("utf-8"),
trust=load_trust(getattr(cfg,'pdp_trust_file',None)),
verifier=getattr(cfg,'pdp_verifier',None))
if authenticated != envelope:
if json.dumps(authenticated,sort_keys=True,allow_nan=False) != json.dumps(envelope,sort_keys=True,allow_nan=False):
_refuse()
window=read_window(cfg)
lower,upper=validity_bounds(window=window)
@ -107,31 +196,14 @@ def _validate(cfg, action, contract):
_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 not _same_json(binding.get('context'),request['context'])
or not _same_json(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()
_task_identity(request,envelope,descriptor,upper)
native=binding_from_check_request(binding)
supplied=claim['binding']
# Recompute the served native act too: Python equality alone conflates
@ -140,6 +212,22 @@ def _validate(cfg, action, contract):
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()
observation=envelope['provenance'].get('approval_observation')
if observation is not None:
# Signed producer source evidence supplements the original carried claim;
# it cannot replace original context, extend freshness, or authorize CAS.
if (not isinstance(observation,dict)
or any(not isinstance(observation.get(k),str) or not observation[k]
for k in ('source_id','source_revision'))
or any(not isinstance(observation.get(k),str)
or re.fullmatch(r'sha256:[0-9a-f]{64}',observation[k]) is None
for k in ('configuration_digest','claim_digest','native_binding_digest'))
or observation['native_binding_digest']!=supplied['digest']
or _stamp(observation.get('observed_at'))>_stamp(observation.get('validated_at'))
or _stamp(observation.get('validated_at'))>upper
or upper>=_stamp(observation.get('not_after'))
or _stamp(envelope['lifetime']['expires_at'])>_stamp(observation.get('not_after'))):
_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'],