Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
135 lines
7.2 KiB
Python
135 lines
7.2 KiB
Python
#!/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())
|