#!/usr/bin/env python3 """Verify captured native decision receipts with the installed Secrets verifier. Offline only: no token input, network request, key generation, build or effect. Use an independently reviewed admission file; success never admits that file. """ from __future__ import annotations import argparse import copy from datetime import datetime, timezone import hashlib import json from pathlib import Path import re # These are the consumer's existing verifier/binding functions, not another PDP # or signature/canonicalization implementation. Install the reviewed bundle first. from secrets_engine.authorization import request_digest, validate_decision_envelope from secrets_engine.decision_signature import authenticate_response, load_trust, strict_json from secrets_engine.errors import DecisionError FIELDS = {'schema','trust_file','verifier','verifier_sha256','allow_request','allow_envelope','allow_record','deny_request','deny_envelope','deny_record','expected_caller'} def read(path: str) -> bytes: target = Path(path) if not target.is_absolute(): raise ValueError('absolute input path required') with target.open("rb") as stream: data = stream.read(524289) if len(data) > 524288: raise ValueError('receipt input too large') return data def same_json_types(left, right) -> bool: """JSON value correspondence only; never signature canonicalization.""" if type(left) is not type(right): return False if isinstance(left, dict): return left.keys() == right.keys() and all(same_json_types(value,right[key]) for key,value in left.items()) if isinstance(left, list): return len(left) == len(right) and all(same_json_types(a,b) for a,b in zip(left,right)) return left == right def stamp(value: str) -> datetime: parsed = datetime.fromisoformat(value.replace('Z','+00:00')) if parsed.tzinfo is None: raise ValueError('timezone required') return parsed.astimezone(timezone.utc) def verify(config: dict, *, now: datetime | None = None) -> dict: if not isinstance(config,dict) or set(config) != FIELDS or config['schema'] != 'access-engine.signer-receipt-check.v1': raise ValueError('unsupported admission fields') current = now or datetime.now(timezone.utc) caller = config['expected_caller'] if caller != {'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine','audience':'flex-auth'}: raise ValueError('retained native caller admission required') executable = Path(config['verifier']) if not executable.is_absolute() or not re.fullmatch('[a-f0-9]{64}',config['verifier_sha256']) or hashlib.sha256(executable.read_bytes()).hexdigest() != config['verifier_sha256']: raise ValueError('installed verifier digest mismatch') trust_bytes = read(config['trust_file']) trust = load_trust(Path(config['trust_file'])) if not same_json_types(trust,strict_json(trust_bytes)): raise ValueError('trust input changed during verification') if (trust['evaluator'],trust['mode'],trust['policy_package'],trust['policy_version']) != ('flex-auth/local','standalone','secrets-engine.catalog-lane.lifecycle','v2'): raise ValueError('signer-only lifecycle package required') checks = {} envelopes = {} inputs = {} requests = {} for label in ('allow','deny'): blobs = {suffix:read(config[label+'_'+suffix]) for suffix in ('request','envelope','record')} body = blobs['envelope'] # Strict decode before helper; authenticated typed return alone is acted on. strict_json(body.decode('utf-8')) envelope = authenticate_response(body,trust=trust,verifier=executable) request = strict_json(blobs['request']) record = strict_json(blobs['record']) if not same_json_types(record,envelope): raise ValueError('captured original source record differs from authenticated response') provenance = envelope['provenance']['caller'] if any(provenance.get(key) != value for key,value in caller.items()) or stamp(provenance['not_after']) <= current: raise ValueError('native caller provenance missing or expired') if label == 'allow': validate_decision_envelope(envelope,request,accepted_policy_packages={trust['policy_package']},accepted_policy_versions={trust['policy_version']},now=current) else: if envelope['effect'] != 'deny' or envelope['binding']['submitted_request_digest'] != request_digest(request): raise ValueError('deny receipt does not bind exact negative request') envelopes[label] = envelope requests[label] = request for suffix in ('request','envelope','record'): inputs[label+'_'+suffix] = hashlib.sha256(blobs[suffix]).hexdigest() checks[label+'_authenticated_bound_and_record_matched'] = True # These are local refusals against the captured real producer envelope. They # do not claim a rejected caller was exercised on the native HTTP endpoint. for label in ('altered','unsigned','unknown_kid','wrong_policy_pin'): bad = copy.deepcopy(envelopes['allow']); pins = copy.deepcopy(trust) if label == 'altered': bad['binding']['action'] = 'forged-action' elif label == 'unsigned': bad['signature'] = {'mode':'unsigned'} elif label == 'unknown_kid': bad['signature']['kid'] = 'unadmitted-key' else: pins['policy_package_digest'] = 'sha256:'+'0'*64 try: authenticate_response(json.dumps(bad).encode(),trust=pins,verifier=executable) except DecisionError: checks[label+'_refused'] = True else: raise ValueError('negative authentication case accepted') try: expiry=stamp(envelopes['allow']['lifetime']['expires_at']) request=requests['allow'] validate_decision_envelope(envelopes['allow'],request,accepted_policy_packages={trust['policy_package']},accepted_policy_versions={trust['policy_version']},now=expiry) except DecisionError: checks['expiry_boundary_refused'] = True else: raise ValueError('expired allow accepted') return {'schema':'access-engine.signer-receipt-result.v1','checks':checks,'input_sha256':inputs,'verifier_sha256':config['verifier_sha256'],'trust_file_sha256':hashlib.sha256(trust_bytes).hexdigest(),'boundary':'offline captured signed receipt checks only; no native caller-denial, record durability, receiver custody, trust admission, rotation or protected effect inferred'} def main() -> int: parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--admission',type=Path,required=True) parser.add_argument('--output',type=Path,required=True) args=parser.parse_args() try: config=strict_json(read(str(args.admission))) result=verify(config) args.output.write_text(json.dumps(result,indent=2)+'\n') except (OSError,ValueError,TypeError,KeyError,RecursionError,DecisionError): # Never dump source objects, unexpected key material or verifier stderr. print('signer receipt verification refused') return 1 print('captured signer receipt checks passed') return 0 if __name__ == '__main__': raise SystemExit(main())